Compose autocomplete improvements
This commit is contained in:
parent
968a6ea9b3
commit
89501271ce
|
@ -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){
|
||||
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,10 +358,27 @@ public class ComposeAutocompleteViewController{
|
|||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
|
||||
|
||||
@NonNull
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue