From 89501271ceb457ce67b98331eea74419a7211646 Mon Sep 17 00:00:00 2001 From: Grishka Date: Fri, 12 May 2023 03:39:46 +0300 Subject: [PATCH] Compose autocomplete improvements --- .../android/fragments/ComposeFragment.java | 87 +++++++- .../account_list/BaseAccountListFragment.java | 8 +- .../ComposeAccountSearchFragment.java | 135 +++++++++++++ .../android/ui/utils/UiUtils.java | 7 + .../ComposeAutocompleteViewController.java | 188 +++++++++++++----- .../ui/viewholders/AccountViewHolder.java | 11 + .../android/ui/views/ComposeEditText.java | 9 +- .../android/ui/views/FilterChipView.java | 19 +- .../src/main/res/color/m3_primary_alpha11.xml | 4 + .../src/main/res/drawable/bg_m3_surface3.xml | 6 + .../src/main/res/drawable/ic_mood_20px.xml | 9 + .../src/main/res/drawable/ic_search_20px.xml | 9 + mastodon/src/main/res/values/strings.xml | 3 + 13 files changed, 429 insertions(+), 66 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/ComposeAccountSearchFragment.java create mode 100644 mastodon/src/main/res/color/m3_primary_alpha11.xml create mode 100644 mastodon/src/main/res/drawable/bg_m3_surface3.xml create mode 100644 mastodon/src/main/res/drawable/ic_mood_20px.xml create mode 100644 mastodon/src/main/res/drawable/ic_search_20px.xml diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index 0cbf07143..2d6ca8d1f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -28,6 +28,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.WindowManager; +import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; @@ -52,6 +53,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusUpdatedEvent; +import org.joinmastodon.android.fragments.account_list.ComposeAccountSearchFragment; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.EmojiCategory; @@ -96,6 +98,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private static final int MEDIA_RESULT=717; public static final int IMAGE_DESCRIPTION_RESULT=363; + private static final int AUTOCOMPLETE_ACCOUNT_RESULT=779; private static final String TAG="ComposeFragment"; private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE); @@ -262,6 +265,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr @Override public void onIconChanged(int icon){ emojiBtn.setSelected(icon!=PopupKeyboard.ICON_HIDDEN); + if(autocompleteViewController.getMode()==ComposeAutocompleteViewController.Mode.EMOJIS){ + contentView.layout(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom()); + if(icon==PopupKeyboard.ICON_HIDDEN) + showAutocomplete(); + else + hideAutocomplete(); + } } }); @@ -294,7 +304,25 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr updateVisibilityIcon(); autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID); - autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected); + autocompleteViewController.setCompletionSelectedListener(new ComposeAutocompleteViewController.AutocompleteListener(){ + @Override + public void onCompletionSelected(String completion){ + onAutocompleteOptionSelected(completion); + } + + @Override + public void onSetEmojiPanelOpen(boolean open){ + if(open!=emojiKeyboard.isVisible()) + emojiKeyboard.toggleKeyboardPopup(mainEditText); + } + + @Override + public void onLaunchAccountSearch(){ + Bundle args=new Bundle(); + args.putString("account", accountID); + Nav.goForResult(getActivity(), ComposeAccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this); + } + }); View autocompleteView=autocompleteViewController.getView(); autocompleteView.setVisibility(View.INVISIBLE); bottomBar.addView(autocompleteView, 0, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(56))); @@ -315,6 +343,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr mediaViewController.onSaveInstanceState(outState); outState.putBoolean("hasSpoiler", hasSpoiler); outState.putSerializable("visibility", statusVisibility); + if(currentAutocompleteSpan!=null){ + Editable e=mainEditText.getText(); + outState.putInt("autocompleteStart", e.getSpanStart(currentAutocompleteSpan)); + outState.putInt("autocompleteEnd", e.getSpanEnd(currentAutocompleteSpan)); + } } @Override @@ -471,6 +504,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr updateMediaPollStates(); } + @Override + public void onViewStateRestored(Bundle savedInstanceState){ + super.onViewStateRestored(savedInstanceState); + if(savedInstanceState!=null && savedInstanceState.containsKey("autocompleteStart")){ + int start=savedInstanceState.getInt("autocompleteStart"), end=savedInstanceState.getInt("autocompleteEnd"); + currentAutocompleteSpan=new ComposeAutocompleteSpan(); + mainEditText.getText().setSpan(currentAutocompleteSpan, start, end, Editable.SPAN_EXCLUSIVE_INCLUSIVE); + startAutocomplete(currentAutocompleteSpan); + } + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ inflater.inflate(editingStatus==null ? R.menu.compose : R.menu.compose_edit, menu); @@ -537,6 +581,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void onCustomEmojiClick(Emoji emoji){ if(getActivity().getCurrentFocus() instanceof EditText edit){ + if(edit==mainEditText && currentAutocompleteSpan!=null && autocompleteViewController.getMode()==ComposeAutocompleteViewController.Mode.EMOJIS){ + Editable text=mainEditText.getText(); + int start=text.getSpanStart(currentAutocompleteSpan); + int end=text.getSpanEnd(currentAutocompleteSpan); + finishAutocomplete(); + text.replace(start, end, ':'+emoji.shortcode+':'); + return; + } int start=edit.getSelectionStart(); String prefix=start>0 && !Character.isWhitespace(edit.getText().charAt(start-1)) ? " :" : ":"; edit.getText().replace(start, edit.getSelectionEnd(), prefix+emoji.shortcode+':'); @@ -549,6 +601,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Background, R.attr.colorM3Primary, 0.11f); getToolbar().setBackgroundColor(color); setStatusBarColor(color); + setNavigationBarColor(color); bottomBar.setBackgroundColor(color); } @@ -694,6 +747,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr String attID=result.getString("attachment"); String text=result.getString("text"); mediaViewController.setAltTextByID(attID, text); + }else if(reqCode==AUTOCOMPLETE_ACCOUNT_RESULT && success){ + Account acc=Parcels.unwrap(result.getParcelable("selectedAccount")); + if(currentAutocompleteSpan==null) + return; + Editable e=mainEditText.getText(); + int start=e.getSpanStart(currentAutocompleteSpan); + int end=e.getSpanEnd(currentAutocompleteSpan); + e.removeSpan(currentAutocompleteSpan); + e.replace(start, end, '@'+acc.acct+' '); + finishAutocomplete(); } } @@ -905,6 +968,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr Editable e=mainEditText.getText(); String spanText=e.toString().substring(e.getSpanStart(span), e.getSpanEnd(span)); autocompleteViewController.setText(spanText); + showAutocomplete(); + } + + private void finishAutocomplete(){ + if(currentAutocompleteSpan==null) + return; + autocompleteViewController.setText(null); + currentAutocompleteSpan=null; + hideAutocomplete(); + } + + private void showAutocomplete(){ UiUtils.beginLayoutTransition(bottomBar); View autocompleteView=autocompleteViewController.getView(); bottomBar.getLayoutParams().height=ViewGroup.LayoutParams.WRAP_CONTENT; @@ -913,11 +988,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr autocompleteDivider.setVisibility(View.VISIBLE); } - private void finishAutocomplete(){ - if(currentAutocompleteSpan==null) - return; - autocompleteViewController.setText(null); - currentAutocompleteSpan=null; + private void hideAutocomplete(){ UiUtils.beginLayoutTransition(bottomBar); bottomBar.getLayoutParams().height=V.dp(48); bottomBar.requestLayout(); @@ -930,8 +1001,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr int start=e.getSpanStart(currentAutocompleteSpan); int end=e.getSpanEnd(currentAutocompleteSpan); e.replace(start, end, text+" "); - mainEditText.setSelection(start+text.length()+1); finishAutocomplete(); + InputConnection conn=mainEditText.getCurrentInputConnection(); + if(conn!=null) + conn.finishComposingText(); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java index 9c39219c1..fb4ca4a52 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java @@ -136,6 +136,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment=29 && insets.getTappableElementInsets().bottom==0){ list.setPadding(0, V.dp(16), 0, V.dp(16)+insets.getSystemWindowInsetBottom()); + emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom()); + progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom()); insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom()); }else{ list.setPadding(0, V.dp(16), 0, V.dp(16)); @@ -143,6 +145,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment implements ImageLoaderRecyclerAdapter{ public AccountsAdapter(){ super(imgLoader); @@ -151,7 +155,9 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment{ + currentQuery=searchEdit.getText().toString(); + if(currentRequest!=null){ + currentRequest.cancel(); + currentRequest=null; + } + if(!TextUtils.isEmpty(currentQuery)) + loadData(); + }; + private boolean resultDelivered; + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + setRefreshEnabled(false); + setEmptyText(""); + dataLoaded(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + searchLayout=new LinearLayout(view.getContext()); + searchLayout.setOrientation(LinearLayout.HORIZONTAL); + + searchEdit=new EditText(view.getContext()); + searchEdit.setHint(R.string.search_hint); + searchEdit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER); + searchEdit.setBackground(null); + searchEdit.addTextChangedListener(new SimpleTextWatcher(e->{ + searchEdit.removeCallbacks(debouncer); + searchEdit.postDelayed(debouncer, 300); + })); + searchEdit.setImeActionLabel(null, EditorInfo.IME_ACTION_SEARCH); + searchEdit.setOnEditorActionListener((v, actionId, event)->{ + searchEdit.removeCallbacks(debouncer); + debouncer.run(); + return true; + }); + searchLayout.addView(searchEdit, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)); + + clearSearchButton=new ImageButton(view.getContext()); + clearSearchButton.setImageResource(R.drawable.ic_baseline_close_24); + clearSearchButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurfaceVariant))); + clearSearchButton.setBackground(UiUtils.getThemeDrawable(getToolbarContext(), android.R.attr.actionBarItemBackground)); + clearSearchButton.setOnClickListener(v->searchEdit.setText("")); + searchLayout.addView(clearSearchButton, new LinearLayout.LayoutParams(V.dp(56), ViewGroup.LayoutParams.MATCH_PARENT)); + + super.onViewCreated(view, savedInstanceState); + + view.setBackgroundResource(R.drawable.bg_m3_surface3); + int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f); + setStatusBarColor(color); + setNavigationBarColor(color); + } + + @Override + protected void doLoadData(int offset, int count){ + refreshing=true; + currentRequest=new GetSearchResults(currentQuery, GetSearchResults.Type.ACCOUNTS, false) + .setCallback(new SimpleCallback<>(this){ + @Override + public void onSuccess(SearchResults result){ + setEmptyText(R.string.no_search_results); + onDataLoaded(result.accounts.stream().map(AccountViewModel::new).collect(Collectors.toList()), false); + } + }) + .exec(accountID); + } + + @Override + protected void onUpdateToolbar(){ + super.onUpdateToolbar(); + if(searchLayout.getParent()!=null) + ((ViewGroup) searchLayout.getParent()).removeView(searchLayout); + getToolbar().addView(searchLayout, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + getToolbar().setBackgroundResource(R.drawable.bg_m3_surface3); + searchEdit.requestFocus(); + } + + @Override + protected boolean wantsElevationOnScrollEffect(){ + return false; + } + + @Override + protected void onConfigureViewHolder(AccountViewHolder holder){ + super.onConfigureViewHolder(holder); + holder.setOnClickListener(this::onItemClick); + } + + private void onItemClick(AccountViewHolder holder){ + if(resultDelivered) + return; + + resultDelivered=true; + Bundle res=new Bundle(); + res.putParcelable("selectedAccount", Parcels.wrap(holder.getItem().account)); + setResult(true, res); + Nav.finish(this, false); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index d89a79dbe..b07679282 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -733,4 +733,11 @@ public class UiUtils{ .setInterpolator(CubicBezierInterpolator.DEFAULT) ); } + + public static Drawable getThemeDrawable(Context context, @AttrRes int attr){ + TypedArray ta=context.obtainStyledAttributes(new int[]{attr}); + Drawable d=ta.getDrawable(0); + ta.recycle(); + return d; + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeAutocompleteViewController.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeAutocompleteViewController.java index c487c3a06..4d89e0e05 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeAutocompleteViewController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeAutocompleteViewController.java @@ -3,13 +3,13 @@ package org.joinmastodon.android.ui.viewcontrollers; import android.annotation.SuppressLint; import android.app.Activity; import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.ProgressBar; +import android.widget.LinearLayout; import android.widget.TextView; import org.joinmastodon.android.R; @@ -23,11 +23,13 @@ import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.utils.CustomEmojiHelper; +import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter; import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.ui.views.FilterChipView; +import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -44,16 +46,18 @@ import me.grishka.appkit.imageloader.RecyclerViewDelegate; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; public class ComposeAutocompleteViewController{ + private static final int LOADING_FAKE_USER_COUNT=3; + private Activity activity; private String accountID; private FrameLayout contentView; private UsableRecyclerView list; private ListImageLoaderWrapper imgLoader; - private ProgressBar progress; private List users=Collections.emptyList(); private List hashtags=Collections.emptyList(); private List emojis=Collections.emptyList(); @@ -61,13 +65,17 @@ public class ComposeAutocompleteViewController{ private APIRequest currentRequest; private Runnable usersDebouncer=this::doSearchUsers, hashtagsDebouncer=this::doSearchHashtags; private String lastText; - private boolean listIsHidden=true; + private boolean isLoading; + private FilterChipView emptyButton; + private HideableSingleViewRecyclerAdapter emptyButtonAdapter; private UsersAdapter usersAdapter; private HashtagsAdapter hashtagsAdapter; private EmojisAdapter emojisAdapter; + private MergeRecyclerAdapter usersMergeAdapter; + private MergeRecyclerAdapter emojisMergeAdapter; - private Consumer completionSelectedListener; + private AutocompleteListener completionSelectedListener; public ComposeAutocompleteViewController(Activity activity, String accountID){ this.activity=activity; @@ -77,7 +85,6 @@ public class ComposeAutocompleteViewController{ list=new UsableRecyclerView(activity); list.setLayoutManager(new LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false)); list.setItemAnimator(new BetterItemAnimator()); - list.setVisibility(View.GONE); list.setPadding(V.dp(16), V.dp(12), V.dp(16), V.dp(12)); list.setClipToPadding(false); list.setSelector(null); @@ -90,10 +97,15 @@ public class ComposeAutocompleteViewController{ }); contentView.addView(list, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - progress=new ProgressBar(activity); - FrameLayout.LayoutParams progressLP=new FrameLayout.LayoutParams(V.dp(48), V.dp(48), Gravity.CENTER_HORIZONTAL|Gravity.TOP); - progressLP.topMargin=V.dp(16); - contentView.addView(progress, progressLP); + emptyButton=new FilterChipView(activity); + emptyButtonAdapter=new HideableSingleViewRecyclerAdapter(emptyButton); + emptyButton.setOnClickListener(v->{ + if(mode==Mode.EMOJIS){ + completionSelectedListener.onSetEmojiPanelOpen(true); + }else if(mode==Mode.USERS){ + completionSelectedListener.onLaunchAccountSearch(); + } + }); imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null); } @@ -104,13 +116,15 @@ public class ComposeAutocompleteViewController{ }else if(mode==Mode.HASHTAGS){ list.removeCallbacks(hashtagsDebouncer); } - if(text==null) - return; - Mode prevMode=mode; if(currentRequest!=null){ currentRequest.cancel(); currentRequest=null; } + if(text==null){ + reset(); + return; + } + Mode prevMode=mode; mode=switch(text.charAt(0)){ case '@' -> Mode.USERS; case '#' -> Mode.HASHTAGS; @@ -118,16 +132,33 @@ public class ComposeAutocompleteViewController{ default -> throw new IllegalStateException("Unexpected value: "+text.charAt(0)); }; if(prevMode!=mode){ + if(mode==Mode.USERS){ + isLoading=true; + emptyButtonAdapter.setVisible(false); + } + list.setAdapter(switch(mode){ case USERS -> { - if(usersAdapter==null) + if(usersAdapter==null){ usersAdapter=new UsersAdapter(); - yield usersAdapter; + usersMergeAdapter=new MergeRecyclerAdapter(); + usersMergeAdapter.addAdapter(emptyButtonAdapter); + usersMergeAdapter.addAdapter(usersAdapter); + } + emptyButton.setText(R.string.compose_autocomplete_users_empty); + emptyButton.setDrawableStartTinted(R.drawable.ic_search_20px); + yield usersMergeAdapter; } case EMOJIS -> { - if(emojisAdapter==null) + if(emojisAdapter==null){ emojisAdapter=new EmojisAdapter(); - yield emojisAdapter; + emojisMergeAdapter=new MergeRecyclerAdapter(); + emojisMergeAdapter.addAdapter(emptyButtonAdapter); + emojisMergeAdapter.addAdapter(emojisAdapter); + } + emptyButton.setText(R.string.compose_autocomplete_emoji_empty); + emptyButton.setDrawableStartTinted(R.drawable.ic_mood_20px); + yield emojisMergeAdapter; } case HASHTAGS -> { if(hashtagsAdapter==null) @@ -135,20 +166,18 @@ public class ComposeAutocompleteViewController{ yield hashtagsAdapter; } }); - if(mode!=Mode.EMOJIS){ - list.setVisibility(View.GONE); - progress.setVisibility(View.VISIBLE); - listIsHidden=true; - }else if(listIsHidden){ - list.setVisibility(View.VISIBLE); - progress.setVisibility(View.GONE); - listIsHidden=false; - } } lastText=text; if(mode==Mode.USERS){ list.postDelayed(usersDebouncer, 300); }else if(mode==Mode.HASHTAGS){ + List oldList=hashtags; + hashtags=new ArrayList<>(); + Hashtag tag=new Hashtag(); + tag.name=lastText.substring(1); + hashtags.add(tag); + UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name)); + list.postDelayed(hashtagsDebouncer, 300); }else if(mode==Mode.EMOJIS){ String _text=text.substring(1); // remove ':' @@ -165,12 +194,14 @@ public class ComposeAutocompleteViewController{ .filter(e -> e.shortcode.toLowerCase().contains(_text.toLowerCase()))) .map(WrappedEmoji::new) .collect(Collectors.toList()); + emptyButtonAdapter.setVisible(emojis.isEmpty()); UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode)); + list.invalidateItemDecorations(); imgLoader.updateImages(); } } - public void setCompletionSelectedListener(Consumer completionSelectedListener){ + public void setCompletionSelectedListener(AutocompleteListener completionSelectedListener){ this.completionSelectedListener=completionSelectedListener; } @@ -178,6 +209,17 @@ public class ComposeAutocompleteViewController{ return contentView; } + public void reset(){ + mode=null; + users.clear(); + emojis.clear(); + hashtags.clear(); + } + + public Mode getMode(){ + return mode; + } + private void doSearchUsers(){ currentRequest=new GetSearchResults(lastText, GetSearchResults.Type.ACCOUNTS, false) .setCallback(new Callback<>(){ @@ -186,13 +228,22 @@ public class ComposeAutocompleteViewController{ currentRequest=null; List oldList=users; users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList()); - UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id)); - imgLoader.updateImages(); - if(listIsHidden){ - listIsHidden=false; - V.setVisibilityAnimated(list, View.VISIBLE); - V.setVisibilityAnimated(progress, View.GONE); + if(isLoading){ + isLoading=false; + if(users.size()>=LOADING_FAKE_USER_COUNT){ + usersAdapter.notifyItemRangeChanged(0, LOADING_FAKE_USER_COUNT); + if(users.size()>LOADING_FAKE_USER_COUNT) + usersAdapter.notifyItemRangeInserted(LOADING_FAKE_USER_COUNT, users.size()-LOADING_FAKE_USER_COUNT); + }else{ + usersAdapter.notifyItemRangeChanged(0, users.size()); + usersAdapter.notifyItemRangeRemoved(users.size(), LOADING_FAKE_USER_COUNT-users.size()); + } + }else{ + UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id)); } + list.invalidateItemDecorations(); + emptyButtonAdapter.setVisible(users.isEmpty()); + imgLoader.updateImages(); } @Override @@ -209,15 +260,12 @@ public class ComposeAutocompleteViewController{ @Override public void onSuccess(SearchResults result){ currentRequest=null; + if(result.hashtags.isEmpty() || (result.hashtags.size()==1 && result.hashtags.get(0).name.equals(lastText.substring(1)))) + return; List oldList=hashtags; hashtags=result.hashtags; UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name)); - imgLoader.updateImages(); - if(listIsHidden){ - listIsHidden=false; - V.setVisibilityAnimated(list, View.VISIBLE); - V.setVisibilityAnimated(progress, View.GONE); - } + list.invalidateItemDecorations(); } @Override @@ -236,23 +284,31 @@ public class ComposeAutocompleteViewController{ @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ - return new UserViewHolder(); + return switch(viewType){ + case 0 -> new UserViewHolder(); + case 1 -> new LoadingUserViewHolder(); + default -> throw new IllegalStateException("Unexpected value: "+viewType); + }; } @Override public int getItemCount(){ + if(isLoading) + return LOADING_FAKE_USER_COUNT; return users.size(); } @Override public void onBindViewHolder(UserViewHolder holder, int position){ - holder.bind(users.get(position)); - super.onBindViewHolder(holder, position); + if(!isLoading){ + holder.bind(users.get(position)); + super.onBindViewHolder(holder, position); + } } @Override public int getImageCountForItem(int position){ - return 1/*+users.get(position).emojiHelper.getImageCount()*/; + return isLoading ? 0 : 1; } @Override @@ -262,13 +318,18 @@ public class ComposeAutocompleteViewController{ return a.avaRequest; return a.emojiHelper.getImageRequest(image-1); } + + @Override + public int getItemViewType(int position){ + return isLoading ? 1 : 0; + } } private class UserViewHolder extends BindableViewHolder implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{ - private final ImageView ava; - private final TextView username; + protected final ImageView ava; + protected final TextView username; - private UserViewHolder(){ + public UserViewHolder(){ super(activity, R.layout.item_autocomplete_user, list); ava=findViewById(R.id.photo); username=findViewById(R.id.username); @@ -283,7 +344,7 @@ public class ComposeAutocompleteViewController{ @Override public void onClick(){ - completionSelectedListener.accept("@"+item.account.acct); + completionSelectedListener.onCompletionSelected("@"+item.account.acct); } @Override @@ -297,7 +358,24 @@ public class ComposeAutocompleteViewController{ @Override public void clearImage(int index){ - setImage(index, null); + if(index==0) + ava.setImageResource(R.drawable.image_placeholder); + else + setImage(index, null); + } + } + + private class LoadingUserViewHolder extends UserViewHolder implements UsableRecyclerView.DisableableClickable{ + public LoadingUserViewHolder(){ + int color=UiUtils.getThemeColor(activity, R.attr.colorM3OutlineVariant); + ava.setImageDrawable(new ColorDrawable(color)); + username.setLayoutParams(new LinearLayout.LayoutParams(V.dp(64), V.dp(10))); + username.setBackgroundColor(color); + } + + @Override + public boolean isEnabled(){ + return false; } } @@ -336,7 +414,7 @@ public class ComposeAutocompleteViewController{ @Override public void onClick(){ - completionSelectedListener.accept("#"+item.name); + completionSelectedListener.onCompletionSelected("#"+item.name); } } @@ -401,7 +479,7 @@ public class ComposeAutocompleteViewController{ @Override public void onClick(){ - completionSelectedListener.accept(":"+item.emoji.shortcode+":"); + completionSelectedListener.onCompletionSelected(":"+item.emoji.shortcode+":"); } } @@ -430,9 +508,15 @@ public class ComposeAutocompleteViewController{ } } - private enum Mode{ + public enum Mode{ USERS, HASHTAGS, EMOJIS } + + public interface AutocompleteListener{ + void onCompletionSelected(String completion); + void onSetEmojiPanelOpen(boolean open); + void onLaunchAccountSearch(); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java index f8901178b..08ddff4e4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java @@ -36,6 +36,7 @@ import org.parceler.Parcels; import java.util.HashMap; import java.util.Objects; +import java.util.function.Consumer; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; @@ -56,6 +57,8 @@ public class AccountViewHolder extends BindableViewHolder impl private final Fragment fragment; private final HashMap relationships; + private Consumer onClick; + public AccountViewHolder(Fragment fragment, ViewGroup list, HashMap relationships){ super(fragment.getActivity(), R.layout.item_account_list, list); this.fragment=fragment; @@ -140,6 +143,10 @@ public class AccountViewHolder extends BindableViewHolder impl @Override public void onClick(){ + if(onClick!=null){ + onClick.accept(this); + return; + } Bundle args=new Bundle(); args.putString("account", accountID); args.putParcelable("profileAccount", Parcels.wrap(item.account)); @@ -253,4 +260,8 @@ public class AccountViewHolder extends BindableViewHolder impl relationships.put(item.account.id, r); bindRelationship(); } + + public void setOnClickListener(Consumer listener){ + onClick=listener; + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/ComposeEditText.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/ComposeEditText.java index 0d03085bf..9612f9a4d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/ComposeEditText.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/ComposeEditText.java @@ -21,6 +21,7 @@ import androidx.annotation.RequiresApi; public class ComposeEditText extends EditText{ private SelectionListener selectionListener; + private InputConnection currentInputConnection; public ComposeEditText(Context context){ super(context); @@ -49,15 +50,19 @@ public class ComposeEditText extends EditText{ this.selectionListener=selectionListener; } + public InputConnection getCurrentInputConnection(){ + return currentInputConnection; + } + // Support receiving images from keyboards @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs){ final InputConnection ic=super.onCreateInputConnection(outAttrs); if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N_MR1){ outAttrs.contentMimeTypes=selectionListener.onGetAllowedMediaMimeTypes(); - return new MediaAcceptingInputConnection(ic); + return currentInputConnection=new MediaAcceptingInputConnection(ic); } - return ic; + return currentInputConnection=ic; } // Support pasting images diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FilterChipView.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FilterChipView.java index 560388267..82bb71b4b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FilterChipView.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FilterChipView.java @@ -1,7 +1,6 @@ package org.joinmastodon.android.ui.views; import android.content.Context; -import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.widget.Button; @@ -30,7 +29,6 @@ public class FilterChipView extends Button{ setBackgroundResource(R.drawable.bg_filter_chip); setTextAppearance(R.style.m3_label_large); setTextColor(getResources().getColorStateList(R.color.filter_chip_text, context.getTheme())); - setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurface))); updatePadding(); } @@ -40,7 +38,9 @@ public class FilterChipView extends Button{ if(currentlySelected==isSelected()) return; currentlySelected=isSelected(); - Drawable start=currentlySelected ? getResources().getDrawable(R.drawable.ic_baseline_check_18, getContext().getTheme()) : null; + Drawable start=currentlySelected ? getResources().getDrawable(R.drawable.ic_baseline_check_18, getContext().getTheme()).mutate() : null; + if(start!=null) + start.setTint(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurface)); Drawable end=getCompoundDrawablesRelative()[2]; setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null); updatePadding(); @@ -53,7 +53,18 @@ public class FilterChipView extends Button{ } public void setDrawableEnd(@DrawableRes int drawable){ - setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, drawable, 0); + Drawable icon=getResources().getDrawable(drawable, getContext().getTheme()).mutate(); + icon.setBounds(0, 0, V.dp(18), V.dp(18)); + icon.setTint(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurface)); + setCompoundDrawablesRelativeWithIntrinsicBounds(getCompoundDrawablesRelative()[0], null, icon, null); + updatePadding(); + } + + public void setDrawableStartTinted(@DrawableRes int drawable){ + Drawable icon=getResources().getDrawable(drawable, getContext().getTheme()).mutate(); + icon.setBounds(0, 0, V.dp(18), V.dp(18)); + icon.setTint(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary)); + setCompoundDrawablesRelative(icon, null, getCompoundDrawablesRelative()[2], null); updatePadding(); } } diff --git a/mastodon/src/main/res/color/m3_primary_alpha11.xml b/mastodon/src/main/res/color/m3_primary_alpha11.xml new file mode 100644 index 000000000..44c7b0b68 --- /dev/null +++ b/mastodon/src/main/res/color/m3_primary_alpha11.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_m3_surface3.xml b/mastodon/src/main/res/drawable/bg_m3_surface3.xml new file mode 100644 index 000000000..c1b10d943 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_m3_surface3.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_mood_20px.xml b/mastodon/src/main/res/drawable/ic_mood_20px.xml new file mode 100644 index 000000000..a99bd218f --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_mood_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_search_20px.xml b/mastodon/src/main/res/drawable/ic_search_20px.xml new file mode 100644 index 000000000..6758775ad --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_search_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index 4cd2a9b96..9b16d2866 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -481,4 +481,7 @@ Alt text provides image descriptions for people with vision impairments, low-bandwidth connections, or those seeking extra context.\n\nYou can improve accessibility and understanding for everyone by writing clear, concise, and objective alt text.\n\n
  • Capture important elements
  • \n
  • Summarize text in images
  • \n
  • Use regular sentence structure
  • \n
  • Avoid redundant information
  • \n
  • Focus on trends and key findings in complex visuals (like diagrams or maps)
Edit post No verified link + Browse emoji + Find who you\'re looking for + Could not find anything for these search terms \ No newline at end of file