Compose autocomplete improvements

This commit is contained in:
Grishka 2023-05-12 03:39:46 +03:00
parent 968a6ea9b3
commit 89501271ce
13 changed files with 429 additions and 66 deletions

View File

@ -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

View File

@ -136,6 +136,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=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<A
super.onApplyWindowInsets(insets);
}
protected void onConfigureViewHolder(AccountViewHolder holder){}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
@ -151,7 +155,9 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
onConfigureViewHolder(holder);
return holder;
}
@Override

View File

@ -0,0 +1,135 @@
package org.joinmastodon.android.fragments.account_list;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.text.InputType;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.parceler.Parcels;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
private LinearLayout searchLayout;
private EditText searchEdit;
private ImageButton clearSearchButton;
private String currentQuery;
private Runnable debouncer=()->{
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);
}
}

View File

@ -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;
}
}

View File

@ -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<WrappedAccount> users=Collections.emptyList();
private List<Hashtag> hashtags=Collections.emptyList();
private List<WrappedEmoji> 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<String> 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<Hashtag> 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<String> 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<WrappedAccount> 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<Hashtag> 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<WrappedAccount> 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();
}
}

View File

@ -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<AccountViewModel> impl
private final Fragment fragment;
private final HashMap<String, Relationship> relationships;
private Consumer<AccountViewHolder> onClick;
public AccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships){
super(fragment.getActivity(), R.layout.item_account_list, list);
this.fragment=fragment;
@ -140,6 +143,10 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> 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<AccountViewModel> impl
relationships.put(item.account.id, r);
bindRelationship();
}
public void setOnClickListener(Consumer<AccountViewHolder> listener){
onClick=listener;
}
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3Primary" android:alpha="0.11"/>
</selector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="@color/m3_primary_alpha11"
android:tintMode="src_over">
<solid android:color="?colorM3Surface" />
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M12.938,9.229Q13.479,9.229 13.844,8.854Q14.208,8.479 14.208,7.938Q14.208,7.396 13.844,7.021Q13.479,6.646 12.938,6.646Q12.396,6.646 12.01,7.021Q11.625,7.396 11.625,7.938Q11.625,8.479 12.01,8.854Q12.396,9.229 12.938,9.229ZM7.125,9.229Q7.667,9.229 8.031,8.854Q8.396,8.479 8.396,7.938Q8.396,7.396 8.031,7.021Q7.667,6.646 7.125,6.646Q6.583,6.646 6.198,7.021Q5.812,7.396 5.812,7.938Q5.812,8.479 6.198,8.854Q6.583,9.229 7.125,9.229ZM10.021,14.583Q11.458,14.583 12.615,13.781Q13.771,12.979 14.271,11.646H5.75Q6.25,12.979 7.417,13.781Q8.583,14.583 10.021,14.583ZM10,18.333Q8.271,18.333 6.75,17.677Q5.229,17.021 4.104,15.896Q2.979,14.771 2.323,13.25Q1.667,11.729 1.667,10Q1.667,8.271 2.323,6.75Q2.979,5.229 4.104,4.104Q5.229,2.979 6.75,2.323Q8.271,1.667 10,1.667Q11.729,1.667 13.25,2.323Q14.771,2.979 15.896,4.104Q17.021,5.229 17.677,6.75Q18.333,8.271 18.333,10Q18.333,11.729 17.677,13.25Q17.021,14.771 15.896,15.896Q14.771,17.021 13.25,17.677Q11.729,18.333 10,18.333ZM10,10Q10,10 10,10Q10,10 10,10Q10,10 10,10Q10,10 10,10Q10,10 10,10Q10,10 10,10Q10,10 10,10Q10,10 10,10ZM10,16.583Q12.729,16.583 14.656,14.656Q16.583,12.729 16.583,10Q16.583,7.271 14.656,5.344Q12.729,3.417 10,3.417Q7.271,3.417 5.344,5.344Q3.417,7.271 3.417,10Q3.417,12.729 5.344,14.656Q7.271,16.583 10,16.583Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M15.938,17 L10.958,12.021Q10.333,12.479 9.583,12.74Q8.833,13 8,13Q5.917,13 4.458,11.542Q3,10.083 3,8Q3,5.917 4.458,4.458Q5.917,3 8,3Q10.083,3 11.542,4.458Q13,5.917 13,8Q13,8.833 12.74,9.583Q12.479,10.333 12.021,10.958L17,15.938ZM8,11.5Q9.458,11.5 10.479,10.479Q11.5,9.458 11.5,8Q11.5,6.542 10.479,5.521Q9.458,4.5 8,4.5Q6.542,4.5 5.521,5.521Q4.5,6.542 4.5,8Q4.5,9.458 5.521,10.479Q6.542,11.5 8,11.5Z"/>
</vector>

View File

@ -481,4 +481,7 @@
<string name="alt_text_help">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<ul><li>Capture important elements</li>\n<li>Summarize text in images</li>\n<li>Use regular sentence structure</li>\n<li>Avoid redundant information</li>\n<li>Focus on trends and key findings in complex visuals (like diagrams or maps)</li></ul></string>
<string name="edit_post">Edit post</string>
<string name="no_verified_link">No verified link</string>
<string name="compose_autocomplete_emoji_empty">Browse emoji</string>
<string name="compose_autocomplete_users_empty">Find who you\'re looking for</string>
<string name="no_search_results">Could not find anything for these search terms</string>
</resources>