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
This commit is contained in:
parent
797132a643
commit
e2adddf7b8
8 changed files with 154 additions and 41 deletions
|
@ -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<Emoji> 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<Instance>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Instance> call, @NonNull Response<Instance> response) {
|
||||
if (response.isSuccessful() && response.body().getMaxTootChars() != null) {
|
||||
maximumTootCharacters = response.body().getMaxTootChars();
|
||||
updateVisibleCharactersLeft();
|
||||
cacheInstanceMetadata(activeAccount);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<Instance> call, @NonNull Throwable t) {
|
||||
Log.w(TAG, "error loading instance data", t);
|
||||
loadCachedInstanceMetadata(activeAccount);
|
||||
}
|
||||
});
|
||||
|
||||
mastodonApi.getCustomEmojis().enqueue(new Callback<List<Emoji>>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<List<Emoji>> call, @NonNull Response<List<Emoji>> response) {
|
||||
emojiList = response.body();
|
||||
enableButton(emojiButton, true, emojiList.size() > 0);
|
||||
cacheInstanceMetadata(activeAccount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<List<Emoji>> 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<List<Emoji>>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<List<Emoji>> call, @NonNull Response<List<Emoji>> response) {
|
||||
List<Emoji> 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<List<Emoji>> 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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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`;");
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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?
|
||||
}
|
|
@ -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<Emoji>)
|
||||
data class InstanceEntity(
|
||||
@field:PrimaryKey var instance: String,
|
||||
val emojiList: List<Emoji>?,
|
||||
val maximumTootCharacters: Int?)
|
||||
|
||||
|
||||
class Converters {
|
46
app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt
Normal file
46
app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt
Normal file
|
@ -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 <http://www.gnu.org/licenses>. */
|
||||
|
||||
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<String,String>,
|
||||
val stats: Map<String,Int>?,
|
||||
val thumbnail: String?,
|
||||
val languages: List<String>,
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<Emoji>> getCustomEmojis();
|
||||
|
||||
@GET("api/v1/instance")
|
||||
Call<Instance> getInstance();
|
||||
}
|
||||
|
|
|
@ -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<List<Emoji>>?) {}
|
||||
})
|
||||
`when`(apiMock.instance).thenReturn(object: Call<Instance> {
|
||||
override fun isExecuted(): Boolean {
|
||||
return false
|
||||
}
|
||||
override fun clone(): Call<Instance> {
|
||||
throw Error("not implemented")
|
||||
}
|
||||
override fun isCanceled(): Boolean {
|
||||
throw Error("not implemented")
|
||||
}
|
||||
override fun cancel() {
|
||||
throw Error("not implemented")
|
||||
}
|
||||
override fun execute(): Response<Instance> {
|
||||
throw Error("not implemented")
|
||||
}
|
||||
override fun request(): Request {
|
||||
throw Error("not implemented")
|
||||
}
|
||||
|
||||
override fun enqueue(callback: Callback<Instance>?) {}
|
||||
})
|
||||
|
||||
activity.mastodonApi = apiMock
|
||||
activity.accountManager = accountManagerMock
|
||||
|
|
Loading…
Reference in a new issue