Merge branch 'master' into donations

# Conflicts:
#	mastodon/build.gradle
#	mastodon/src/main/res/values/strings.xml
This commit is contained in:
Grishka
2024-04-20 07:16:11 +03:00
20 changed files with 329 additions and 47 deletions

View File

@@ -23,6 +23,10 @@ import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -32,15 +36,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -66,7 +69,9 @@ import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard;
import org.joinmastodon.android.ui.ExtendedPopupMenu;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.PopupKeyboard;
@@ -87,10 +92,12 @@ import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -131,7 +138,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, languageBtn;
private TextView replyText;
private Button visibilityBtn;
private LinearLayout visibilityBtn;
private TextView visibilityText1, visibilityText2, visibilityCurrentText;
private LinearLayout bottomBar;
private View autocompleteDivider;
@@ -271,6 +279,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
emojiBtn=view.findViewById(R.id.btn_emoji);
spoilerBtn=view.findViewById(R.id.btn_spoiler);
visibilityBtn=view.findViewById(R.id.btn_visibility);
visibilityText1=view.findViewById(R.id.visibility_text1);
visibilityText2=view.findViewById(R.id.visibility_text2);
visibilityCurrentText=visibilityText1;
languageBtn=view.findViewById(R.id.btn_language);
replyText=view.findViewById(R.id.reply_text);
@@ -280,9 +291,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
spoilerBtn.setOnClickListener(v->toggleSpoiler());
languageBtn.setOnClickListener(v->showLanguageAlert());
visibilityBtn.setOnClickListener(this::onVisibilityClick);
visibilityBtn.setAccessibilityDelegate(new View.AccessibilityDelegate(){
@Override
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName("android.widget.Spinner");
}
});
Drawable arrow=getResources().getDrawable(R.drawable.ic_baseline_arrow_drop_down_18, getActivity().getTheme()).mutate();
arrow.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
visibilityBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrow, null);
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
@Override
public void onIconChanged(int icon){
@@ -323,7 +340,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(editingStatus!=null && editingStatus.visibility!=null) {
statusVisibility=editingStatus.visibility;
}
updateVisibilityIcon();
updateVisibilityIcon(false);
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
autocompleteViewController.setCompletionSelectedListener(new ComposeAutocompleteViewController.AutocompleteListener(){
@@ -909,24 +926,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
private void onVisibilityClick(View v){
PopupMenu menu=new PopupMenu(getActivity(), v);
menu.inflate(R.menu.compose_visibility);
menu.setOnMenuItemClickListener(item->{
int id=item.getItemId();
if(id==R.id.vis_public){
statusVisibility=StatusPrivacy.PUBLIC;
}else if(id==R.id.vis_unlisted){
statusVisibility=StatusPrivacy.UNLISTED;
}else if(id==R.id.vis_followers){
statusVisibility=StatusPrivacy.PRIVATE;
}else if(id==R.id.vis_private){
statusVisibility=StatusPrivacy.DIRECT;
ArrayList<ListItem<StatusPrivacy>> items=new ArrayList<>();
ExtendedPopupMenu menu=new ExtendedPopupMenu(getActivity(), items);
Consumer<ListItem<StatusPrivacy>> onClick=i->{
if(statusVisibility!=i.parentObject){
statusVisibility=i.parentObject;
updateVisibilityIcon(true);
}
item.setChecked(true);
updateVisibilityIcon();
return true;
});
menu.show();
menu.dismiss();
};
items.add(new ListItem<>(R.string.visibility_public, R.string.visibility_subtitle_public, R.drawable.ic_public_24px, StatusPrivacy.PUBLIC, onClick));
items.add(new ListItem<>(R.string.visibility_unlisted, R.string.visibility_subtitle_unlisted, R.drawable.ic_clear_night_24px, StatusPrivacy.UNLISTED, onClick));
items.add(new ListItem<>(R.string.visibility_followers_only, R.string.visibility_subtitle_followers, R.drawable.ic_lock_24px, StatusPrivacy.PRIVATE, onClick));
items.add(new ListItem<>(R.string.visibility_private, R.string.visibility_subtitle_private, R.drawable.ic_alternate_email_24px, StatusPrivacy.DIRECT, onClick));
menu.showAsDropDown(v);
}
private void loadDefaultStatusVisibility(Bundle savedInstanceState){
@@ -960,16 +973,31 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
updateVisibilityIcon();
updateVisibilityIcon(false);
}
private void updateVisibilityIcon(){
private void updateVisibilityIcon(boolean animated){
if(getActivity()==null)
return;
if(statusVisibility==null){ // TODO find out why this happens
statusVisibility=StatusPrivacy.PUBLIC;
}
visibilityBtn.setText(switch(statusVisibility){
TextView visibilityText;
if(!animated){
visibilityText=visibilityCurrentText;
}else{
TransitionManager.beginDelayedTransition(visibilityBtn, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.addTransition(new ChangeBounds().excludeTarget(TextView.class, true))
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
);
visibilityText=visibilityCurrentText==visibilityText1 ? visibilityText2 : visibilityText1;
visibilityText.setVisibility(View.VISIBLE);
visibilityCurrentText.setVisibility(View.GONE);
visibilityCurrentText=visibilityText;
}
visibilityText.setText(switch(statusVisibility){
case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
@@ -983,7 +1011,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}, getActivity().getTheme()).mutate();
icon.setBounds(0, 0, V.dp(18), V.dp(18));
icon.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
visibilityBtn.setCompoundDrawablesRelative(icon, null, visibilityBtn.getCompoundDrawablesRelative()[2], null);
visibilityText.setCompoundDrawablesRelative(icon, null, null, null);
}
@Override

View File

@@ -68,6 +68,12 @@ public class ListItem<T>{
this.subtitleRes=subtitleRes;
}
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, T parentObject, Consumer<ListItem<T>> onClick){
this(null, null, iconRes, onClick, parentObject, 0, false);
this.titleRes=titleRes;
this.subtitleRes=subtitleRes;
}
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick, int colorOverrideAttr, boolean dividerAfter){
this(null, null, iconRes, onClick, null, colorOverrideAttr, dividerAfter);
this.titleRes=titleRes;

View File

@@ -0,0 +1,61 @@
package org.joinmastodon.android.ui;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupWindow;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ExtendedPopupMenu extends PopupWindow{
private UsableRecyclerView list;
public <T> ExtendedPopupMenu(Context context, List<ListItem<T>> items){
super(context, null, 0, R.style.Widget_Mastodon_PopupMenu);
setWidth(V.dp(200));
setElevation(V.dp(3));
setOutsideTouchable(true);
setFocusable(true);
setInputMethodMode(INPUT_METHOD_NOT_NEEDED);
list=new UsableRecyclerView(context);
list.setLayoutManager(new LinearLayoutManager(context));
list.setAdapter(new ReducedPaddingItemsAdapter<>(items));
list.setClipToPadding(false);
setContentView(list);
}
@Override
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity){
super.showAsDropDown(anchor, xoff, yoff, gravity);
View bgView=(View) list.getParent();
list.setPadding(0, bgView.getPaddingTop(), 0, bgView.getPaddingBottom());
bgView.setPadding(0, 0, 0, 0);
}
private static class ReducedPaddingItemsAdapter<T> extends GenericListItemsAdapter<T>{
public ReducedPaddingItemsAdapter(List<ListItem<T>> listItems){
super(listItems);
}
@NonNull
@Override
public ListItemViewHolder<?> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
ListItemViewHolder<?> holder=super.onCreateViewHolder(parent, viewType);
int padH=V.dp(12), padV=V.dp(8);
holder.itemView.setPadding(padH, padV, padH, padV);
View icon=holder.itemView.findViewById(R.id.icon);
((ViewGroup.MarginLayoutParams)icon.getLayoutParams()).setMarginEnd(padH);
return holder;
}
}
}

View File

@@ -6,6 +6,7 @@ import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -19,6 +20,9 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragment;
import org.joinmastodon.android.fragments.account_list.StatusReblogsListFragment;
import org.joinmastodon.android.fragments.account_list.StatusRelatedAccountListFragment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -48,6 +52,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private final ImageView share;
private final ColorStateList buttonColors;
private final View replyBtn, boostBtn, favoriteBtn, shareBtn;
private final PopupMenu boostLongTapMenu, favoriteLongTapMenu;
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
@Override
@@ -97,11 +102,20 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
replyBtn.setOnClickListener(this::onReplyClick);
replyBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
boostBtn.setOnClickListener(this::onBoostClick);
boostBtn.setOnLongClickListener(this::onBoostLongClick);
boostBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
favoriteBtn.setOnClickListener(this::onFavoriteClick);
favoriteBtn.setOnLongClickListener(this::onFavoriteLongClick);
favoriteBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
shareBtn.setOnClickListener(this::onShareClick);
shareBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
favoriteLongTapMenu=new PopupMenu(activity, favoriteBtn);
favoriteLongTapMenu.inflate(R.menu.favorite_longtap);
favoriteLongTapMenu.setOnMenuItemClickListener(this::onLongTapMenuItemSelected);
boostLongTapMenu=new PopupMenu(activity, boostBtn);
boostLongTapMenu.inflate(R.menu.boost_longtap);
boostLongTapMenu.setOnMenuItemClickListener(this::onLongTapMenuItemSelected);
}
@Override
@@ -172,6 +186,45 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
UiUtils.openSystemShareSheet(v.getContext(), item.status);
}
private boolean onBoostLongClick(View v){
MenuItem boost=boostLongTapMenu.getMenu().findItem(R.id.boost);
boost.setTitle(item.status.reblogged ? R.string.undo_reblog : R.string.button_reblog);
boostLongTapMenu.show();
return true;
}
private boolean onFavoriteLongClick(View v){
MenuItem favorite=favoriteLongTapMenu.getMenu().findItem(R.id.favorite);
MenuItem bookmark=favoriteLongTapMenu.getMenu().findItem(R.id.bookmark);
favorite.setTitle(item.status.favourited ? R.string.undo_favorite : R.string.button_favorite);
bookmark.setTitle(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark);
favoriteLongTapMenu.show();
return true;
}
private boolean onLongTapMenuItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.favorite){
onFavoriteClick(null);
}else if(id==R.id.boost){
onBoostClick(null);
}else if(id==R.id.bookmark){
AccountSessionManager.getInstance().getAccount(this.item.accountID).getStatusInteractionController().setBookmarked(this.item.status, !this.item.status.bookmarked);
}else if(id==R.id.view_favorites){
startAccountListFragment(StatusFavoritesListFragment.class);
}else if(id==R.id.view_boosts){
startAccountListFragment(StatusReblogsListFragment.class);
}
return true;
}
private void startAccountListFragment(Class<? extends StatusRelatedAccountListFragment> cls){
Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
args.putParcelable("status", Parcels.wrap(item.status));
Nav.go(item.parentFragment.getActivity(), cls, args);
}
private int descriptionForId(int id){
if(id==R.id.reply_btn)
return R.string.button_reply;

View File

@@ -119,6 +119,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private final TextView name, timeAndUsername, extraText;
private final ImageView avatar, more;
private final PopupMenu optionsMenu;
private final View clickableThing;
public Holder(Activity activity, ViewGroup parent){
this(activity, R.layout.display_item_header, parent);
@@ -131,7 +132,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
avatar=findViewById(R.id.avatar);
more=findViewById(R.id.more);
extraText=findViewById(R.id.extra_text);
avatar.setOnClickListener(this::onAvaClick);
clickableThing=findViewById(R.id.clickable_thing);
clickableThing.setOnClickListener(this::onAvaClick);
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
avatar.setClipToOutline(true);
more.setOnClickListener(this::onMoreClick);
@@ -262,7 +264,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
time=item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt));
timeAndUsername.setText(time+" · @"+item.user.acct);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : V.dp(4));
if(TextUtils.isEmpty(item.extraText)){
extraText.setVisibility(View.GONE);
}else{
@@ -270,8 +272,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
extraText.setText(item.extraText);
}
more.setVisibility(item.inset ? View.GONE : View.VISIBLE);
avatar.setClickable(!item.inset);
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
clickableThing.setClickable(!item.inset);
clickableThing.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
}
@Override

View File

@@ -125,7 +125,8 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
public void onBind(NotificationHeaderStatusDisplayItem item){
text.setText(item.text);
avatar.setVisibility(item.notification.type==Notification.Type.POLL ? View.GONE : View.VISIBLE);
// TODO use real icons
if(item.notification.type!=Notification.Type.POLL)
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.notification.account.acct));
icon.setImageResource(switch(item.notification.type){
case FAVORITE -> R.drawable.ic_star_fill1_24px;
case REBLOG -> R.drawable.ic_repeat_fill1_24px;

View File

@@ -94,7 +94,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
text.setTextIsSelectable(item.textSelectable);
text.setInvalidateOnEveryFrame(false);
itemView.setClickable(false);
text.setPadding(text.getPaddingLeft(), item.reduceTopPadding ? V.dp(8) : V.dp(16), text.getPaddingRight(), text.getPaddingBottom());
text.setPadding(text.getPaddingLeft(), item.reduceTopPadding ? V.dp(8) : V.dp(12), text.getPaddingRight(), text.getPaddingBottom());
text.setTextColor(UiUtils.getThemeColor(text.getContext(), item.inset ? R.attr.colorM3OnSurfaceVariant : R.attr.colorM3OnSurface));
updateTranslation(false);
}

View File

@@ -69,6 +69,7 @@ public class PhotoViewerInfoSheet extends BottomSheet{
backButton.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
backButton.setElevation(V.dp(2));
backButton.setAlpha(0f);
backButton.setContentDescription(context.getString(R.string.back));
backButton.setOnClickListener(v->{
listener.onDismissEntireViewer();
dismiss();
@@ -82,6 +83,7 @@ public class PhotoViewerInfoSheet extends BottomSheet{
infoButton.setElevation(V.dp(2));
infoButton.setAlpha(0f);
infoButton.setSelected(true);
infoButton.setContentDescription(context.getString(R.string.info));
infoButton.setOnClickListener(v->dismiss());
FrameLayout.LayoutParams lp=new FrameLayout.LayoutParams(V.dp(48), V.dp(48));