From e2adddf7b8a31004ea9a50e8aaa467d731aaeed9 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Sun, 22 Apr 2018 10:35:46 +0200 Subject: [PATCH] Query instance for toot character limit (#571) * Query instance for toot character limit Fixes #393 * Move maximumTootCharacters to instance field * Add caching for maximum toot characters, expanding on the emoji list storage * Update formatting per review feedback * Fix compose activity tests * Rename mastodon api point for nicer interaction with kotlin * Default emoji list to empty list instead of null, to appease json converters in failure cases * Use empty list helper * Fix database migration --- .../keylesspalace/tusky/ComposeActivity.java | 92 ++++++++++++------- .../keylesspalace/tusky/TuskyApplication.java | 2 +- .../keylesspalace/tusky/db/AppDatabase.java | 13 ++- .../db/{EmojiListDao.kt => InstanceDao.kt} | 9 +- .../{EmojiListEntity.kt => InstanceEntity.kt} | 6 +- .../keylesspalace/tusky/entity/Instance.kt | 46 ++++++++++ .../tusky/network/MastodonApi.java | 4 + .../tusky/ComposeActivityTest.kt | 23 +++++ 8 files changed, 154 insertions(+), 41 deletions(-) rename app/src/main/java/com/keylesspalace/tusky/db/{EmojiListDao.kt => InstanceDao.kt} (81%) rename app/src/main/java/com/keylesspalace/tusky/db/{EmojiListEntity.kt => InstanceEntity.kt} (90%) create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index 3eb088a0..54d54174 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -31,6 +31,7 @@ import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Parcel; @@ -85,11 +86,12 @@ import com.keylesspalace.tusky.adapter.MentionAutoCompleteAdapter; import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener; import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountManager; -import com.keylesspalace.tusky.db.EmojiListEntity; +import com.keylesspalace.tusky.db.InstanceEntity; import com.keylesspalace.tusky.di.Injectable; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.entity.Instance; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.ProgressRequestBody; @@ -124,6 +126,7 @@ import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; @@ -200,6 +203,8 @@ public final class ComposeActivity private int currentFlags; private Uri photoUploadUri; private int savedTootUid = 0; + private List emojiList; + private int maximumTootCharacters = STATUS_CHARACTER_LIMIT; private SaveTootHelper saveTootHelper; @@ -222,6 +227,7 @@ public final class ComposeActivity emojiButton = findViewById(R.id.composeEmojiButton); hideMediaToggle = findViewById(R.id.composeHideMediaButton); emojiView = findViewById(R.id.emojiView); + emojiList = Collections.emptyList(); saveTootHelper = new SaveTootHelper(TuskyApplication.getDB().tootDao(), this); @@ -258,6 +264,37 @@ public final class ComposeActivity getString(R.string.compose_active_account_description, activeAccount.getFullName())); + mastodonApi.getInstance().enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body().getMaxTootChars() != null) { + maximumTootCharacters = response.body().getMaxTootChars(); + updateVisibleCharactersLeft(); + cacheInstanceMetadata(activeAccount); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.w(TAG, "error loading instance data", t); + loadCachedInstanceMetadata(activeAccount); + } + }); + + mastodonApi.getCustomEmojis().enqueue(new Callback>() { + @Override + public void onResponse(@NonNull Call> call, @NonNull Response> response) { + emojiList = response.body(); + enableButton(emojiButton, true, emojiList.size() > 0); + cacheInstanceMetadata(activeAccount); + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + Log.w(TAG, "error loading custom emojis", t); + loadCachedInstanceMetadata(activeAccount); + } + }); } else { // do not do anything when not logged in, activity will be finished in super.onCreate() anyway return; @@ -277,34 +314,6 @@ public final class ComposeActivity enableButton(emojiButton, false, false); - mastodonApi.getCustomEmojis().enqueue(new Callback>() { - @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { - List emojiList = response.body(); - - if (emojiList != null) { - emojiView.setAdapter(new EmojiAdapter(emojiList, ComposeActivity.this)); - - enableButton(emojiButton, true, emojiList.size() > 0); - - EmojiListEntity emojiListEntity = new EmojiListEntity(activeAccount.getDomain(), emojiList); - - TuskyApplication.getDB().emojiListDao().insertOrReplace(emojiListEntity); - } - } - - @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { - Log.w(TAG, "error loading custom emojis", t); - EmojiListEntity emojiListEntity = TuskyApplication.getDB().emojiListDao().loadEmojisForInstance(activeAccount.getDomain()); - - if(emojiListEntity != null) { - emojiView.setAdapter(new EmojiAdapter(emojiListEntity.getEmojiList(), ComposeActivity.this)); - enableButton(emojiButton, true, emojiListEntity.getEmojiList().size() > 0); - } - } - }); - // Setup the interface buttons. tootButton.setOnClickListener(v -> onSendClicked()); pickButton.setOnClickListener(v -> openPickDialog()); @@ -761,7 +770,7 @@ public final class ComposeActivity } private void updateVisibleCharactersLeft() { - int charactersLeft = STATUS_CHARACTER_LIMIT - textEditor.length(); + int charactersLeft = maximumTootCharacters - textEditor.length(); if (statusHideText) { charactersLeft -= contentWarningEditor.length(); } @@ -914,7 +923,7 @@ public final class ComposeActivity if (characterCount <= 0 && mediaQueued.size()==0) { textEditor.setError(getString(R.string.error_empty)); enableButtons(); - } else if (characterCount <= STATUS_CHARACTER_LIMIT) { + } else if (characterCount <= maximumTootCharacters) { sendStatus(contentText, visibility, sensitive, spoilerText); } else { @@ -1423,6 +1432,27 @@ public final class ComposeActivity textEditor.getText().insert(textEditor.getSelectionStart(), ":"+shortcode+": "); } + private void loadCachedInstanceMetadata(@NotNull AccountEntity activeAccount) + { + InstanceEntity instanceEntity = TuskyApplication.getDB().instanceDao().loadMetadataForInstance(activeAccount.getDomain()); + + if(instanceEntity != null) { + Integer max = instanceEntity.getMaximumTootCharacters(); + maximumTootCharacters = (max == null ? STATUS_CHARACTER_LIMIT : max.intValue()); + emojiList = instanceEntity.getEmojiList(); + if (emojiList != null) { + emojiView.setAdapter(new EmojiAdapter(emojiList, ComposeActivity.this)); + enableButton(emojiButton, true, emojiList.size() > 0); + } + } + } + + private void cacheInstanceMetadata(@NotNull AccountEntity activeAccount) + { + InstanceEntity instanceEntity = new InstanceEntity(activeAccount.getDomain(), emojiList, maximumTootCharacters); + TuskyApplication.getDB().instanceDao().insertOrReplace(instanceEntity); + } + public static final class QueuedMedia { Type type; ProgressImageView preview; diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java index 4d6089c2..a7be757c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java @@ -79,7 +79,7 @@ public class TuskyApplication extends Application implements HasActivityInjector db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "tuskyDB") .allowMainThreadQueries() - .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, AppDatabase.MIGRATION_5_6) + .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7) .build(); accountManager = new AccountManager(db); serviceLocator = new ServiceLocator() { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index e6e50fda..e2ec737c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -25,12 +25,12 @@ import android.support.annotation.NonNull; * DB version & declare DAO */ -@Database(entities = {TootEntity.class, AccountEntity.class, EmojiListEntity.class}, version = 6, exportSchema = false) +@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class}, version = 7, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { public abstract TootDao tootDao(); public abstract AccountDao accountDao(); - public abstract EmojiListDao emojiListDao(); + public abstract InstanceDao instanceDao(); public static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override @@ -82,4 +82,13 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("CREATE TABLE IF NOT EXISTS `EmojiListEntity` (`instance` TEXT NOT NULL, `emojiList` TEXT NOT NULL, PRIMARY KEY(`instance`))"); } }; + + public static final Migration MIGRATION_6_7 = new Migration(6, 7) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `InstanceEntity` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))"); + database.execSQL("INSERT OR REPLACE INTO `InstanceEntity` SELECT `instance`,`emojiList`,NULL FROM `EmojiListEntity`;"); + database.execSQL("DROP TABLE `EmojiListEntity`;"); + } + }; } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/EmojiListDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt similarity index 81% rename from app/src/main/java/com/keylesspalace/tusky/db/EmojiListDao.kt rename to app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt index 8630b165..8ed2d75d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/EmojiListDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt @@ -21,11 +21,10 @@ import android.arch.persistence.room.OnConflictStrategy import android.arch.persistence.room.Query @Dao -interface EmojiListDao { +interface InstanceDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertOrReplace(emojiList: EmojiListEntity) - - @Query("SELECT * FROM EmojiListEntity WHERE instance = :instance LIMIT 1") - fun loadEmojisForInstance(instance: String): EmojiListEntity? + fun insertOrReplace(instance: InstanceEntity) + @Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1") + fun loadMetadataForInstance(instance: String): InstanceEntity? } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/EmojiListEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt similarity index 90% rename from app/src/main/java/com/keylesspalace/tusky/db/EmojiListEntity.kt rename to app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt index ac87e8bf..aba14f58 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/EmojiListEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt @@ -25,8 +25,10 @@ import com.keylesspalace.tusky.entity.Emoji @Entity @TypeConverters(Converters::class) -data class EmojiListEntity(@field:PrimaryKey var instance: String, - val emojiList: List) +data class InstanceEntity( + @field:PrimaryKey var instance: String, + val emojiList: List?, + val maximumTootCharacters: Int?) class Converters { diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt new file mode 100644 index 00000000..ee440247 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -0,0 +1,46 @@ +/* Copyright 2018 Levi Bard + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName +import java.util.* + +data class Instance ( + val uri: String, + val title: String, + val description: String, + val email: String, + val version: String, + val urls: Map, + val stats: Map?, + val thumbnail: String?, + val languages: List, + @SerializedName("contact_account") val contactAccount: Account, + @SerializedName("max_toot_chars") val maxTootChars: Int? +) { + override fun hashCode(): Int { + return uri.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other !is Instance) { + return false + } + val instance = other as Instance? + return instance?.uri.equals(uri) + } +} + diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java index 74fde587..dacbe483 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java @@ -23,6 +23,7 @@ import com.keylesspalace.tusky.entity.AppCredentials; import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.entity.Instance; import com.keylesspalace.tusky.entity.MastoList; import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Relationship; @@ -270,4 +271,7 @@ public interface MastodonApi { @GET("/api/v1/custom_emojis") Call> getCustomEmojis(); + + @GET("api/v1/instance") + Call getInstance(); } diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index 4eb0df83..4a963fa9 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -20,6 +20,7 @@ import android.widget.EditText import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.network.MastodonApi import okhttp3.Request import org.junit.Assert.assertFalse @@ -103,6 +104,28 @@ class ComposeActivityTest { override fun enqueue(callback: Callback>?) {} }) + `when`(apiMock.instance).thenReturn(object: Call { + override fun isExecuted(): Boolean { + return false + } + override fun clone(): Call { + throw Error("not implemented") + } + override fun isCanceled(): Boolean { + throw Error("not implemented") + } + override fun cancel() { + throw Error("not implemented") + } + override fun execute(): Response { + throw Error("not implemented") + } + override fun request(): Request { + throw Error("not implemented") + } + + override fun enqueue(callback: Callback?) {} + }) activity.mastodonApi = apiMock activity.accountManager = accountManagerMock