Compose autocomplete improvements
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user