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
main
Levi Bard 7 years ago committed by Konrad Pozniak
parent 4e350b4f87
commit a1eba23229
  1. 92
      app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java
  2. 2
      app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
  3. 13
      app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
  4. 9
      app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt
  5. 6
      app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt
  6. 46
      app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt
  7. 4
      app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java
  8. 23
      app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt

@ -31,6 +31,7 @@ import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.Parcel; import android.os.Parcel;
@ -85,11 +86,12 @@ import com.keylesspalace.tusky.adapter.MentionAutoCompleteAdapter;
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener; import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener;
import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager; 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.di.Injectable;
import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Instance;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.ProgressRequestBody; import com.keylesspalace.tusky.network.ProgressRequestBody;
@ -124,6 +126,7 @@ import java.io.InputStream;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -200,6 +203,8 @@ public final class ComposeActivity
private int currentFlags; private int currentFlags;
private Uri photoUploadUri; private Uri photoUploadUri;
private int savedTootUid = 0; private int savedTootUid = 0;
private List<Emoji> emojiList;
private int maximumTootCharacters = STATUS_CHARACTER_LIMIT;
private SaveTootHelper saveTootHelper; private SaveTootHelper saveTootHelper;
@ -222,6 +227,7 @@ public final class ComposeActivity
emojiButton = findViewById(R.id.composeEmojiButton); emojiButton = findViewById(R.id.composeEmojiButton);
hideMediaToggle = findViewById(R.id.composeHideMediaButton); hideMediaToggle = findViewById(R.id.composeHideMediaButton);
emojiView = findViewById(R.id.emojiView); emojiView = findViewById(R.id.emojiView);
emojiList = Collections.emptyList();
saveTootHelper = new SaveTootHelper(TuskyApplication.getDB().tootDao(), this); saveTootHelper = new SaveTootHelper(TuskyApplication.getDB().tootDao(), this);
@ -258,6 +264,37 @@ public final class ComposeActivity
getString(R.string.compose_active_account_description, getString(R.string.compose_active_account_description,
activeAccount.getFullName())); 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 { } else {
// do not do anything when not logged in, activity will be finished in super.onCreate() anyway // do not do anything when not logged in, activity will be finished in super.onCreate() anyway
return; return;
@ -277,34 +314,6 @@ public final class ComposeActivity
enableButton(emojiButton, false, false); 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. // Setup the interface buttons.
tootButton.setOnClickListener(v -> onSendClicked()); tootButton.setOnClickListener(v -> onSendClicked());
pickButton.setOnClickListener(v -> openPickDialog()); pickButton.setOnClickListener(v -> openPickDialog());
@ -761,7 +770,7 @@ public final class ComposeActivity
} }
private void updateVisibleCharactersLeft() { private void updateVisibleCharactersLeft() {
int charactersLeft = STATUS_CHARACTER_LIMIT - textEditor.length(); int charactersLeft = maximumTootCharacters - textEditor.length();
if (statusHideText) { if (statusHideText) {
charactersLeft -= contentWarningEditor.length(); charactersLeft -= contentWarningEditor.length();
} }
@ -914,7 +923,7 @@ public final class ComposeActivity
if (characterCount <= 0 && mediaQueued.size()==0) { if (characterCount <= 0 && mediaQueued.size()==0) {
textEditor.setError(getString(R.string.error_empty)); textEditor.setError(getString(R.string.error_empty));
enableButtons(); enableButtons();
} else if (characterCount <= STATUS_CHARACTER_LIMIT) { } else if (characterCount <= maximumTootCharacters) {
sendStatus(contentText, visibility, sensitive, spoilerText); sendStatus(contentText, visibility, sensitive, spoilerText);
} else { } else {
@ -1423,6 +1432,27 @@ public final class ComposeActivity
textEditor.getText().insert(textEditor.getSelectionStart(), ":"+shortcode+": "); 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 { public static final class QueuedMedia {
Type type; Type type;
ProgressImageView preview; ProgressImageView preview;

@ -79,7 +79,7 @@ public class TuskyApplication extends Application implements HasActivityInjector
db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "tuskyDB") db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "tuskyDB")
.allowMainThreadQueries() .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(); .build();
accountManager = new AccountManager(db); accountManager = new AccountManager(db);
serviceLocator = new ServiceLocator() { serviceLocator = new ServiceLocator() {

@ -25,12 +25,12 @@ import android.support.annotation.NonNull;
* DB version & declare DAO * 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 class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao(); public abstract TootDao tootDao();
public abstract AccountDao accountDao(); public abstract AccountDao accountDao();
public abstract EmojiListDao emojiListDao(); public abstract InstanceDao instanceDao();
public static final Migration MIGRATION_2_3 = new Migration(2, 3) { public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override @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`))"); 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 import android.arch.persistence.room.Query
@Dao @Dao
interface EmojiListDao { interface InstanceDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(emojiList: EmojiListEntity) fun insertOrReplace(instance: InstanceEntity)
@Query("SELECT * FROM EmojiListEntity WHERE instance = :instance LIMIT 1")
fun loadEmojisForInstance(instance: String): EmojiListEntity?
@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 @Entity
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
data class EmojiListEntity(@field:PrimaryKey var instance: String, data class InstanceEntity(
val emojiList: List<Emoji>) @field:PrimaryKey var instance: String,
val emojiList: List<Emoji>?,
val maximumTootCharacters: Int?)
class Converters { class Converters {

@ -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.Attachment;
import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Instance;
import com.keylesspalace.tusky.entity.MastoList; import com.keylesspalace.tusky.entity.MastoList;
import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Relationship; import com.keylesspalace.tusky.entity.Relationship;
@ -270,4 +271,7 @@ public interface MastodonApi {
@GET("/api/v1/custom_emojis") @GET("/api/v1/custom_emojis")
Call<List<Emoji>> getCustomEmojis(); 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.AccountEntity
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import okhttp3.Request import okhttp3.Request
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
@ -103,6 +104,28 @@ class ComposeActivityTest {
override fun enqueue(callback: Callback<List<Emoji>>?) {} 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.mastodonApi = apiMock
activity.accountManager = accountManagerMock activity.accountManager = accountManagerMock

Loading…
Cancel
Save