From cec8f6dd65aa01588e47c7fdc170048cdb002166 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 17 May 2022 19:55:37 +0200 Subject: [PATCH] modernize autocomplete (#2510) * modernize autocomplete * use @WorkerThread annotation --- .../components/compose/ComposeActivity.kt | 4 +- .../compose/ComposeAutoCompleteAdapter.java | 320 ------------------ .../compose/ComposeAutoCompleteAdapter.kt | 175 ++++++++++ .../compose}/ComposeTokenizer.kt | 2 +- .../components/compose/ComposeViewModel.kt | 60 ++-- .../tusky/network/MastodonApi.kt | 18 + .../tusky/util/CallExtensions.kt | 23 ++ .../res/layout/item_autocomplete_account.xml | 89 +++-- .../res/layout/item_autocomplete_divider.xml | 5 - .../res/layout/item_autocomplete_emoji.xml | 18 +- .../res/layout/item_autocomplete_hashtag.xml | 10 +- .../tusky/ComposeTokenizerTest.kt | 2 +- 12 files changed, 314 insertions(+), 412 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.kt rename app/src/main/java/com/keylesspalace/tusky/{util => components/compose}/ComposeTokenizer.kt (98%) create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/CallExtensions.kt delete mode 100644 app/src/main/res/layout/item_autocomplete_divider.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index a6e8d677..57723c72 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -78,7 +78,6 @@ import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.ComposeTokenizer import com.keylesspalace.tusky.util.PickMediaFiles import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.afterTextChanged @@ -307,7 +306,8 @@ class ComposeActivity : ComposeAutoCompleteAdapter( this, preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), - preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), + preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true) ) ) binding.composeEditField.setTokenizer(ComposeTokenizer()) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java deleted file mode 100644 index 8a4f0ce1..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java +++ /dev/null @@ -1,320 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * 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.components.compose; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.Filter; -import android.widget.Filterable; -import android.widget.ImageView; -import android.widget.TextView; - -import com.bumptech.glide.Glide; -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.entity.Emoji; -import com.keylesspalace.tusky.entity.HashTag; -import com.keylesspalace.tusky.entity.TimelineAccount; -import com.keylesspalace.tusky.util.CustomEmojiHelper; -import com.keylesspalace.tusky.util.ImageLoadingHelper; - -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Created by charlag on 12/11/17. - */ - -public class ComposeAutoCompleteAdapter extends BaseAdapter - implements Filterable { - private static final int ACCOUNT_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 resultList; - private final AutocompletionProvider autocompletionProvider; - private final boolean animateAvatar; - private final boolean animateEmojis; - - public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider, boolean animateAvatar, boolean animateEmojis) { - super(); - resultList = new ArrayList<>(); - this.autocompletionProvider = autocompletionProvider; - this.animateAvatar = animateAvatar; - this.animateEmojis = animateEmojis; - } - - @Override - public int getCount() { - return resultList.size(); - } - - @Override - public AutocompleteResult getItem(int index) { - return resultList.get(index); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - @NonNull - public Filter getFilter() { - return new Filter() { - @Override - public CharSequence convertResultToString(Object resultValue) { - if (resultValue instanceof AccountResult) { - return formatUsername(((AccountResult) resultValue)); - } else if (resultValue instanceof HashtagResult) { - return formatHashtag((HashtagResult) resultValue); - } else if (resultValue instanceof EmojiResult) { - return formatEmoji((EmojiResult) resultValue); - } else { - return ""; - } - } - - // This method is invoked in a worker thread. - @Override - protected FilterResults performFiltering(CharSequence constraint) { - FilterResults filterResults = new FilterResults(); - if (constraint != null) { - List results = - autocompletionProvider.search(constraint.toString()); - filterResults.values = results; - filterResults.count = results.size(); - } - return filterResults; - } - - @SuppressWarnings("unchecked") - @Override - protected void publishResults(CharSequence constraint, FilterResults results) { - if (results != null && results.count > 0) { - resultList.clear(); - resultList.addAll((List) results.values); - notifyDataSetChanged(); - } else { - notifyDataSetInvalidated(); - } - } - }; - } - - @Override - @NonNull - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - View view = convertView; - final Context context = parent.getContext(); - - switch (getItemViewType(position)) { - case ACCOUNT_VIEW_TYPE: - AccountViewHolder accountViewHolder; - if (convertView == null) { - view = ((LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE)) - .inflate(R.layout.item_autocomplete_account, parent, false); - } - if (view.getTag() == null) { - view.setTag(new AccountViewHolder(view)); - } - accountViewHolder = (AccountViewHolder) view.getTag(); - - AccountResult accountResult = ((AccountResult) getItem(position)); - if (accountResult != null) { - TimelineAccount account = accountResult.account; - String formattedUsername = context.getString( - R.string.post_username_format, - account.getUsername() - ); - accountViewHolder.username.setText(formattedUsername); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), - account.getEmojis(), accountViewHolder.displayName, animateEmojis); - accountViewHolder.displayName.setText(emojifiedName); - - int avatarRadius = accountViewHolder.avatar.getContext().getResources() - .getDimensionPixelSize(R.dimen.avatar_radius_42dp); - - ImageLoadingHelper.loadAvatar( - account.getAvatar(), - accountViewHolder.avatar, - avatarRadius, - animateAvatar - ); - } - break; - - case HASHTAG_VIEW_TYPE: - if (convertView == null) { - view = ((LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE)) - .inflate(R.layout.item_autocomplete_hashtag, parent, false); - } - - HashtagResult result = (HashtagResult) getItem(position); - if (result != null) { - ((TextView) view).setText(formatHashtag(result)); - } - 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); - Glide.with(emojiViewHolder.preview) - .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: - throw new AssertionError("unknown view type"); - } - - return view; - } - - private static String formatUsername(AccountResult result) { - return String.format("@%s", result.account.getUsername()); - } - - private static String formatHashtag(HashtagResult result) { - return String.format("#%s", result.hashtag); - } - - private static String formatEmoji(EmojiResult result) { - return String.format(":%s:", result.emoji.getShortcode()); - } - - @Override - public int getViewTypeCount() { - return 4; - } - - @Override - public int getItemViewType(int position) { - AutocompleteResult item = getItem(position); - - if (item instanceof AccountResult) { - return ACCOUNT_VIEW_TYPE; - } else if (item instanceof HashtagResult) { - 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 { - AutocompleteResult() { - } - } - - public final static class AccountResult extends AutocompleteResult { - private final TimelineAccount account; - - public AccountResult(TimelineAccount account) { - this.account = account; - } - } - - public final static class HashtagResult extends AutocompleteResult { - private final String hashtag; - - public HashtagResult(HashTag hashtag) { - this.hashtag = hashtag.getName(); - } - } - - 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 { - List search(String mention); - } - - private class AccountViewHolder { - final TextView username; - final TextView displayName; - final ImageView avatar; - - private AccountViewHolder(View view) { - username = view.findViewById(R.id.username); - displayName = view.findViewById(R.id.display_name); - 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); - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.kt new file mode 100644 index 00000000..e825798c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.kt @@ -0,0 +1,175 @@ +/* Copyright 2022 Tusky Contributors + * + * 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.components.compose + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.Filter +import android.widget.Filterable +import androidx.annotation.WorkerThread +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding +import com.keylesspalace.tusky.databinding.ItemAutocompleteEmojiBinding +import com.keylesspalace.tusky.databinding.ItemAutocompleteHashtagBinding +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.visible + +class ComposeAutoCompleteAdapter( + private val autocompletionProvider: AutocompletionProvider, + private val animateAvatar: Boolean, + private val animateEmojis: Boolean, + private val showBotBadge: Boolean +) : BaseAdapter(), Filterable { + + private var resultList: List = emptyList() + + override fun getCount() = resultList.size + + override fun getItem(index: Int): AutocompleteResult { + return resultList[index] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getFilter(): Filter { + return object : Filter() { + + override fun convertResultToString(resultValue: Any): CharSequence { + return when (resultValue) { + is AutocompleteResult.AccountResult -> formatUsername(resultValue) + is AutocompleteResult.HashtagResult -> formatHashtag(resultValue) + is AutocompleteResult.EmojiResult -> formatEmoji(resultValue) + else -> "" + } + } + + @WorkerThread + override fun performFiltering(constraint: CharSequence?): FilterResults { + val filterResults = FilterResults() + if (constraint != null) { + val results = autocompletionProvider.search(constraint.toString()) + filterResults.values = results + filterResults.count = results.size + } + return filterResults + } + + override fun publishResults(constraint: CharSequence?, results: FilterResults) { + if (results.count > 0) { + resultList = results.values as List + notifyDataSetChanged() + } else { + notifyDataSetInvalidated() + } + } + } + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val itemViewType = getItemViewType(position) + val context = parent.context + + val view: View = convertView ?: run { + val layoutInflater = LayoutInflater.from(context) + val binding = when (itemViewType) { + ACCOUNT_VIEW_TYPE -> ItemAutocompleteAccountBinding.inflate(layoutInflater) + HASHTAG_VIEW_TYPE -> ItemAutocompleteHashtagBinding.inflate(layoutInflater) + EMOJI_VIEW_TYPE -> ItemAutocompleteEmojiBinding.inflate(layoutInflater) + else -> throw AssertionError("unknown view type") + } + binding.root.tag = binding + binding.root + } + + when (val binding = view.tag) { + is ItemAutocompleteAccountBinding -> { + val accountResult = getItem(position) as AutocompleteResult.AccountResult + val account = accountResult.account + binding.username.text = context.getString(R.string.post_username_format, account.username) + binding.displayName.text = account.name.emojify(account.emojis, binding.displayName, animateEmojis) + val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp) + loadAvatar( + account.avatar, + binding.avatar, + avatarRadius, + animateAvatar + ) + binding.avatarBadge.visible(showBotBadge && account.bot) + } + is ItemAutocompleteHashtagBinding -> { + val result = getItem(position) as AutocompleteResult.HashtagResult + binding.root.text = formatHashtag(result) + } + is ItemAutocompleteEmojiBinding -> { + val emojiResult = getItem(position) as AutocompleteResult.EmojiResult + val (shortcode, url) = emojiResult.emoji + binding.shortcode.text = context.getString(R.string.emoji_shortcode_format, shortcode) + Glide.with(binding.preview) + .load(url) + .into(binding.preview) + } + } + return view + } + + override fun getViewTypeCount() = 3 + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is AutocompleteResult.AccountResult -> ACCOUNT_VIEW_TYPE + is AutocompleteResult.HashtagResult -> HASHTAG_VIEW_TYPE + is AutocompleteResult.EmojiResult -> EMOJI_VIEW_TYPE + } + } + + sealed class AutocompleteResult { + class AccountResult(val account: TimelineAccount) : AutocompleteResult() + + class HashtagResult(val hashtag: String) : AutocompleteResult() + + class EmojiResult(val emoji: Emoji) : AutocompleteResult() + } + + interface AutocompletionProvider { + fun search(token: String): List + } + + companion object { + private const val ACCOUNT_VIEW_TYPE = 0 + private const val HASHTAG_VIEW_TYPE = 1 + private const val EMOJI_VIEW_TYPE = 2 + + private fun formatUsername(result: AutocompleteResult.AccountResult): String { + return String.format("@%s", result.account.username) + } + + private fun formatHashtag(result: AutocompleteResult.HashtagResult): String { + return String.format("#%s", result.hashtag) + } + + private fun formatEmoji(result: AutocompleteResult.EmojiResult): String { + return String.format(":%s:", result.emoji.shortcode) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer.kt similarity index 98% rename from app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt rename to app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer.kt index 6fee42ed..7b3d208b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.util +package com.keylesspalace.tusky.components.compose import android.text.SpannableString import android.text.Spanned diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 7faf1139..2c0da583 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia +import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository @@ -38,6 +39,7 @@ import com.keylesspalace.tusky.service.ServiceClient import com.keylesspalace.tusky.service.StatusToSend import com.keylesspalace.tusky.util.combineLiveData import com.keylesspalace.tusky.util.randomAlphanumericString +import com.keylesspalace.tusky.util.result import com.keylesspalace.tusky.util.toLiveData import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.Dispatchers @@ -51,7 +53,6 @@ import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.rxSingle import kotlinx.coroutines.withContext -import java.util.Locale import javax.inject.Inject class ComposeViewModel @Inject constructor( @@ -330,48 +331,39 @@ class ComposeViewModel @Inject constructor( return true } - fun searchAutocompleteSuggestions(token: String): List { + fun searchAutocompleteSuggestions(token: String): List { when (token[0]) { '@' -> { - return try { - api.searchAccounts(query = token.substring(1), limit = 10) - .blockingGet() - .map { ComposeAutoCompleteAdapter.AccountResult(it) } - } catch (e: Throwable) { - Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e) - emptyList() - } + return api.searchAccountsCall(query = token.substring(1), limit = 10) + .result() + .fold({ accounts -> + accounts.map { AutocompleteResult.AccountResult(it) } + }, { e -> + Log.e(TAG, "Autocomplete search for $token failed.", e) + emptyList() + }) } '#' -> { - return try { - api.searchObservable(query = token, type = SearchType.Hashtag.apiParameter, limit = 10) - .blockingGet() - .hashtags - .map { ComposeAutoCompleteAdapter.HashtagResult(it) } - } catch (e: Throwable) { - Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e) - emptyList() - } + return api.searchCall(query = token, type = SearchType.Hashtag.apiParameter, limit = 10) + .result() + .fold({ searchResult -> + searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) } + }, { e -> + Log.e(TAG, "Autocomplete search for $token failed.", e) + emptyList() + }) } ':' -> { val emojiList = emoji.value ?: return emptyList() + val incomplete = token.substring(1) - val incomplete = token.substring(1).lowercase(Locale.ROOT) - val results = ArrayList() - val resultsInside = ArrayList() - for (emoji in emojiList) { - val shortcode = emoji.shortcode.lowercase(Locale.ROOT) - if (shortcode.startsWith(incomplete)) { - results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji)) - } else if (shortcode.indexOf(incomplete, 1) != -1) { - resultsInside.add(ComposeAutoCompleteAdapter.EmojiResult(emoji)) - } + return emojiList.filter { emoji -> + emoji.shortcode.contains(incomplete, ignoreCase = true) + }.sortedBy { emoji -> + emoji.shortcode.indexOf(incomplete, ignoreCase = true) + }.map { emoji -> + AutocompleteResult.EmojiResult(emoji) } - if (results.isNotEmpty() && resultsInside.isNotEmpty()) { - results.add(ComposeAutoCompleteAdapter.ResultSeparator()) - } - results.addAll(resultsInside) - return results } else -> { Log.w(TAG, "Unexpected autocompletion token: $token") diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index ef81ed11..3a34169c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -288,6 +288,14 @@ interface MastodonApi { @Query("following") following: Boolean? = null ): Single> + @GET("api/v1/accounts/search") + fun searchAccountsCall( + @Query("q") query: String, + @Query("resolve") resolve: Boolean? = null, + @Query("limit") limit: Int? = null, + @Query("following") following: Boolean? = null + ): Call> + @GET("api/v1/accounts/{id}") fun account( @Path("id") accountId: String @@ -593,6 +601,16 @@ interface MastodonApi { @Query("following") following: Boolean? = null ): Single + @GET("api/v2/search") + fun searchCall( + @Query("q") query: String?, + @Query("type") type: String? = null, + @Query("resolve") resolve: Boolean? = null, + @Query("limit") limit: Int? = null, + @Query("offset") offset: Int? = null, + @Query("following") following: Boolean? = null + ): Call + @FormUrlEncoded @POST("api/v1/accounts/{id}/note") fun updateAccountNote( diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CallExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/CallExtensions.kt new file mode 100644 index 00000000..809dcd2b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CallExtensions.kt @@ -0,0 +1,23 @@ +package com.keylesspalace.tusky.util + +import retrofit2.Call +import retrofit2.HttpException + +/** + * Synchronously executes the call and returns the response encapsulated in a kotlin.Result. + * Since Result is an inline class it is not possible to do this with a Retrofit adapter unfortunately. + * More efficient then calling a suspending method with runBlocking + */ +fun Call.result(): Result { + return try { + val response = execute() + val responseBody = response.body() + if (response.isSuccessful && responseBody != null) { + Result.success(responseBody) + } else { + Result.failure(HttpException(response)) + } + } catch (e: Exception) { + Result.failure(e) + } +} diff --git a/app/src/main/res/layout/item_autocomplete_account.xml b/app/src/main/res/layout/item_autocomplete_account.xml index 681f9919..000bae53 100644 --- a/app/src/main/res/layout/item_autocomplete_account.xml +++ b/app/src/main/res/layout/item_autocomplete_account.xml @@ -1,48 +1,65 @@ - + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="8dp"> - + - + - + - - - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_autocomplete_divider.xml b/app/src/main/res/layout/item_autocomplete_divider.xml deleted file mode 100644 index f9b211b0..00000000 --- a/app/src/main/res/layout/item_autocomplete_divider.xml +++ /dev/null @@ -1,5 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_autocomplete_emoji.xml b/app/src/main/res/layout/item_autocomplete_emoji.xml index 2f910040..fbc2f5c9 100644 --- a/app/src/main/res/layout/item_autocomplete_emoji.xml +++ b/app/src/main/res/layout/item_autocomplete_emoji.xml @@ -5,24 +5,24 @@ android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="horizontal" - android:padding="8dp"> + tools:ignore="UseCompoundDrawables"> + android:layout_marginStart="16dp" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:importantForAccessibility="no" /> + tools:text="#Tusky" /> diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt index e203dde2..fa0bba94 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt @@ -15,7 +15,7 @@ package com.keylesspalace.tusky -import com.keylesspalace.tusky.util.ComposeTokenizer +import com.keylesspalace.tusky.components.compose.ComposeTokenizer import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith