Add autocompletion for custom emoji (#1089)

* Remove unnecessary //noinspection ConstantConditions

* Add autocompletion for custom emoji

* Update MentionTagTokenizer tests for emoji autocomplete support

* Move 1) emoji list retrieval notifying and 2) setting of emojiList field into setEmojiList() method of ComposeActivity

* Convert RelativeLayout in item_autocomplete_emoji.xml to LinearLayout

* Rename MentionTag* to Compose*

* Improve emoji autocomplete matching

* Make hashtag autocomplete results bold

* Use Context.getString()'s built-in formatting

* Add a divider between emoji autocomplete results that *start with* the token and those that *contain* it
This commit is contained in:
autumnontape 2019-03-04 10:28:08 -08:00 committed by Konrad Pozniak
parent d43b4fef4b
commit 10fcee4798
14 changed files with 232 additions and 38 deletions

View file

@ -66,7 +66,7 @@ import com.google.android.material.snackbar.Snackbar;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.keylesspalace.tusky.adapter.EmojiAdapter; import com.keylesspalace.tusky.adapter.EmojiAdapter;
import com.keylesspalace.tusky.adapter.MentionTagAutoCompleteAdapter; import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter;
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.AppDatabase; import com.keylesspalace.tusky.db.AppDatabase;
@ -84,7 +84,7 @@ import com.keylesspalace.tusky.service.SendTootService;
import com.keylesspalace.tusky.util.CountUpDownLatch; import com.keylesspalace.tusky.util.CountUpDownLatch;
import com.keylesspalace.tusky.util.DownsizeImageTask; import com.keylesspalace.tusky.util.DownsizeImageTask;
import com.keylesspalace.tusky.util.ListUtils; import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.MentionTagTokenizer; import com.keylesspalace.tusky.util.ComposeTokenizer;
import com.keylesspalace.tusky.util.SaveTootHelper; import com.keylesspalace.tusky.util.SaveTootHelper;
import com.keylesspalace.tusky.util.SpanUtilsKt; import com.keylesspalace.tusky.util.SpanUtilsKt;
import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.util.StringUtils;
@ -111,6 +111,7 @@ import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import javax.inject.Inject; import javax.inject.Inject;
@ -157,7 +158,7 @@ import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvid
public final class ComposeActivity public final class ComposeActivity
extends BaseActivity extends BaseActivity
implements ComposeOptionsListener, implements ComposeOptionsListener,
MentionTagAutoCompleteAdapter.AutocompletionProvider, ComposeAutoCompleteAdapter.AutocompletionProvider,
OnEmojiSelectedListener, OnEmojiSelectedListener,
Injectable, InputConnectionCompat.OnCommitContentListener { Injectable, InputConnectionCompat.OnCommitContentListener {
@ -226,6 +227,7 @@ public final class ComposeActivity
private Uri photoUploadUri; private Uri photoUploadUri;
private int savedTootUid = 0; private int savedTootUid = 0;
private List<Emoji> emojiList; private List<Emoji> emojiList;
private CountDownLatch emojiListRetrievalLatch = new CountDownLatch(1);
private int maximumTootCharacters = STATUS_CHARACTER_LIMIT; private int maximumTootCharacters = STATUS_CHARACTER_LIMIT;
private @Px private @Px
int thumbnailViewSize; int thumbnailViewSize;
@ -313,7 +315,7 @@ public final class ComposeActivity
mastodonApi.getCustomEmojis().enqueue(new Callback<List<Emoji>>() { mastodonApi.getCustomEmojis().enqueue(new Callback<List<Emoji>>() {
@Override @Override
public void onResponse(@NonNull Call<List<Emoji>> call, @NonNull Response<List<Emoji>> response) { public void onResponse(@NonNull Call<List<Emoji>> call, @NonNull Response<List<Emoji>> response) {
emojiList = response.body(); List<Emoji> emojiList = response.body();
if(emojiList == null) { if(emojiList == null) {
emojiList = Collections.emptyList(); emojiList = Collections.emptyList();
} }
@ -519,8 +521,8 @@ public final class ComposeActivity
}); });
textEditor.setAdapter( textEditor.setAdapter(
new MentionTagAutoCompleteAdapter(this)); new ComposeAutoCompleteAdapter(this));
textEditor.setTokenizer(new MentionTagTokenizer()); textEditor.setTokenizer(new ComposeTokenizer());
// Add any mentions to the text field when a reply is first composed. // Add any mentions to the text field when a reply is first composed.
if (mentionedUsernames != null) { if (mentionedUsernames != null) {
@ -1545,7 +1547,7 @@ public final class ComposeActivity
} }
@Override @Override
public List<MentionTagAutoCompleteAdapter.AutocompleteResult> search(String token) { public List<ComposeAutoCompleteAdapter.AutocompleteResult> search(String token) {
try { try {
switch (token.charAt(0)) { switch (token.charAt(0)) {
case '@': case '@':
@ -1557,18 +1559,53 @@ public final class ComposeActivity
if (accountList != null) { if (accountList != null) {
resultList.addAll(accountList); resultList.addAll(accountList);
} }
return CollectionsKt.map(resultList, MentionTagAutoCompleteAdapter.AccountResult::new); return CollectionsKt.map(resultList, ComposeAutoCompleteAdapter.AccountResult::new);
case '#': case '#':
Response<SearchResults> response = mastodonApi.search(token, false).execute(); Response<SearchResults> response = mastodonApi.search(token, false).execute();
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
return CollectionsKt.map( return CollectionsKt.map(
response.body().getHashtags(), response.body().getHashtags(),
MentionTagAutoCompleteAdapter.HashtagResult::new ComposeAutoCompleteAdapter.HashtagResult::new
); );
} else { } else {
Log.e(TAG, String.format("Autocomplete search for %s failed.", token)); Log.e(TAG, String.format("Autocomplete search for %s failed.", token));
return Collections.emptyList(); return Collections.emptyList();
} }
case ':':
try {
emojiListRetrievalLatch.await();
} catch (InterruptedException e) {
Log.e(TAG, String.format("Autocomplete search for %s was interrupted.", token));
return Collections.emptyList();
}
if (emojiList != null) {
String incomplete = token.substring(1).toLowerCase();
List<ComposeAutoCompleteAdapter.AutocompleteResult> results =
new ArrayList<>();
List<ComposeAutoCompleteAdapter.AutocompleteResult> resultsInside =
new ArrayList<>();
for (Emoji emoji : emojiList) {
String shortcode = emoji.getShortcode().toLowerCase();
if (shortcode.startsWith(incomplete)) {
results.add(new ComposeAutoCompleteAdapter.EmojiResult(emoji));
} else if (shortcode.indexOf(incomplete, 1) != -1) {
resultsInside.add(new ComposeAutoCompleteAdapter.EmojiResult(emoji));
}
}
if (!results.isEmpty() && !resultsInside.isEmpty()) {
// both lists have results. include a separator between them.
results.add(new ComposeAutoCompleteAdapter.ResultSeparator());
}
results.addAll(resultsInside);
return results;
} else {
return Collections.emptyList();
}
default: default:
Log.w(TAG, "Unexpected autocompletion token: " + token); Log.w(TAG, "Unexpected autocompletion token: " + token);
return Collections.emptyList(); return Collections.emptyList();
@ -1591,13 +1628,16 @@ public final class ComposeActivity
if(instanceEntity != null) { if(instanceEntity != null) {
Integer max = instanceEntity.getMaximumTootCharacters(); Integer max = instanceEntity.getMaximumTootCharacters();
maximumTootCharacters = (max == null ? STATUS_CHARACTER_LIMIT : max); maximumTootCharacters = (max == null ? STATUS_CHARACTER_LIMIT : max);
emojiList = instanceEntity.getEmojiList(); setEmojiList(instanceEntity.getEmojiList());
setEmojiList(emojiList);
updateVisibleCharactersLeft(); updateVisibleCharactersLeft();
} }
} }
private void setEmojiList(@Nullable List<Emoji> emojiList) { private void setEmojiList(@Nullable List<Emoji> emojiList) {
this.emojiList = emojiList;
emojiListRetrievalLatch.countDown();
if (emojiList != null) { if (emojiList != null) {
emojiView.setAdapter(new EmojiAdapter(emojiList, ComposeActivity.this)); emojiView.setAdapter(new EmojiAdapter(emojiList, ComposeActivity.this));
enableButton(emojiButton, true, emojiList.size() > 0); enableButton(emojiButton, true, emojiList.size() > 0);

View file

@ -27,6 +27,7 @@ import android.widget.TextView;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
@ -40,15 +41,17 @@ import androidx.annotation.Nullable;
* Created by charlag on 12/11/17. * Created by charlag on 12/11/17.
*/ */
public class MentionTagAutoCompleteAdapter extends BaseAdapter public class ComposeAutoCompleteAdapter extends BaseAdapter
implements Filterable { implements Filterable {
private static final int ACCOUNT_VIEW_TYPE = 0; private static final int ACCOUNT_VIEW_TYPE = 1;
private static final int HASHTAG_VIEW_TYPE = 1; private static final int HASHTAG_VIEW_TYPE = 2;
private static final int EMOJI_VIEW_TYPE = 3;
private static final int SEPARATOR_VIEW_TYPE = 0;
private final ArrayList<AutocompleteResult> resultList; private final ArrayList<AutocompleteResult> resultList;
private final AutocompletionProvider autocompletionProvider; private final AutocompletionProvider autocompletionProvider;
public MentionTagAutoCompleteAdapter(AutocompletionProvider autocompletionProvider) { public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider) {
super(); super();
resultList = new ArrayList<>(); resultList = new ArrayList<>();
this.autocompletionProvider = autocompletionProvider; this.autocompletionProvider = autocompletionProvider;
@ -77,8 +80,12 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
public CharSequence convertResultToString(Object resultValue) { public CharSequence convertResultToString(Object resultValue) {
if (resultValue instanceof AccountResult) { if (resultValue instanceof AccountResult) {
return formatUsername(((AccountResult) resultValue)); return formatUsername(((AccountResult) resultValue));
} else { } else if (resultValue instanceof HashtagResult) {
return formatHashtag((HashtagResult) resultValue); return formatHashtag((HashtagResult) resultValue);
} else if (resultValue instanceof EmojiResult) {
return formatEmoji((EmojiResult) resultValue);
} else {
return "";
} }
} }
@ -117,9 +124,8 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
switch (getItemViewType(position)) { switch (getItemViewType(position)) {
case ACCOUNT_VIEW_TYPE: case ACCOUNT_VIEW_TYPE:
AccountViewHolder holder; AccountViewHolder accountViewHolder;
if (convertView == null) { if (convertView == null) {
//noinspection ConstantConditions
view = ((LayoutInflater) context view = ((LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE)) .getSystemService(Context.LAYOUT_INFLATER_SERVICE))
.inflate(R.layout.item_autocomplete_account, parent, false); .inflate(R.layout.item_autocomplete_account, parent, false);
@ -127,22 +133,24 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
if (view.getTag() == null) { if (view.getTag() == null) {
view.setTag(new AccountViewHolder(view)); view.setTag(new AccountViewHolder(view));
} }
holder = (AccountViewHolder) view.getTag(); accountViewHolder = (AccountViewHolder) view.getTag();
AccountResult accountResult = ((AccountResult) getItem(position)); AccountResult accountResult = ((AccountResult) getItem(position));
if (accountResult != null) { if (accountResult != null) {
Account account = accountResult.account; Account account = accountResult.account;
String format = context.getString(R.string.status_username_format); String formattedUsername = context.getString(
String formattedUsername = String.format(format, account.getUsername()); R.string.status_username_format,
holder.username.setText(formattedUsername); account.getUsername()
);
accountViewHolder.username.setText(formattedUsername);
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(account.getName(), CharSequence emojifiedName = CustomEmojiHelper.emojifyString(account.getName(),
account.getEmojis(), holder.displayName); account.getEmojis(), accountViewHolder.displayName);
holder.displayName.setText(emojifiedName); accountViewHolder.displayName.setText(emojifiedName);
if (!account.getAvatar().isEmpty()) { if (!account.getAvatar().isEmpty()) {
Picasso.with(context) Picasso.with(context)
.load(account.getAvatar()) .load(account.getAvatar())
.placeholder(R.drawable.avatar_default) .placeholder(R.drawable.avatar_default)
.into(holder.avatar); .into(accountViewHolder.avatar);
} }
} }
break; break;
@ -151,7 +159,7 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
if (convertView == null) { if (convertView == null) {
view = ((LayoutInflater) context view = ((LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE)) .getSystemService(Context.LAYOUT_INFLATER_SERVICE))
.inflate(R.layout.item_hashtag, parent, false); .inflate(R.layout.item_autocomplete_hashtag, parent, false);
} }
HashtagResult result = (HashtagResult) getItem(position); HashtagResult result = (HashtagResult) getItem(position);
@ -159,6 +167,40 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
((TextView) view).setText(formatHashtag(result)); ((TextView) view).setText(formatHashtag(result));
} }
break; break;
case EMOJI_VIEW_TYPE:
EmojiViewHolder emojiViewHolder;
if (convertView == null) {
view = ((LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
.inflate(R.layout.item_autocomplete_emoji, parent, false);
}
if (view.getTag() == null) {
view.setTag(new EmojiViewHolder(view));
}
emojiViewHolder = (EmojiViewHolder) view.getTag();
EmojiResult emojiResult = ((EmojiResult) getItem(position));
if (emojiResult != null) {
Emoji emoji = emojiResult.emoji;
String formattedShortcode = context.getString(
R.string.emoji_shortcode_format,
emoji.getShortcode()
);
emojiViewHolder.shortcode.setText(formattedShortcode);
Picasso.with(context)
.load(emoji.getUrl())
.into(emojiViewHolder.preview);
}
break;
case SEPARATOR_VIEW_TYPE:
if (convertView == null) {
view = ((LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
.inflate(R.layout.item_autocomplete_divider, parent, false);
}
break;
default: default:
throw new AssertionError("unknown view type"); throw new AssertionError("unknown view type");
} }
@ -174,20 +216,41 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
return String.format("#%s", result.hashtag); return String.format("#%s", result.hashtag);
} }
private String formatEmoji(EmojiResult result) {
return String.format(":%s:", result.emoji.getShortcode());
}
@Override @Override
public int getViewTypeCount() { public int getViewTypeCount() {
return 2; return 4;
} }
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
if (getItem(position) instanceof AccountResult) { AutocompleteResult item = getItem(position);
if (item instanceof AccountResult) {
return ACCOUNT_VIEW_TYPE; return ACCOUNT_VIEW_TYPE;
} else { } else if (item instanceof HashtagResult) {
return HASHTAG_VIEW_TYPE; return HASHTAG_VIEW_TYPE;
} else if (item instanceof EmojiResult) {
return EMOJI_VIEW_TYPE;
} else {
return SEPARATOR_VIEW_TYPE;
} }
} }
@Override
public boolean areAllItemsEnabled() {
// there may be separators
return false;
}
@Override
public boolean isEnabled(int position) {
return !(getItem(position) instanceof ResultSeparator);
}
public abstract static class AutocompleteResult { public abstract static class AutocompleteResult {
AutocompleteResult() { AutocompleteResult() {
} }
@ -209,6 +272,16 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
} }
} }
public final static class EmojiResult extends AutocompleteResult {
private final Emoji emoji;
public EmojiResult(Emoji emoji) {
this.emoji = emoji;
}
}
public final static class ResultSeparator extends AutocompleteResult {}
public interface AutocompletionProvider { public interface AutocompletionProvider {
List<AutocompleteResult> search(String mention); List<AutocompleteResult> search(String mention);
} }
@ -224,4 +297,14 @@ public class MentionTagAutoCompleteAdapter extends BaseAdapter
avatar = view.findViewById(R.id.avatar); avatar = view.findViewById(R.id.avatar);
} }
} }
private class EmojiViewHolder {
final TextView shortcode;
final ImageView preview;
private EmojiViewHolder(View view) {
shortcode = view.findViewById(R.id.shortcode);
preview = view.findViewById(R.id.preview);
}
}
} }

View file

@ -20,14 +20,14 @@ import android.text.Spanned
import android.text.TextUtils import android.text.TextUtils
import android.widget.MultiAutoCompleteTextView import android.widget.MultiAutoCompleteTextView
class MentionTagTokenizer : MultiAutoCompleteTextView.Tokenizer { class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer {
override fun findTokenStart(text: CharSequence, cursor: Int): Int { override fun findTokenStart(text: CharSequence, cursor: Int): Int {
if (cursor == 0) { if (cursor == 0) {
return cursor return cursor
} }
var i = cursor var i = cursor
var character = text[i - 1] var character = text[i - 1]
while (i > 0 && character != '@' && character != '#') { while (i > 0 && character != '@' && character != '#' && character != ':') {
// See SpanUtils.MENTION_REGEX // See SpanUtils.MENTION_REGEX
if (!Character.isLetterOrDigit(character) && character != '_') { if (!Character.isLetterOrDigit(character) && character != '_') {
return cursor return cursor
@ -36,7 +36,7 @@ class MentionTagTokenizer : MultiAutoCompleteTextView.Tokenizer {
character = if (i == 0) ' ' else text[i - 1] character = if (i == 0) ' ' else text[i - 1]
} }
if (i < 1 if (i < 1
|| (character != '@' && character != '#') || (character != '@' && character != '#' && character != ':')
|| i > 1 && !Character.isWhitespace(text[i - 2])) { || i > 1 && !Character.isWhitespace(text[i - 2])) {
return cursor return cursor
} }

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:height="1dp" />
<solid android:color="@color/autocomplete_divider_dark" />
</shape>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:height="1dp" />
<solid android:color="@color/status_divider_light" />
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/autocomplete_divider_drawable" />

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:id="@+id/preview"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:contentDescription="@null"
android:padding="4dp" />
<TextView
android:id="@+id/shortcode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_large"
android:textStyle="normal|bold"
tools:text=":mastodon:" />
</LinearLayout>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/hashtag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="?attr/status_text_medium"
android:textStyle="normal|bold" />

View file

@ -72,6 +72,8 @@
<item name="compound_button_color">@color/compound_button_color_dark</item> <item name="compound_button_color">@color/compound_button_color_dark</item>
<item name="autocomplete_divider_drawable">@drawable/autocomplete_divider_dark</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item> <item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
<item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size --> <item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size -->

View file

@ -38,6 +38,7 @@
<attr name="card_background" format="reference|color" /> <attr name="card_background" format="reference|color" />
<attr name="card_image_background" format="reference|color" /> <attr name="card_image_background" format="reference|color" />
<attr name="compound_button_color" format="reference" /> <attr name="compound_button_color" format="reference" />
<attr name="autocomplete_divider_drawable" format="reference" />
<attr name="play_indicator_drawable" format="reference" /> <attr name="play_indicator_drawable" format="reference" />
<attr name="status_text_small" format="dimension" /> <attr name="status_text_small" format="dimension" />

View file

@ -28,6 +28,7 @@
<color name="compose_media_button_disabled_dark">#586173</color> <color name="compose_media_button_disabled_dark">#586173</color>
<color name="custom_tab_toolbar_dark">#313543</color> <color name="custom_tab_toolbar_dark">#313543</color>
<color name="compose_reply_content_background_dark">#373c4b</color> <color name="compose_reply_content_background_dark">#373c4b</color>
<color name="autocomplete_divider_dark">#424a5b</color>
<!--Black Theme Colors--> <!--Black Theme Colors-->
<color name="color_primary_black">#000000</color> <color name="color_primary_black">#000000</color>
<color name="color_primary_dark_black">#111111</color> <!--Dark Dark--> <color name="color_primary_dark_black">#111111</color> <!--Dark Dark-->

View file

@ -355,6 +355,7 @@
<string name="caption_systememoji">Your device\'s default emoji set</string> <string name="caption_systememoji">Your device\'s default emoji set</string>
<string name="caption_blobmoji">The Blob emojis known from Android 4.47.1</string> <string name="caption_blobmoji">The Blob emojis known from Android 4.47.1</string>
<string name="caption_twemoji">Mastodon\'s standard emoji set</string> <string name="caption_twemoji">Mastodon\'s standard emoji set</string>
<string name="emoji_shortcode_format" translatable="false">:%s:</string>
<string name="download_failed">Download failed</string> <string name="download_failed">Download failed</string>

View file

@ -120,6 +120,8 @@
<item name="compound_button_color">@color/compound_button_color_light</item> <item name="compound_button_color">@color/compound_button_color_light</item>
<item name="autocomplete_divider_drawable">@drawable/autocomplete_divider_light</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item> <item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
<item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size --> <item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size -->

View file

@ -15,16 +15,16 @@
package com.keylesspalace.tusky package com.keylesspalace.tusky
import com.keylesspalace.tusky.util.MentionTagTokenizer import com.keylesspalace.tusky.util.ComposeTokenizer
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
@RunWith(Parameterized::class) @RunWith(Parameterized::class)
class MentionTagTokenizerTest(private val text: CharSequence, class ComposeTokenizerTest(private val text: CharSequence,
private val expectedStartIndex: Int, private val expectedStartIndex: Int,
private val expectedEndIndex: Int) { private val expectedEndIndex: Int) {
companion object { companion object {
@Parameterized.Parameters(name = "{0}") @Parameterized.Parameters(name = "{0}")
@ -50,12 +50,18 @@ class MentionTagTokenizerTest(private val text: CharSequence,
arrayOf("#tusky", 0, 6), arrayOf("#tusky", 0, 6),
arrayOf("#@tusky", 7, 7), arrayOf("#@tusky", 7, 7),
arrayOf("@#tusky", 7, 7), arrayOf("@#tusky", 7, 7),
arrayOf(" @#tusky", 8, 8) arrayOf(" @#tusky", 8, 8),
arrayOf(":mastodon", 0, 9),
arrayOf(":@mastodon", 10, 10),
arrayOf("@:mastodon", 10, 10),
arrayOf(" @:mastodon", 11, 11),
arrayOf("#@:mastodon", 11, 11),
arrayOf(" #@:mastodon", 12, 12)
) )
} }
} }
private val tokenizer = MentionTagTokenizer() private val tokenizer = ComposeTokenizer()
@Test @Test
fun tokenIndices_matchExpectations() { fun tokenIndices_matchExpectations() {