chore(merging-upstream): bunch of conflicts to solve
This commit is contained in:
@@ -1,16 +1,19 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
@@ -23,14 +26,15 @@ import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.SplashFragment;
|
||||
import org.joinmastodon.android.ui.ClickableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
@@ -57,15 +61,24 @@ import me.grishka.appkit.views.UsableRecyclerView;
|
||||
public class AccountSwitcherSheet extends BottomSheet{
|
||||
private final Activity activity;
|
||||
private final HomeFragment fragment;
|
||||
private final boolean externalShare, openInApp;
|
||||
private BiConsumer<String, Boolean> onClick;
|
||||
private UsableRecyclerView list;
|
||||
private List<WrappedAccount> accounts;
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
private AccountsAdapter accountsAdapter;
|
||||
private Runnable onLoggedOutCallback;
|
||||
|
||||
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment){
|
||||
this(activity, fragment, false, false);
|
||||
}
|
||||
|
||||
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment, boolean externalShare, boolean openInApp){
|
||||
super(activity);
|
||||
this.activity=activity;
|
||||
this.fragment=fragment;
|
||||
this.externalShare = externalShare;
|
||||
this.openInApp = openInApp;
|
||||
|
||||
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
|
||||
|
||||
@@ -78,13 +91,30 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
View handle=new View(activity);
|
||||
handle.setBackgroundResource(R.drawable.bg_bottom_sheet_handle);
|
||||
handle.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(36)));
|
||||
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(handle));
|
||||
adapter.addAdapter(new AccountsAdapter());
|
||||
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.add_account, R.drawable.ic_add_24px), ()->{
|
||||
Nav.go(activity, SplashFragment.class, null);
|
||||
dismiss();
|
||||
}));
|
||||
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.log_out_all_accounts, R.drawable.ic_logout_24px), this::confirmLogOutAll));
|
||||
|
||||
if (externalShare) {
|
||||
FrameLayout shareHeading = new FrameLayout(activity);
|
||||
activity.getLayoutInflater().inflate(R.layout.item_external_share_heading, shareHeading);
|
||||
((TextView) shareHeading.findViewById(R.id.title)).setText(openInApp
|
||||
? R.string.sk_external_share_or_open_title
|
||||
: R.string.sk_external_share_title);
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(shareHeading));
|
||||
|
||||
setOnDismissListener((d) -> activity.finish());
|
||||
}
|
||||
|
||||
adapter.addAdapter(accountsAdapter = new AccountsAdapter());
|
||||
|
||||
if (!externalShare) {
|
||||
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.add_account, R.drawable.ic_fluent_add_24_regular), () -> {
|
||||
Nav.go(activity, CustomWelcomeFragment.class, null);
|
||||
dismiss();
|
||||
}));
|
||||
// disabled in megalodon
|
||||
// adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.log_out_all_accounts, R.drawable.ic_fluent_person_arrow_right_24_filled), this::confirmLogOutAll));
|
||||
}
|
||||
|
||||
list.setAdapter(adapter);
|
||||
|
||||
@@ -101,9 +131,14 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setOnClick(BiConsumer<String, Boolean> onClick) {
|
||||
this.onClick = onClick;
|
||||
}
|
||||
|
||||
private void confirmLogOut(String accountID){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new M3AlertDialogBuilder(activity)
|
||||
.setTitle(R.string.log_out)
|
||||
.setMessage(activity.getString(R.string.confirm_log_out, session.getFullUsername()))
|
||||
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut(accountID))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
@@ -167,6 +202,22 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
}
|
||||
}
|
||||
|
||||
private void onLoggedOut(String accountID){
|
||||
AccountSessionManager.getInstance().removeAccount(accountID);
|
||||
String activeAccountID = fragment != null
|
||||
? fragment.getAccountID()
|
||||
: AccountSessionManager.getInstance().getLastActiveAccountID();
|
||||
if (accountID.equals(activeAccountID)) {
|
||||
activity.finish();
|
||||
activity.startActivity(new Intent(activity, MainActivity.class));
|
||||
} else {
|
||||
accounts.stream().filter(w -> accountID.equals(w.session.getID())).findAny().ifPresent(w -> {
|
||||
accountsAdapter.notifyItemRemoved(accounts.indexOf(w));
|
||||
accounts.remove(w);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onWindowInsetsUpdated(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29){
|
||||
@@ -226,25 +277,38 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
private final TextView name, username;
|
||||
private final ImageView avatar;
|
||||
private final CheckableRelativeLayout view;
|
||||
private final View radioButton, extraBtnWrap;
|
||||
private final ImageButton extraBtn;
|
||||
|
||||
public AccountViewHolder(){
|
||||
super(activity, R.layout.item_account_switcher, list);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
View radioButton=findViewById(R.id.radiobtn);
|
||||
radioButton=findViewById(R.id.radiobtn);
|
||||
radioButton.setBackground(new RadioButton(activity).getButtonDrawable());
|
||||
avatar=findViewById(R.id.avatar);
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(OutlineProviders.RADIUS_MEDIUM));
|
||||
avatar.setClipToOutline(true);
|
||||
view=(CheckableRelativeLayout) itemView;
|
||||
extraBtnWrap = findViewById(R.id.extra_btn_wrap);
|
||||
extraBtn = findViewById(R.id.extra_btn);
|
||||
extraBtn.setOnClickListener(this::onExtraBtnClick);
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void onBind(AccountSession item){
|
||||
name.setText(item.self.displayName);
|
||||
HtmlParser.setTextWithCustomEmoji(name, item.self.getDisplayName(), item.self.emojis);
|
||||
username.setText(item.getFullUsername());
|
||||
view.setChecked(AccountSessionManager.getInstance().getLastActiveAccountID().equals(item.getID()));
|
||||
radioButton.setVisibility(externalShare ? View.GONE : View.VISIBLE);
|
||||
extraBtnWrap.setVisibility(externalShare && openInApp ? View.VISIBLE : View.GONE);
|
||||
if (externalShare) view.setCheckable(false);
|
||||
else {
|
||||
String accountId = fragment != null
|
||||
? fragment.getAccountID()
|
||||
: AccountSessionManager.getInstance().getLastActiveAccountID();
|
||||
view.setChecked(accountId.equals(item.getID()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -259,23 +323,29 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
setImage(index, null);
|
||||
}
|
||||
|
||||
private void onExtraBtnClick(View view) {
|
||||
setOnDismissListener(null);
|
||||
dismiss();
|
||||
onClick.accept(item.getID(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
dismiss();
|
||||
if(AccountSessionManager.getInstance().getLastActiveAccountID().equals(item.getID())){
|
||||
if(fragment!=null){
|
||||
fragment.setCurrentTab(R.id.tab_profile);
|
||||
}
|
||||
setOnDismissListener(null);
|
||||
if (onClick != null) {
|
||||
dismiss();
|
||||
onClick.accept(item.getID(), false);
|
||||
return;
|
||||
}
|
||||
if(AccountSessionManager.getInstance().tryGetAccount(item.getID())!=null){
|
||||
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
|
||||
((MainActivity)activity).restartHomeFragment();
|
||||
((MainActivity)activity).restartActivity();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(){
|
||||
if (externalShare) return false;
|
||||
confirmLogOut(item.getID());
|
||||
return true;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,87 @@
|
||||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.BottomSheet;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ImageDescriptionSheet extends BottomSheet{
|
||||
private UsableRecyclerView list;
|
||||
|
||||
public ImageDescriptionSheet(@NonNull Activity activity, Attachment attachment){
|
||||
super(activity);
|
||||
|
||||
View handleView=new View(activity);
|
||||
handleView.setBackgroundResource(R.drawable.bg_bottom_sheet_handle);
|
||||
ViewGroup handle=new FrameLayout(activity);
|
||||
handle.addView(handleView);
|
||||
handle.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(24)));
|
||||
|
||||
TextView textView = new TextView(activity);
|
||||
if (attachment.description == null || attachment.description.isEmpty()) {
|
||||
textView.setText(R.string.media_no_description);
|
||||
textView.setTypeface(null, Typeface.ITALIC);
|
||||
} else {
|
||||
textView.setText(attachment.description);
|
||||
textView.setTextIsSelectable(true);
|
||||
}
|
||||
|
||||
TextView heading=new TextView(activity);
|
||||
heading.setText(R.string.sk_image_description);
|
||||
heading.setAllCaps(true);
|
||||
heading.setTypeface(null, Typeface.BOLD);
|
||||
heading.setPadding(0, V.dp(24), 0, V.dp(8));
|
||||
|
||||
LinearLayout linearLayout = new LinearLayout(activity);
|
||||
linearLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
linearLayout.setPadding(V.dp(24), 0, V.dp(24), 0);
|
||||
linearLayout.addView(heading);
|
||||
linearLayout.addView(textView);
|
||||
|
||||
FrameLayout layout=new FrameLayout(activity);
|
||||
layout.addView(handle);
|
||||
layout.addView(linearLayout);
|
||||
|
||||
list=new UsableRecyclerView(activity);
|
||||
list.setLayoutManager(new LinearLayoutManager(activity));
|
||||
list.setBackgroundResource(R.drawable.bg_bottom_sheet);
|
||||
list.setAdapter(new SingleViewRecyclerAdapter(layout));
|
||||
list.setClipToPadding(false);
|
||||
|
||||
setContentView(list);
|
||||
setNavigationBarBackground(new ColorDrawable(UiUtils.getThemeColor(activity, R.attr.colorM3Surface)), !UiUtils.isDarkTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onWindowInsetsUpdated(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29){
|
||||
int tappableBottom=insets.getTappableElementInsets().bottom;
|
||||
int insetBottom=insets.getSystemWindowInsetBottom();
|
||||
if(tappableBottom==0 && insetBottom>0){
|
||||
list.setPadding(0, 0, 0, V.dp(48)-insetBottom);
|
||||
}else{
|
||||
list.setPadding(0, 0, 0, V.dp(24));
|
||||
}
|
||||
}else{
|
||||
list.setPadding(0, 0, 0, V.dp(24));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
|
||||
View title=alert.findViewById(titleID);
|
||||
if(title!=null){
|
||||
int pad=V.dp(24);
|
||||
title.setPadding(pad, pad, pad, pad);
|
||||
title.setPadding(pad, pad, pad, V.dp(18));
|
||||
}
|
||||
}
|
||||
int titleDividerID=getContext().getResources().getIdentifier("titleDividerNoCustom", "id", "android");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
@@ -14,7 +14,6 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
@@ -37,6 +37,16 @@ public class OutlineProviders{
|
||||
}
|
||||
};
|
||||
|
||||
private final static int BUTTON_BG_HEIGHT=V.dp(40);
|
||||
public static final ViewOutlineProvider M3_BUTTON=new ViewOutlineProvider(){
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
int viewHeight=view.getHeight();
|
||||
int top=Math.floorDiv(viewHeight - BUTTON_BG_HEIGHT, 2);
|
||||
outline.setRoundRect(0, top, view.getWidth(), top + BUTTON_BG_HEIGHT, V.dp(20));
|
||||
}
|
||||
};
|
||||
|
||||
public static ViewOutlineProvider roundedRect(int dp){
|
||||
ViewOutlineProvider provider=roundedRects.get(dp);
|
||||
if(provider!=null)
|
||||
|
||||
@@ -10,12 +10,16 @@ import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class PhotoLayoutHelper{
|
||||
public static final int MAX_WIDTH=1000;
|
||||
public static final int MAX_HEIGHT=1777; // 9:16
|
||||
public static final int MIN_HEIGHT=563;
|
||||
public static final int MAX_HEIGHT=1700;
|
||||
public static final float GAP=1.5f;
|
||||
|
||||
// 2 * margin + close button height - gap. i don't know if the gap subtraction is correct
|
||||
public static final int MIN_HEIGHT = Math.round(V.dp(2 * 12) + V.dp(40) - GAP);
|
||||
|
||||
@NonNull
|
||||
public static TiledLayoutResult processThumbs(List<Attachment> thumbs){
|
||||
float maxRatio=MAX_WIDTH/(float)MAX_HEIGHT;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
@@ -45,13 +45,14 @@ public class SearchViewHelper{
|
||||
searchEdit.setHint(hint);
|
||||
searchEdit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
|
||||
searchEdit.setBackground(null);
|
||||
searchEdit.setPadding(0, 0, 0, 0);
|
||||
searchEdit.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
searchEdit.removeCallbacks(debouncer);
|
||||
searchEdit.postDelayed(debouncer, 500);
|
||||
boolean newIsEmpty=e.length()==0;
|
||||
if(isEmpty!=newIsEmpty){
|
||||
isEmpty=newIsEmpty;
|
||||
V.setVisibilityAnimated(clearSearchButton, isEmpty ? View.INVISIBLE : View.VISIBLE);
|
||||
V.setVisibilityAnimated(clearSearchButton, isEmpty ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
if(listenerWithoutDebounce!=null)
|
||||
listenerWithoutDebounce.accept(e.toString());
|
||||
@@ -70,7 +71,7 @@ public class SearchViewHelper{
|
||||
searchLayout.addView(searchEdit, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||
|
||||
clearSearchButton=new ImageButton(context);
|
||||
clearSearchButton.setImageResource(R.drawable.ic_baseline_close_24);
|
||||
clearSearchButton.setImageResource(R.drawable.ic_fluent_dismiss_24_regular);
|
||||
clearSearchButton.setContentDescription(context.getString(R.string.clear));
|
||||
clearSearchButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurfaceVariant)));
|
||||
clearSearchButton.setBackground(UiUtils.getThemeDrawable(toolbarContext, android.R.attr.actionBarItemBackground));
|
||||
@@ -79,7 +80,7 @@ public class SearchViewHelper{
|
||||
searchEdit.removeCallbacks(debouncer);
|
||||
debouncer.run();
|
||||
});
|
||||
clearSearchButton.setVisibility(View.INVISIBLE);
|
||||
clearSearchButton.setVisibility(View.GONE);
|
||||
searchLayout.addView(clearSearchButton, new LinearLayout.LayoutParams(V.dp(56), ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ public class Snackbar{
|
||||
if(current!=null)
|
||||
current.dismiss();
|
||||
current=this;
|
||||
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.LAST_APPLICATION_WINDOW, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT);
|
||||
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT);
|
||||
lp.width=ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
lp.height=ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
lp.gravity=Gravity.BOTTOM;
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
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.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AccountCardStatusDisplayItem extends StatusDisplayItem{
|
||||
private final Account account;
|
||||
private final Notification notification;
|
||||
public ImageLoaderRequest avaRequest, coverRequest;
|
||||
public CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
public CharSequence parsedName, parsedBio;
|
||||
|
||||
public AccountCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, String accountID, Account account, Notification notification){
|
||||
super(parentID, parentFragment);
|
||||
this.account=account;
|
||||
this.notification=notification;
|
||||
avaRequest=new UrlImageLoaderRequest(
|
||||
TextUtils.isEmpty(account.avatar) ? AccountSessionManager.get(accountID).getDefaultAvatarUrl() : account.avatar,
|
||||
V.dp(50), V.dp(50));
|
||||
if(!TextUtils.isEmpty(account.header))
|
||||
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
|
||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), parentFragment.getAccountID());
|
||||
if(account.emojis.isEmpty()){
|
||||
parsedName=account.getDisplayName();
|
||||
}else{
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.getDisplayName(), account.emojis);
|
||||
emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.ACCOUNT_CARD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return 2+emojiHelper.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return switch(index){
|
||||
case 0 -> avaRequest;
|
||||
case 1 -> coverRequest;
|
||||
default -> emojiHelper.getImageRequest(index-2);
|
||||
};
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<AccountCardStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final ImageView cover, avatar;
|
||||
private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel;
|
||||
private final ProgressBarButton actionButton, acceptButton, rejectButton;
|
||||
private final ProgressBar actionProgress, acceptProgress, rejectProgress;
|
||||
private final View actionWrap, acceptWrap, rejectWrap;
|
||||
|
||||
private Relationship relationship;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_account_card, parent);
|
||||
|
||||
cover=findViewById(R.id.cover);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
bio=findViewById(R.id.bio);
|
||||
followersCount=findViewById(R.id.followers_count);
|
||||
followersLabel=findViewById(R.id.followers_label);
|
||||
followingCount=findViewById(R.id.following_count);
|
||||
followingLabel=findViewById(R.id.following_label);
|
||||
postsCount=findViewById(R.id.posts_count);
|
||||
postsLabel=findViewById(R.id.posts_label);
|
||||
actionButton=findViewById(R.id.action_btn);
|
||||
actionProgress=findViewById(R.id.action_progress);
|
||||
actionWrap=findViewById(R.id.action_btn_wrap);
|
||||
acceptButton=findViewById(R.id.accept_btn);
|
||||
acceptProgress=findViewById(R.id.accept_progress);
|
||||
acceptWrap=findViewById(R.id.accept_btn_wrap);
|
||||
rejectButton=findViewById(R.id.reject_btn);
|
||||
rejectProgress=findViewById(R.id.reject_progress);
|
||||
rejectWrap=findViewById(R.id.reject_btn_wrap);
|
||||
|
||||
View card=findViewById(R.id.card);
|
||||
card.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
card.setClipToOutline(true);
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(15));
|
||||
avatar.setClipToOutline(true);
|
||||
View border=findViewById(R.id.avatar_border);
|
||||
border.setOutlineProvider(OutlineProviders.roundedRect(17));
|
||||
border.setClipToOutline(true);
|
||||
cover.setOutlineProvider(OutlineProviders.roundedRect(9));
|
||||
cover.setClipToOutline(true);
|
||||
actionButton.setOnClickListener(this::onActionButtonClick);
|
||||
acceptButton.setOnClickListener(this::onFollowRequestButtonClick);
|
||||
rejectButton.setOnClickListener(this::onFollowRequestButtonClick);
|
||||
card.setOnClickListener(v->onClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountCardStatusDisplayItem item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText('@'+item.account.acct);
|
||||
bio.setText(item.parsedBio);
|
||||
followersCount.setText(UiUtils.abbreviateNumber(item.account.followersCount));
|
||||
followingCount.setText(UiUtils.abbreviateNumber(item.account.followingCount));
|
||||
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
|
||||
followersLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
||||
followingLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
||||
postsLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.sk_posts_count_label, (int)(item.account.statusesCount%1000), item.account.statusesCount));
|
||||
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
|
||||
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
|
||||
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||
relationship=item.parentFragment.getRelationship(item.account.id);
|
||||
UiUtils.setExtraTextInfo(item.parentFragment.getContext(), null,true, false, false, item.account);
|
||||
|
||||
if(item.notification.type==Notification.Type.FOLLOW_REQUEST && (relationship==null || !relationship.followedBy)){
|
||||
actionWrap.setVisibility(View.GONE);
|
||||
acceptWrap.setVisibility(View.VISIBLE);
|
||||
rejectWrap.setVisibility(View.VISIBLE);
|
||||
|
||||
acceptButton.setCompoundDrawableTintList(acceptButton.getTextColors());
|
||||
acceptProgress.setIndeterminateTintList(acceptButton.getTextColors());
|
||||
rejectButton.setCompoundDrawableTintList(rejectButton.getTextColors());
|
||||
rejectProgress.setIndeterminateTintList(rejectButton.getTextColors());
|
||||
}else if(relationship==null){
|
||||
actionWrap.setVisibility(View.GONE);
|
||||
acceptWrap.setVisibility(View.GONE);
|
||||
rejectWrap.setVisibility(View.GONE);
|
||||
}else{
|
||||
actionWrap.setVisibility(View.VISIBLE);
|
||||
acceptWrap.setVisibility(View.GONE);
|
||||
rejectWrap.setVisibility(View.GONE);
|
||||
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
|
||||
}
|
||||
}
|
||||
|
||||
private void onFollowRequestButtonClick(View v) {
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, item.parentFragment.getAccountID(), null, v == acceptButton, relationship, rel -> {
|
||||
if(v.getContext()==null || rel==null) return;
|
||||
itemView.setHasTransientState(false);
|
||||
item.parentFragment.putRelationship(item.account.id, rel);
|
||||
RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter = getBindingAdapter();
|
||||
if (!rel.requested && !rel.followedBy && adapter != null) {
|
||||
int index = item.parentFragment.getDisplayItems().indexOf(item);
|
||||
item.parentFragment.getDisplayItems().remove(index);
|
||||
item.parentFragment.getDisplayItems().remove(index - 1);
|
||||
adapter.notifyItemRangeRemoved(getLayoutPosition()-1, 2);
|
||||
} else {
|
||||
rebind();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onActionButtonClick(View v){
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.performAccountAction((Activity) v.getContext(), item.account, item.parentFragment.getAccountID(), relationship, actionButton, this::setActionProgressVisible, rel->{
|
||||
if(v.getContext()==null) return;
|
||||
itemView.setHasTransientState(false);
|
||||
item.parentFragment.putRelationship(item.account.id, rel);
|
||||
rebind();
|
||||
});
|
||||
}
|
||||
|
||||
private void setActionProgressVisible(boolean visible){
|
||||
actionButton.setTextVisible(!visible);
|
||||
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
if(visible)
|
||||
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
|
||||
actionButton.setClickable(!visible);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
if(index==0){
|
||||
avatar.setImageDrawable(image);
|
||||
}else if(index==1){
|
||||
cover.setImageDrawable(image);
|
||||
}else{
|
||||
item.emojiHelper.setImageDrawable(index-2, image);
|
||||
name.invalidate();
|
||||
bio.invalidate();
|
||||
}
|
||||
if(image instanceof Animatable && !((Animatable) image).isRunning())
|
||||
((Animatable) image).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
public final Attachment attachment;
|
||||
private final ImageLoaderRequest imageRequest;
|
||||
|
||||
@@ -40,7 +39,7 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
this.attachment=attachment;
|
||||
imageRequest=new UrlImageLoaderRequest(TextUtils.isEmpty(attachment.previewUrl) ? status.account.avatarStatic : attachment.previewUrl, V.dp(100), V.dp(100));
|
||||
imageRequest=new UrlImageLoaderRequest(TextUtils.isEmpty(attachment.previewUrl) ? (status.account != null ? status.account.avatarStatic : "") : attachment.previewUrl, V.dp(100), V.dp(100));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -214,7 +213,7 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void setPlayButtonPlaying(boolean playing, boolean animated){
|
||||
playPauseBtn.setImageResource(playing ? R.drawable.ic_pause_48px : R.drawable.ic_play_arrow_48px);
|
||||
playPauseBtn.setImageResource(playing ? R.drawable.ic_fluent_pause_48_regular : R.drawable.ic_fluent_play_48_regular);
|
||||
playPauseBtn.setContentDescription(item.parentFragment.getString(playing ? R.string.pause : R.string.play));
|
||||
if(playing)
|
||||
bgDrawable.startAnimation();
|
||||
|
||||
@@ -3,10 +3,12 @@ package org.joinmastodon.android.ui.displayitems;
|
||||
import android.app.Activity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportAddPostsChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
|
||||
@@ -15,8 +17,8 @@ import java.time.Instant;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{
|
||||
public CheckableHeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText){
|
||||
super(parentID, user, createdAt, parentFragment, accountID, status, extraText);
|
||||
public CheckableHeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment<?> parentFragment, String accountID, Status status, CharSequence extraText){
|
||||
super(parentID, user, createdAt, parentFragment, accountID, status, extraText, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -32,8 +34,16 @@ public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_header_checkable, parent);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
view=(CheckableRelativeLayout) itemView;
|
||||
view=findViewById(R.id.checkbox_wrap);
|
||||
checkbox.setBackground(new CheckBox(activity).getButtonDrawable());
|
||||
view.setOnClickListener(this::onToggle);
|
||||
view.setAccessibilityDelegate(new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info){
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setClassName(CheckBox.class.getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,6 +54,12 @@ public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{
|
||||
}
|
||||
}
|
||||
|
||||
private void onToggle(View v){
|
||||
if(item.parentFragment instanceof ReportAddPostsChoiceFragment reportFragment){
|
||||
reportFragment.onToggleItem(item.parentID);
|
||||
}
|
||||
}
|
||||
|
||||
public void setIsChecked(Predicate<Holder> isChecked){
|
||||
this.isChecked=isChecked;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Space;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class DummyStatusDisplayItem extends StatusDisplayItem {
|
||||
|
||||
public DummyStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment) {
|
||||
super(parentID, parentFragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.DUMMY;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<DummyStatusDisplayItem> {
|
||||
private final RecyclerView.LayoutParams params;
|
||||
|
||||
public Holder(Context context) {
|
||||
super(new Space(context));
|
||||
params=new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
|
||||
|
||||
// BetterItemAnimator appears not to handle InsetStatusItemDecoration's getItemOffsets
|
||||
// correctly, causing removed inset views to jump while animating. i don't quite
|
||||
// understand it, but this workaround appears to work.
|
||||
// see InsetStatusItemDecoration#getItemOffsets
|
||||
params.setMargins(0, 0, 0, V.dp(16));
|
||||
itemView.setLayoutParams(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(DummyStatusDisplayItem item) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.announcements.AddAnnouncementReaction;
|
||||
import org.joinmastodon.android.api.requests.announcements.DeleteAnnouncementReaction;
|
||||
import org.joinmastodon.android.api.requests.statuses.AddStatusReaction;
|
||||
import org.joinmastodon.android.api.requests.statuses.DeleteStatusReaction;
|
||||
import org.joinmastodon.android.api.requests.statuses.PleromaAddStatusReaction;
|
||||
import org.joinmastodon.android.api.requests.statuses.PleromaDeleteStatusReaction;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.StatusEmojiReactionsListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiReaction;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard;
|
||||
import org.joinmastodon.android.ui.utils.TextDrawable;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
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.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
|
||||
private final Drawable placeholder;
|
||||
private final boolean hideEmpty, forAnnouncement, playGifs;
|
||||
private final String accountID;
|
||||
private static final float ALPHA_DISABLED=0.55f;
|
||||
|
||||
public EmojiReactionsStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, String accountID, boolean hideEmpty, boolean forAnnouncement) {
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
this.hideEmpty=hideEmpty;
|
||||
this.forAnnouncement=forAnnouncement;
|
||||
this.accountID=accountID;
|
||||
placeholder=parentFragment.getContext().getDrawable(R.drawable.image_placeholder).mutate();
|
||||
placeholder.setBounds(0, 0, V.sp(24), V.sp(24));
|
||||
playGifs=GlobalUserPreferences.playGifs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return (int) status.reactions.stream().filter(r->r.getUrl(playGifs)!=null).count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return status.reactions.get(index).request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.EMOJI_REACTIONS;
|
||||
}
|
||||
|
||||
public boolean isHidden(){
|
||||
return status.reactions.isEmpty() && hideEmpty;
|
||||
}
|
||||
|
||||
// borrowed from ProfileFragment
|
||||
private void setActionProgressVisible(Holder.EmojiReactionViewHolder vh, boolean visible){
|
||||
if(vh==null) return;
|
||||
vh.progress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
vh.btn.setClickable(!visible);
|
||||
vh.btn.setAlpha(visible ? ALPHA_DISABLED : 1);
|
||||
}
|
||||
|
||||
private MastodonAPIRequest<?> createRequest(String name, int count, boolean delete, Holder.EmojiReactionViewHolder vh, Runnable cb, Runnable err){
|
||||
setActionProgressVisible(vh, true);
|
||||
boolean ak=parentFragment.isInstanceAkkoma();
|
||||
boolean keepSpinning=delete && count == 1;
|
||||
if(forAnnouncement){
|
||||
MastodonAPIRequest<Object> req=delete
|
||||
? new DeleteAnnouncementReaction(status.id, name)
|
||||
: new AddAnnouncementReaction(status.id, name);
|
||||
return req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
if(!keepSpinning) setActionProgressVisible(vh, false);
|
||||
cb.run();
|
||||
}
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
setActionProgressVisible(vh, false);
|
||||
error.showToast(parentFragment.getContext());
|
||||
if(err!=null) err.run();
|
||||
}
|
||||
});
|
||||
}else{
|
||||
MastodonAPIRequest<Status> req=delete
|
||||
? (ak ? new PleromaDeleteStatusReaction(status.id, name) : new DeleteStatusReaction(status.id, name))
|
||||
: (ak ? new PleromaAddStatusReaction(status.id, name) : new AddStatusReaction(status.id, name));
|
||||
return req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
if(!keepSpinning) setActionProgressVisible(vh, false);
|
||||
cb.run();
|
||||
}
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
setActionProgressVisible(vh, false);
|
||||
error.showToast(parentFragment.getContext());
|
||||
if(err!=null) err.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<EmojiReactionsStatusDisplayItem> implements ImageLoaderViewHolder, CustomEmojiPopupKeyboard.Listener {
|
||||
private final UsableRecyclerView list;
|
||||
private final LinearLayout root, line;
|
||||
private CustomEmojiPopupKeyboard emojiKeyboard;
|
||||
private final View space;
|
||||
private final ImageButton addButton;
|
||||
private final ProgressBar progress;
|
||||
private final EmojiReactionsAdapter adapter;
|
||||
private final ListImageLoaderWrapper imgLoader;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent) {
|
||||
super(activity, R.layout.display_item_emoji_reactions, parent);
|
||||
root=(LinearLayout) itemView;
|
||||
line=findViewById(R.id.line);
|
||||
list=findViewById(R.id.list);
|
||||
imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null);
|
||||
list.setAdapter(adapter=new EmojiReactionsAdapter(this, imgLoader));
|
||||
addButton=findViewById(R.id.add_btn);
|
||||
progress=findViewById(R.id.progress);
|
||||
addButton.setOnClickListener(this::onReactClick);
|
||||
space=findViewById(R.id.space);
|
||||
list.setLayoutManager(new LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(EmojiReactionsStatusDisplayItem item) {
|
||||
if(emojiKeyboard != null) root.removeView(emojiKeyboard.getView());
|
||||
addButton.setSelected(false);
|
||||
AccountSession session=item.parentFragment.getSession();
|
||||
item.status.reactions.forEach(r->r.request=r.getUrl(item.playGifs)!=null
|
||||
? new UrlImageLoaderRequest(r.getUrl(item.playGifs), V.sp(24), V.sp(24))
|
||||
: null);
|
||||
emojiKeyboard=new CustomEmojiPopupKeyboard(
|
||||
(Activity) item.parentFragment.getContext(),
|
||||
item.accountID,
|
||||
AccountSessionManager.getInstance().getCustomEmojis(session.domain),
|
||||
session.domain, true);
|
||||
emojiKeyboard.setListener(this);
|
||||
space.setVisibility(View.GONE);
|
||||
root.addView(emojiKeyboard.getView());
|
||||
boolean hidden=item.isHidden();
|
||||
root.setVisibility(hidden ? View.GONE : View.VISIBLE);
|
||||
line.setVisibility(hidden ? View.GONE : View.VISIBLE);
|
||||
line.setPadding(
|
||||
list.getPaddingLeft(),
|
||||
hidden ? 0 : V.dp(8),
|
||||
list.getPaddingRight(),
|
||||
item.forAnnouncement ? V.dp(8) : 0
|
||||
);
|
||||
imgLoader.updateImages();
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void hideEmojiKeyboard(){
|
||||
space.setVisibility(View.GONE);
|
||||
addButton.setSelected(false);
|
||||
if(emojiKeyboard.isVisible()) emojiKeyboard.toggleKeyboardPopup(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEmojiSelected(Emoji emoji) {
|
||||
addEmojiReaction(emoji.shortcode, emoji);
|
||||
hideEmojiKeyboard();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji){
|
||||
addEmojiReaction(emoji, null);
|
||||
hideEmojiKeyboard();
|
||||
}
|
||||
|
||||
private void addEmojiReaction(String emoji, Emoji info) {
|
||||
int countBefore=item.status.reactions.size();
|
||||
for(int i=0; i<item.status.reactions.size(); i++){
|
||||
EmojiReaction r=item.status.reactions.get(i);
|
||||
if(r.name.equals(emoji) && r.me){
|
||||
RecyclerView.SmoothScroller scroller=new LinearSmoothScroller(list.getContext());
|
||||
scroller.setTargetPosition(i);
|
||||
list.getLayoutManager().startSmoothScroll(scroller);
|
||||
return; // nothing to do, already added
|
||||
}
|
||||
}
|
||||
|
||||
progress.setVisibility(View.VISIBLE);
|
||||
addButton.setClickable(false);
|
||||
addButton.setAlpha(ALPHA_DISABLED);
|
||||
|
||||
Runnable resetBtn=()->{
|
||||
progress.setVisibility(View.GONE);
|
||||
addButton.setClickable(true);
|
||||
addButton.setAlpha(1f);
|
||||
};
|
||||
Account me=AccountSessionManager.get(item.accountID).self;
|
||||
EmojiReaction existing=null;
|
||||
for(int i=0; i<item.status.reactions.size(); i++){
|
||||
EmojiReaction r=item.status.reactions.get(i);
|
||||
if(r.name.equals(emoji)){
|
||||
existing=r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EmojiReaction finalExisting=existing;
|
||||
item.createRequest(emoji, existing==null ? 1 : existing.count, false, null, ()->{
|
||||
resetBtn.run();
|
||||
if(finalExisting==null){
|
||||
int pos=item.status.reactions.size();
|
||||
item.status.reactions.add(pos, info!=null ? EmojiReaction.of(info, me) : EmojiReaction.of(emoji, me));
|
||||
adapter.notifyItemRangeInserted(pos, 1);
|
||||
RecyclerView.SmoothScroller scroller=new LinearSmoothScroller(list.getContext());
|
||||
scroller.setTargetPosition(pos);
|
||||
list.getLayoutManager().startSmoothScroll(scroller);
|
||||
}else{
|
||||
finalExisting.add(me);
|
||||
adapter.notifyItemChanged(item.status.reactions.indexOf(finalExisting));
|
||||
}
|
||||
E.post(new EmojiReactionsUpdatedEvent(item.status.id, item.status.reactions, countBefore==0, adapter.parentHolder));
|
||||
}, resetBtn).exec(item.accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackspace() {}
|
||||
|
||||
private void onReactClick(View v){
|
||||
emojiKeyboard.toggleKeyboardPopup(null);
|
||||
v.setSelected(emojiKeyboard.isVisible());
|
||||
space.setVisibility(emojiKeyboard.isVisible() ? View.VISIBLE : View.GONE);
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
int[] locationOnScreen = new int[2];
|
||||
((Activity) v.getContext()).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||
v.getLocationOnScreen(locationOnScreen);
|
||||
double fromScreenTop = (double) locationOnScreen[1] / displayMetrics.heightPixels;
|
||||
if (fromScreenTop > 0.75) {
|
||||
item.parentFragment.scrollBy(0, (int) (displayMetrics.heightPixels * 0.3));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
View child=list.getChildAt(index);
|
||||
if(child==null) return;
|
||||
((EmojiReactionViewHolder) list.getChildViewHolder(child)).setImage(index, image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
if(item.status.reactions.get(index).getUrl(item.playGifs)==null) return;
|
||||
setImage(index, item.placeholder);
|
||||
}
|
||||
|
||||
private class EmojiReactionsAdapter extends UsableRecyclerView.Adapter<EmojiReactionViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
ListImageLoaderWrapper imgLoader;
|
||||
Holder parentHolder;
|
||||
|
||||
public EmojiReactionsAdapter(Holder parentHolder, ListImageLoaderWrapper imgLoader){
|
||||
super(imgLoader);
|
||||
this.parentHolder=parentHolder;
|
||||
this.imgLoader=imgLoader;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiReactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new EmojiReactionViewHolder(parent.getContext(), list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(EmojiReactionViewHolder holder, int position){
|
||||
holder.bind(Pair.create(item, item.status.reactions.get(position)));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return item.status.reactions.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return item.status.reactions.get(position).getUrl(item.playGifs)==null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
return item.status.reactions.get(position).request;
|
||||
}
|
||||
}
|
||||
|
||||
private static class EmojiReactionViewHolder extends BindableViewHolder<Pair<EmojiReactionsStatusDisplayItem, EmojiReaction>> implements ImageLoaderViewHolder{
|
||||
private final ProgressBarButton btn;
|
||||
private final ProgressBar progress;
|
||||
|
||||
public EmojiReactionViewHolder(Context context, RecyclerView list){
|
||||
super(context, R.layout.item_emoji_reaction, list);
|
||||
btn=findViewById(R.id.btn);
|
||||
progress=findViewById(R.id.progress);
|
||||
itemView.setClickable(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable drawable){
|
||||
drawable.setBounds(0, 0, V.sp(24), V.sp(24));
|
||||
btn.setCompoundDrawablesRelative(drawable, null, null, null);
|
||||
if(drawable instanceof Animatable) ((Animatable) drawable).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, item.first.placeholder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Pair<EmojiReactionsStatusDisplayItem, EmojiReaction> item){
|
||||
item.first.setActionProgressVisible(this, false);
|
||||
EmojiReactionsStatusDisplayItem parent=item.first;
|
||||
EmojiReaction reaction=item.second;
|
||||
btn.setText(UiUtils.abbreviateNumber(reaction.count));
|
||||
btn.setContentDescription(reaction.name);
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) btn.setTooltipText(reaction.name);
|
||||
if(reaction.getUrl(parent.playGifs)==null){
|
||||
Paint p=new Paint();
|
||||
p.setTextSize(V.sp(18));
|
||||
TextDrawable drawable=new TextDrawable(p, reaction.name);
|
||||
btn.setCompoundDrawablesRelative(drawable, null, null, null);
|
||||
}else{
|
||||
btn.setCompoundDrawablesRelative(item.first.placeholder, null, null, null);
|
||||
}
|
||||
btn.setSelected(reaction.me);
|
||||
btn.setOnClickListener(e->{
|
||||
boolean deleting=reaction.me;
|
||||
parent.createRequest(reaction.name, reaction.count, deleting, this, ()->{
|
||||
EmojiReactionsAdapter adapter = (EmojiReactionsAdapter) getBindingAdapter();
|
||||
for(int i=0; i<parent.status.reactions.size(); i++){
|
||||
EmojiReaction r=parent.status.reactions.get(i);
|
||||
if(!r.name.equals(reaction.name)) continue;
|
||||
if(deleting && r.count==1) {
|
||||
parent.status.reactions.remove(i);
|
||||
adapter.notifyItemRemoved(i);
|
||||
break;
|
||||
}
|
||||
r.me=!deleting;
|
||||
if(deleting) r.count--;
|
||||
else r.count++;
|
||||
adapter.notifyItemChanged(i);
|
||||
break;
|
||||
}
|
||||
|
||||
if(parent.isHidden()){
|
||||
adapter.parentHolder.root.setVisibility(View.GONE);
|
||||
adapter.parentHolder.line.setVisibility(View.GONE);
|
||||
}
|
||||
E.post(new EmojiReactionsUpdatedEvent(parent.status.id, parent.status.reactions, parent.status.reactions.isEmpty(), adapter.parentHolder));
|
||||
adapter.parentHolder.imgLoader.updateImages();
|
||||
}, null).exec(parent.parentFragment.getAccountID());
|
||||
});
|
||||
|
||||
if (parent.parentFragment.isInstanceAkkoma()) {
|
||||
// glitch-soc doesn't have this, afaik
|
||||
btn.setOnLongClickListener(e->{
|
||||
EmojiReaction emojiReaction=parent.status.reactions.get(getAbsoluteAdapterPosition());
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", parent.parentFragment.getAccountID());
|
||||
args.putString("statusID", parent.status.id);
|
||||
int atSymbolIndex = emojiReaction.name.indexOf("@");
|
||||
args.putString("emoji", atSymbolIndex != -1 ? emojiReaction.name.substring(0, atSymbolIndex) : emojiReaction.name);
|
||||
args.putString("url", emojiReaction.getUrl(parent.playGifs));
|
||||
args.putInt("count", emojiReaction.count);
|
||||
Nav.go(parent.parentFragment.getActivity(), StatusEmojiReactionsListFragment.class, args);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,16 @@ import android.text.style.StyleSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.StatusEditHistoryFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
@@ -24,6 +30,7 @@ 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.ui.Snackbar;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -40,15 +47,16 @@ import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
public final String accountID;
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
|
||||
private static final DateTimeFormatter TIME_FORMATTER_LONG=DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM);
|
||||
private static final DateTimeFormatter DATE_FORMATTER=DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
|
||||
|
||||
public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){
|
||||
public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, String accountID, Status status){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
this.accountID=accountID;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -58,13 +66,18 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<ExtendedFooterStatusDisplayItem>{
|
||||
private final TextView time, date, app, dateAppSeparator;
|
||||
private final TextView favorites, reblogs, editHistory;
|
||||
private final Button favorites, reblogs, editHistory, applicationName;
|
||||
private final ImageView visibility;
|
||||
private final Context context;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_extended_footer, parent);
|
||||
this.context = context;
|
||||
reblogs=findViewById(R.id.reblogs);
|
||||
favorites=findViewById(R.id.favorites);
|
||||
editHistory=findViewById(R.id.edit_history);
|
||||
applicationName=findViewById(R.id.application_name);
|
||||
visibility=findViewById(R.id.visibility);
|
||||
time=findViewById(R.id.time);
|
||||
date=findViewById(R.id.date);
|
||||
app=findViewById(R.id.app_name);
|
||||
@@ -81,31 +94,39 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
@Override
|
||||
public void onBind(ExtendedFooterStatusDisplayItem item){
|
||||
Status s=item.status;
|
||||
favorites.setText(getFormattedPlural(R.plurals.x_favorites, item.status.favouritesCount));
|
||||
reblogs.setText(getFormattedPlural(R.plurals.x_reblogs, item.status.reblogsCount));
|
||||
favorites.setCompoundDrawablesRelativeWithIntrinsicBounds(GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_20_regular : R.drawable.ic_fluent_star_20_regular, 0, 0, 0);
|
||||
favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount));
|
||||
reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int) (s.reblogsCount % 1000), s.reblogsCount));
|
||||
reblogs.setVisibility(s.visibility != StatusPrivacy.DIRECT ? View.VISIBLE : View.GONE);
|
||||
|
||||
if(s.editedAt!=null){
|
||||
editHistory.setVisibility(View.VISIBLE);
|
||||
ZonedDateTime dt=s.editedAt.atZone(ZoneId.systemDefault());
|
||||
String time=TIME_FORMATTER.format(dt);
|
||||
if(!dt.toLocalDate().equals(LocalDate.now())){
|
||||
time+=" · "+DATE_FORMATTER.format(dt);
|
||||
}
|
||||
editHistory.setText(getFormattedSubstitutedString(R.string.last_edit_at_x, time));
|
||||
editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt, false));
|
||||
}else{
|
||||
editHistory.setVisibility(View.GONE);
|
||||
}
|
||||
ZonedDateTime dt=item.status.createdAt.atZone(ZoneId.systemDefault());
|
||||
time.setText(TIME_FORMATTER.format(dt));
|
||||
date.setText(DATE_FORMATTER.format(dt));
|
||||
if(item.status.application!=null && !TextUtils.isEmpty(item.status.application.name)){
|
||||
app.setVisibility(View.VISIBLE);
|
||||
dateAppSeparator.setVisibility(View.VISIBLE);
|
||||
app.setText(item.status.application.name);
|
||||
app.setEnabled(!TextUtils.isEmpty(item.status.application.website));
|
||||
}else{
|
||||
app.setVisibility(View.GONE);
|
||||
dateAppSeparator.setVisibility(View.GONE);
|
||||
String timeStr=item.status.createdAt != null ? TIME_FORMATTER.format(item.status.createdAt.atZone(ZoneId.systemDefault())) : null;
|
||||
|
||||
if (item.status.application!=null && !TextUtils.isEmpty(item.status.application.name)) {
|
||||
time.setText(timeStr != null ? item.parentFragment.getString(R.string.timestamp_via_app, timeStr, "") : "");
|
||||
applicationName.setText(item.status.application.name);
|
||||
if (item.status.application.website != null && item.status.application.website.toLowerCase().startsWith("https://")) {
|
||||
applicationName.setOnClickListener(e -> UiUtils.openURL(context, null, item.status.application.website));
|
||||
} else {
|
||||
applicationName.setEnabled(false);
|
||||
}
|
||||
} else {
|
||||
time.setText(timeStr);
|
||||
applicationName.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
visibility.setImageResource(switch (s.visibility) {
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled;
|
||||
case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
|
||||
case LOCAL -> R.drawable.ic_fluent_eye_20_regular;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -151,6 +172,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void startAccountListFragment(Class<? extends StatusRelatedAccountListFragment> cls){
|
||||
if(item.status.preview) return;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("status", Parcels.wrap(item.status));
|
||||
@@ -158,9 +180,11 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void startEditHistoryFragment(){
|
||||
if(item.status.preview) return;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putString("id", item.status.id);
|
||||
args.putString("url", item.status.url);
|
||||
Nav.go(item.parentFragment.getActivity(), StatusEditHistoryFragment.class, args);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
public class FileStatusDisplayItem extends StatusDisplayItem{
|
||||
private final Attachment attachment;
|
||||
|
||||
public FileStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Attachment attachment) {
|
||||
super(parentID, parentFragment);
|
||||
this.attachment=attachment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.FILE;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<FileStatusDisplayItem> {
|
||||
private final TextView title, domain;
|
||||
|
||||
public Holder(Context context, ViewGroup parent) {
|
||||
super(context, R.layout.display_item_file, parent);
|
||||
title=findViewById(R.id.title);
|
||||
domain=findViewById(R.id.domain);
|
||||
findViewById(R.id.inner).setOnClickListener(this::onClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(FileStatusDisplayItem item) {
|
||||
Uri url = Uri.parse(getUrl());
|
||||
title.setText(item.attachment.description != null
|
||||
? item.attachment.description
|
||||
: url.getLastPathSegment());
|
||||
title.setEllipsize(item.attachment.description != null ? TextUtils.TruncateAt.END : TextUtils.TruncateAt.MIDDLE);
|
||||
domain.setText(url.getHost());
|
||||
}
|
||||
|
||||
private void onClick(View v) {
|
||||
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), getUrl());
|
||||
}
|
||||
|
||||
private String getUrl() {
|
||||
return item.attachment.remoteUrl == null ? item.attachment.url : item.attachment.remoteUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,48 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationSet;
|
||||
import android.view.animation.RotateAnimation;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
@@ -44,10 +62,21 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
|
||||
private final TextView reply, boost, favorite;
|
||||
private final ImageView share;
|
||||
private final ColorStateList buttonColors;
|
||||
private final View replyBtn, boostBtn, favoriteBtn, shareBtn;
|
||||
private final TextView replies, boosts, favorites;
|
||||
private final View reply, boost, favorite, share, bookmark;
|
||||
private final ImageView favIcon;
|
||||
private static Animation spin;
|
||||
|
||||
|
||||
private View touchingView = null;
|
||||
private boolean longClickPerformed = false;
|
||||
private final Runnable longClickRunnable = () -> {
|
||||
longClickPerformed = touchingView != null && touchingView.performLongClick();
|
||||
if (longClickPerformed && touchingView != null) {
|
||||
UiUtils.opacityIn(touchingView);
|
||||
touchingView.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
|
||||
}
|
||||
};
|
||||
|
||||
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
@@ -58,118 +87,380 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
};
|
||||
|
||||
static {
|
||||
spin = new RotateAnimation(0, 360,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
|
||||
0.5f);
|
||||
spin.setDuration(400);
|
||||
}
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_footer, parent);
|
||||
reply=findViewById(R.id.reply);
|
||||
boost=findViewById(R.id.boost);
|
||||
favorite=findViewById(R.id.favorite);
|
||||
share=findViewById(R.id.share);
|
||||
|
||||
float[] hsb={0, 0, 0};
|
||||
Color.colorToHSV(UiUtils.getThemeColor(activity, R.attr.colorM3Primary), hsb);
|
||||
hsb[1]+=0.1f;
|
||||
hsb[2]+=0.16f;
|
||||
replies=findViewById(R.id.reply);
|
||||
boosts=findViewById(R.id.boost);
|
||||
favorites=findViewById(R.id.favorite);
|
||||
|
||||
buttonColors=new ColorStateList(new int[][]{
|
||||
{android.R.attr.state_selected},
|
||||
{android.R.attr.state_enabled},
|
||||
{}
|
||||
}, new int[]{
|
||||
Color.HSVToColor(hsb),
|
||||
UiUtils.getThemeColor(activity, R.attr.colorM3OnSurfaceVariant),
|
||||
UiUtils.getThemeColor(activity, R.attr.colorM3OnSurfaceVariant) & 0x80FFFFFF
|
||||
});
|
||||
reply=findViewById(R.id.reply_btn);
|
||||
boost=findViewById(R.id.boost_btn);
|
||||
favorite=findViewById(R.id.favorite_btn);
|
||||
share=findViewById(R.id.share_btn);
|
||||
bookmark=findViewById(R.id.bookmark_btn);
|
||||
favIcon=findViewById(R.id.favorite_icon);
|
||||
|
||||
boost.setTextColor(buttonColors);
|
||||
boost.setCompoundDrawableTintList(buttonColors);
|
||||
favorite.setTextColor(buttonColors);
|
||||
favorite.setCompoundDrawableTintList(buttonColors);
|
||||
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(favorite);
|
||||
}
|
||||
replyBtn=findViewById(R.id.reply_btn);
|
||||
boostBtn=findViewById(R.id.boost_btn);
|
||||
favoriteBtn=findViewById(R.id.favorite_btn);
|
||||
shareBtn=findViewById(R.id.share_btn);
|
||||
replyBtn.setOnClickListener(this::onReplyClick);
|
||||
replyBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
boostBtn.setOnClickListener(this::onBoostClick);
|
||||
boostBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
favoriteBtn.setOnClickListener(this::onFavoriteClick);
|
||||
favoriteBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
shareBtn.setOnClickListener(this::onShareClick);
|
||||
shareBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
reply.setOnTouchListener(this::onButtonTouch);
|
||||
reply.setOnClickListener(this::onReplyClick);
|
||||
reply.setOnLongClickListener(this::onReplyLongClick);
|
||||
reply.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
boost.setOnTouchListener(this::onButtonTouch);
|
||||
boost.setOnClickListener(this::onBoostClick);
|
||||
boost.setOnLongClickListener(this::onBoostLongClick);
|
||||
boost.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
favorite.setOnTouchListener(this::onButtonTouch);
|
||||
favorite.setOnClickListener(this::onFavoriteClick);
|
||||
favorite.setOnLongClickListener(this::onFavoriteLongClick);
|
||||
favorite.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
bookmark.setOnTouchListener(this::onButtonTouch);
|
||||
bookmark.setOnClickListener(this::onBookmarkClick);
|
||||
bookmark.setOnLongClickListener(this::onBookmarkLongClick);
|
||||
bookmark.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
share.setOnTouchListener(this::onButtonTouch);
|
||||
share.setOnClickListener(this::onShareClick);
|
||||
share.setOnLongClickListener(this::onShareLongClick);
|
||||
share.setAccessibilityDelegate(buttonAccessibilityDelegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(FooterStatusDisplayItem item){
|
||||
bindButton(reply, item.status.repliesCount);
|
||||
bindButton(boost, item.status.reblogsCount);
|
||||
bindButton(favorite, item.status.favouritesCount);
|
||||
boostBtn.setSelected(item.status.reblogged);
|
||||
favoriteBtn.setSelected(item.status.favourited);
|
||||
boolean isOwn=item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id);
|
||||
boostBtn.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED
|
||||
|| (item.status.visibility==StatusPrivacy.PRIVATE && isOwn));
|
||||
Drawable d=itemView.getResources().getDrawable(switch(item.status.visibility){
|
||||
case PUBLIC, UNLISTED -> R.drawable.ic_boost;
|
||||
case PRIVATE -> isOwn ? R.drawable.ic_boost_private : R.drawable.ic_boost_disabled_24px;
|
||||
case DIRECT -> R.drawable.ic_boost_disabled_24px;
|
||||
}, itemView.getContext().getTheme());
|
||||
d.setBounds(0, 0, V.dp(20), V.dp(20));
|
||||
boost.setCompoundDrawablesRelative(d, null, null, null);
|
||||
bindText(replies, item.status.repliesCount);
|
||||
bindText(boosts, item.status.reblogsCount);
|
||||
bindText(favorites, item.status.favouritesCount);
|
||||
// in thread view, direct descendant posts display one direct reply to themselves,
|
||||
// hence in that case displaying whether there is another reply
|
||||
int compareTo = item.isMainStatus || !item.hasDescendantNeighbor ? 0 : 1;
|
||||
reply.setSelected(item.status.repliesCount > compareTo);
|
||||
boost.setSelected(item.status.reblogged);
|
||||
favorite.setSelected(item.status.favourited);
|
||||
bookmark.setSelected(item.status.bookmarked);
|
||||
boost.setEnabled(item.status.isReblogPermitted(item.accountID));
|
||||
|
||||
int nextPos = getAbsoluteAdapterPosition() + 1;
|
||||
boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos &&
|
||||
item.parentFragment.getDisplayItems().get(nextPos) instanceof WarningFilteredStatusDisplayItem;
|
||||
boolean condenseBottom = !item.isMainStatus && item.hasDescendantNeighbor &&
|
||||
!nextIsWarning;
|
||||
|
||||
ColorStateList color=item.parentFragment.getResources().getColorStateList(
|
||||
GlobalUserPreferences.likeIcon ? R.color.like_icon : R.color.favorite_icon, item.parentFragment.getContext().getTheme()
|
||||
);
|
||||
favIcon.setImageResource(GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_24_selector : R.drawable.ic_fluent_star_24_selector);
|
||||
favIcon.setImageTintList(color);
|
||||
favorites.setTextColor(color);
|
||||
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
|
||||
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
|
||||
condenseBottom ? V.dp(-5) : 0);
|
||||
|
||||
itemView.requestLayout();
|
||||
}
|
||||
|
||||
private void bindButton(TextView btn, long count){
|
||||
if(count>0 && !item.hideCounts){
|
||||
private void bindText(TextView btn, long count){
|
||||
if(AccountSessionManager.get(item.accountID).getLocalPreferences().showInteractionCounts
|
||||
&& count>0 && !item.hideCounts){
|
||||
btn.setText(UiUtils.abbreviateNumber(count));
|
||||
btn.setCompoundDrawablePadding(V.dp(6));
|
||||
btn.setCompoundDrawablePadding(V.dp(8));
|
||||
}else{
|
||||
btn.setText("");
|
||||
btn.setCompoundDrawablePadding(0);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean onButtonTouch(View v, MotionEvent event){
|
||||
if(item.status.preview) return false;
|
||||
boolean disabled = !v.isEnabled() || (v instanceof FrameLayout parentFrame &&
|
||||
parentFrame.getChildCount() > 0 && !parentFrame.getChildAt(0).isEnabled());
|
||||
int action = event.getAction();
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
||||
touchingView = null;
|
||||
v.removeCallbacks(longClickRunnable);
|
||||
if (!longClickPerformed) v.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
|
||||
if (disabled) return true;
|
||||
if (action == MotionEvent.ACTION_UP && !longClickPerformed) v.performClick();
|
||||
else if (!longClickPerformed) UiUtils.opacityIn(v);
|
||||
} else if (action == MotionEvent.ACTION_DOWN) {
|
||||
longClickPerformed = false;
|
||||
touchingView = v;
|
||||
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
|
||||
if (disabled) return true;
|
||||
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
|
||||
UiUtils.opacityOut(v);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onReplyClick(View v){
|
||||
item.parentFragment.maybeShowPreReplySheet(item.status, ()->{
|
||||
if(item.status.preview) return;
|
||||
if(item.status.isRemote){
|
||||
UiUtils.lookupStatus(v.getContext(),
|
||||
item.status, item.accountID, null,
|
||||
status -> {
|
||||
UiUtils.opacityIn(v);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.accountID);
|
||||
args.putParcelable("replyTo", Parcels.wrap(status));
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
UiUtils.opacityIn(v);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.accountID);
|
||||
args.putParcelable("replyTo", Parcels.wrap(item.status));
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
private boolean onReplyLongClick(View v) {
|
||||
if(item.status.preview) return false;
|
||||
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
|
||||
UiUtils.pickAccount(v.getContext(), item.accountID, R.string.sk_reply_as, R.drawable.ic_fluent_arrow_reply_28_regular, session -> {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.accountID);
|
||||
args.putParcelable("replyTo", Parcels.wrap(item.status));
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
});
|
||||
String accountID = session.getID();
|
||||
args.putString("account", accountID);
|
||||
UiUtils.lookupStatus(v.getContext(), item.status, accountID, item.accountID, status -> {
|
||||
if (status == null) return;
|
||||
args.putParcelable("replyTo", Parcels.wrap(status));
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
});
|
||||
}, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onBoostClick(View v){
|
||||
if(GlobalUserPreferences.confirmBoost){
|
||||
PopupMenu menu=new PopupMenu(itemView.getContext(), boost);
|
||||
menu.getMenu().add(R.string.button_reblog);
|
||||
menu.setOnMenuItemClickListener(item->{
|
||||
doBoost();
|
||||
return true;
|
||||
});
|
||||
menu.show();
|
||||
}else{
|
||||
doBoost();
|
||||
if(item.status.preview) return;
|
||||
if (GlobalUserPreferences.confirmBoost) {
|
||||
UiUtils.opacityIn(v);
|
||||
onBoostLongClick(v);
|
||||
return;
|
||||
}
|
||||
if(item.status.isRemote){
|
||||
UiUtils.lookupStatus(v.getContext(),
|
||||
item.status, item.accountID, null,
|
||||
status -> {
|
||||
if(status == null)
|
||||
return;
|
||||
boost.setSelected(!status.reblogged);
|
||||
vibrateForAction(boost, !status.reblogged);
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(status, !status.reblogged, null, r->boostConsumer(v, r));
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
boost.setSelected(!item.status.reblogged);
|
||||
vibrateForAction(boost, !item.status.reblogged);
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, null, r->boostConsumer(v, r));
|
||||
}
|
||||
|
||||
private void doBoost(){
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged);
|
||||
boost.setSelected(item.status.reblogged);
|
||||
bindButton(boost, item.status.reblogsCount);
|
||||
private void boostConsumer(View v, Status r) {
|
||||
UiUtils.opacityIn(v);
|
||||
bindText(boosts, r.reblogsCount);
|
||||
}
|
||||
|
||||
private boolean onBoostLongClick(View v){
|
||||
if(item.status.preview) return false;
|
||||
Context ctx = itemView.getContext();
|
||||
View menu = LayoutInflater.from(ctx).inflate(R.layout.item_boost_menu, null);
|
||||
Dialog dialog = new M3AlertDialogBuilder(ctx).setView(menu).create();
|
||||
AccountSession session = AccountSessionManager.getInstance().getAccount(item.accountID);
|
||||
|
||||
Consumer<StatusPrivacy> doReblog = (visibility) -> {
|
||||
UiUtils.opacityOut(v);
|
||||
if(item.status.isRemote){
|
||||
UiUtils.lookupStatus(v.getContext(),
|
||||
item.status, item.accountID, null,
|
||||
status -> {
|
||||
session.getStatusInteractionController()
|
||||
.setReblogged(status, !status.reblogged, visibility, r->boostConsumer(v, r));
|
||||
boost.setSelected(status.reblogged);
|
||||
dialog.dismiss();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
session.getStatusInteractionController()
|
||||
.setReblogged(item.status, !item.status.reblogged, visibility, r->boostConsumer(v, r));
|
||||
boost.setSelected(item.status.reblogged);
|
||||
dialog.dismiss();
|
||||
}
|
||||
};
|
||||
|
||||
View separator = menu.findViewById(R.id.separator);
|
||||
TextView reblogHeader = menu.findViewById(R.id.reblog_header);
|
||||
TextView undoReblog = menu.findViewById(R.id.delete_reblog);
|
||||
TextView reblogAs = menu.findViewById(R.id.reblog_as);
|
||||
TextView itemPublic = menu.findViewById(R.id.vis_public);
|
||||
TextView itemUnlisted = menu.findViewById(R.id.vis_unlisted);
|
||||
TextView itemFollowers = menu.findViewById(R.id.vis_followers);
|
||||
|
||||
undoReblog.setVisibility(item.status.reblogged ? View.VISIBLE : View.GONE);
|
||||
separator.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
|
||||
reblogHeader.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
|
||||
reblogAs.setVisibility(AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1 ? View.VISIBLE : View.GONE);
|
||||
|
||||
itemPublic.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
|
||||
itemUnlisted.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
|
||||
itemFollowers.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
|
||||
|
||||
Drawable checkMark = ctx.getDrawable(R.drawable.ic_fluent_checkmark_circle_20_regular);
|
||||
Drawable publicDrawable = ctx.getDrawable(R.drawable.ic_fluent_earth_24_regular);
|
||||
Drawable unlistedDrawable = ctx.getDrawable(R.drawable.ic_fluent_lock_open_24_regular);
|
||||
Drawable followersDrawable = ctx.getDrawable(R.drawable.ic_fluent_lock_closed_24_regular);
|
||||
|
||||
StatusPrivacy defaultVisibility = session.preferences != null ? session.preferences.postingDefaultVisibility : null;
|
||||
itemPublic.setCompoundDrawablesWithIntrinsicBounds(publicDrawable, null, StatusPrivacy.PUBLIC.equals(defaultVisibility) ? checkMark : null, null);
|
||||
itemUnlisted.setCompoundDrawablesWithIntrinsicBounds(unlistedDrawable, null, StatusPrivacy.UNLISTED.equals(defaultVisibility) ? checkMark : null, null);
|
||||
itemFollowers.setCompoundDrawablesWithIntrinsicBounds(followersDrawable, null, StatusPrivacy.PRIVATE.equals(defaultVisibility) ? checkMark : null, null);
|
||||
|
||||
undoReblog.setOnClickListener(c->doReblog.accept(null));
|
||||
itemPublic.setOnClickListener(c->doReblog.accept(StatusPrivacy.PUBLIC));
|
||||
itemUnlisted.setOnClickListener(c->doReblog.accept(StatusPrivacy.UNLISTED));
|
||||
itemFollowers.setOnClickListener(c->doReblog.accept(StatusPrivacy.PRIVATE));
|
||||
reblogAs.setOnClickListener(c->{
|
||||
dialog.dismiss();
|
||||
UiUtils.pickInteractAs(v.getContext(),
|
||||
item.accountID, item.status,
|
||||
s -> s.reblogged,
|
||||
(ic, status, consumer) -> ic.setReblogged(status, true, null, consumer),
|
||||
R.string.sk_reblog_as,
|
||||
R.string.sk_reblogged_as,
|
||||
R.string.sk_already_reblogged,
|
||||
// TODO: replace once available: https://raw.githubusercontent.com/microsoft/fluentui-system-icons/main/android/library/src/main/res/drawable/ic_fluent_arrow_repeat_all_28_regular.xml
|
||||
R.drawable.ic_fluent_arrow_repeat_all_24_regular
|
||||
);
|
||||
});
|
||||
|
||||
menu.findViewById(R.id.quote).setOnClickListener(c->{
|
||||
dialog.dismiss();
|
||||
UiUtils.opacityIn(v);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.accountID);
|
||||
AccountSession accountSession=AccountSessionManager.getInstance().getAccount(item.accountID);
|
||||
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain);
|
||||
if(instance.pleroma == null){
|
||||
StringBuilder prefilledText = new StringBuilder().append("\n\n");
|
||||
String ownID = AccountSessionManager.getInstance().getAccount(item.accountID).self.id;
|
||||
if (!item.status.account.id.equals(ownID)) prefilledText.append('@').append(item.status.account.acct).append(' ');
|
||||
prefilledText.append(item.status.url);
|
||||
args.putString("prefilledText", prefilledText.toString());
|
||||
args.putInt("selectionStart", 0);
|
||||
}else{
|
||||
args.putParcelable("quote", Parcels.wrap(item.status));
|
||||
}
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onFavoriteClick(View v){
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited);
|
||||
favorite.setSelected(item.status.favourited);
|
||||
bindButton(favorite, item.status.favouritesCount);
|
||||
if(item.status.preview) return;
|
||||
if(item.status.isRemote){
|
||||
UiUtils.lookupStatus(v.getContext(),
|
||||
item.status, item.accountID, null,
|
||||
status -> {
|
||||
if(status == null)
|
||||
return;
|
||||
favorite.setSelected(!status.favourited);
|
||||
vibrateForAction(favorite, !status.favourited);
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(status, !status.favourited, r->{
|
||||
if (status.favourited && !GlobalUserPreferences.reduceMotion && !GlobalUserPreferences.likeIcon) {
|
||||
v.startAnimation(spin);
|
||||
}
|
||||
UiUtils.opacityIn(v);
|
||||
bindText(favorites, r.favouritesCount);
|
||||
});
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
favorite.setSelected(!item.status.favourited);
|
||||
vibrateForAction(favorite, !item.status.favourited);
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
|
||||
if (item.status.favourited && !GlobalUserPreferences.reduceMotion && !GlobalUserPreferences.likeIcon) {
|
||||
v.startAnimation(spin);
|
||||
}
|
||||
UiUtils.opacityIn(v);
|
||||
bindText(favorites, r.favouritesCount);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean onFavoriteLongClick(View v) {
|
||||
if(item.status.preview) return false;
|
||||
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
|
||||
UiUtils.pickInteractAs(v.getContext(),
|
||||
item.accountID, item.status,
|
||||
s -> s.favourited,
|
||||
(ic, status, consumer) -> ic.setFavorited(status, true, consumer),
|
||||
R.string.sk_favorite_as,
|
||||
R.string.sk_favorited_as,
|
||||
R.string.sk_already_favorited,
|
||||
GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_28_regular : R.drawable.ic_fluent_star_28_regular
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onBookmarkClick(View v){
|
||||
if(item.status.preview) return;
|
||||
if(item.status.isRemote){
|
||||
UiUtils.lookupStatus(v.getContext(),
|
||||
item.status, item.accountID, null,
|
||||
status -> {
|
||||
if(status == null)
|
||||
return;
|
||||
bookmark.setSelected(!status.bookmarked);
|
||||
vibrateForAction(bookmark, !status.bookmarked);
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(status, !status.bookmarked, r->{
|
||||
UiUtils.opacityIn(v);
|
||||
});
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
bookmark.setSelected(!item.status.bookmarked);
|
||||
vibrateForAction(bookmark, !item.status.bookmarked);
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{
|
||||
UiUtils.opacityIn(v);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean onBookmarkLongClick(View v) {
|
||||
if(item.status.preview) return false;
|
||||
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
|
||||
UiUtils.pickInteractAs(v.getContext(),
|
||||
item.accountID, item.status,
|
||||
s -> s.bookmarked,
|
||||
(ic, status, consumer) -> ic.setBookmarked(status, true, consumer),
|
||||
R.string.sk_bookmark_as,
|
||||
R.string.sk_bookmarked_as,
|
||||
R.string.sk_already_bookmarked,
|
||||
R.drawable.ic_fluent_bookmark_28_regular
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onShareClick(View v){
|
||||
UiUtils.openSystemShareSheet(v.getContext(), item.status.url);
|
||||
if(item.status.preview) return;
|
||||
UiUtils.opacityIn(v);
|
||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, item.status.url);
|
||||
v.getContext().startActivity(Intent.createChooser(intent, v.getContext().getString(R.string.share_toot_title)));
|
||||
}
|
||||
|
||||
private boolean onShareLongClick(View v){
|
||||
if(item.status.preview) return false;
|
||||
UiUtils.copyText(v, item.status.url);
|
||||
return true;
|
||||
}
|
||||
|
||||
private int descriptionForId(int id){
|
||||
@@ -179,9 +470,33 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
return R.string.button_reblog;
|
||||
if(id==R.id.favorite_btn)
|
||||
return R.string.button_favorite;
|
||||
if(id==R.id.bookmark_btn)
|
||||
return R.string.add_bookmark;
|
||||
if(id==R.id.share_btn)
|
||||
return R.string.button_share;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void vibrateForAction(View view, boolean isPositive) {
|
||||
if (!GlobalUserPreferences.hapticFeedback) return;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
view.performHapticFeedback(isPositive ? HapticFeedbackConstants.CONFIRM : HapticFeedbackConstants.REJECT);
|
||||
} else {
|
||||
Vibrator vibrator = view.getContext().getSystemService(Vibrator.class);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
vibrator.vibrate(VibrationEffect.createPredefined(isPositive ? VibrationEffect.EFFECT_CLICK : VibrationEffect.EFFECT_DOUBLE_CLICK));
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
VibrationEffect effect = isPositive
|
||||
? VibrationEffect.createOneShot(75L, 128)
|
||||
: VibrationEffect.createWaveform(new long[]{0L, 75L, 75L, 75L}, new int[]{0, 128, 0, 128}, -1);
|
||||
vibrator.vibrate(effect);
|
||||
} else {
|
||||
if (isPositive) vibrator.vibrate(75L);
|
||||
else vibrator.vibrate(new long[]{0L, 75L, 75L, 75L}, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,25 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.drawables.SawtoothTearDrawable;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
// Mind the gap!
|
||||
public class GapStatusDisplayItem extends StatusDisplayItem{
|
||||
public boolean loading;
|
||||
|
||||
public GapStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
|
||||
public GapStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
}
|
||||
|
||||
public String getMaxID(){
|
||||
return status.hasGapAfter;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -24,25 +35,59 @@ public class GapStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<GapStatusDisplayItem>{
|
||||
public final ProgressBar progress;
|
||||
public final TextView text;
|
||||
public final ProgressBar progressTop, progressBottom;
|
||||
public final TextView textTop, gap, textBottom;
|
||||
public final View top, bottom;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_gap, parent);
|
||||
progress=findViewById(R.id.progress);
|
||||
text=findViewById(R.id.text);
|
||||
itemView.setForeground(new SawtoothTearDrawable(context));
|
||||
progressTop=findViewById(R.id.progress_top);
|
||||
progressBottom=findViewById(R.id.progress_bottom);
|
||||
textTop=findViewById(R.id.text_top);
|
||||
textBottom=findViewById(R.id.text_bottom);
|
||||
top=findViewById(R.id.top);
|
||||
top.setOnClickListener(this::onViewClick);
|
||||
bottom=findViewById(R.id.bottom);
|
||||
bottom.setOnClickListener(this::onViewClick);
|
||||
gap=findViewById(R.id.gap);
|
||||
gap.setForeground(new SawtoothTearDrawable(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(GapStatusDisplayItem item){
|
||||
text.setVisibility(item.loading ? View.GONE : View.VISIBLE);
|
||||
progress.setVisibility(item.loading ? View.VISIBLE : View.GONE);
|
||||
if(!item.loading){
|
||||
progressBottom.setVisibility(View.GONE);
|
||||
progressTop.setVisibility(View.GONE);
|
||||
textTop.setAlpha(1);
|
||||
textBottom.setAlpha(1);
|
||||
}
|
||||
top.setClickable(!item.loading);
|
||||
bottom.setClickable(!item.loading);
|
||||
Status next=!(item.parentFragment instanceof StatusListFragment) ? null : getNextVisibleDisplayItem(i->{
|
||||
Status s=((StatusListFragment) item.parentFragment).getStatusByID(i.parentID);
|
||||
return s!=null && !s.fromStatusCreated;
|
||||
})
|
||||
.map(i->((StatusListFragment) item.parentFragment).getStatusByID(i.parentID))
|
||||
.orElse(null);
|
||||
bottom.setVisibility(next==null ? View.GONE : View.VISIBLE);
|
||||
Instant dateBelow=next!=null ? next.createdAt : null;
|
||||
String text=dateBelow!=null && item.status.createdAt!=null && dateBelow.isBefore(item.status.createdAt)
|
||||
? UiUtils.formatPeriodBetween(item.parentFragment.getContext(), dateBelow, item.status.createdAt)
|
||||
: null;
|
||||
gap.setText(text);
|
||||
int p=text==null ? V.dp(6) : V.dp(20);
|
||||
gap.setPadding(p, p, p, p);
|
||||
}
|
||||
|
||||
private void onViewClick(View v){
|
||||
if(item.loading) return;
|
||||
boolean isTop=v==top;
|
||||
UiUtils.opacityOut(isTop ? textTop : textBottom);
|
||||
V.setVisibilityAnimated((isTop ? progressTop : progressBottom), View.VISIBLE);
|
||||
item.parentFragment.onGapClick(this, isTop);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.parentFragment.onGapClick(this);
|
||||
}
|
||||
public void onClick(){}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,17 @@ package org.joinmastodon.android.ui.displayitems;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
@@ -24,17 +24,26 @@ import android.widget.Toast;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.announcements.DismissAnnouncement;
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.AddAccountToListsFragment;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.ListsFragment;
|
||||
import org.joinmastodon.android.fragments.NotificationsListFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Announcement;
|
||||
import org.joinmastodon.android.model.Mention;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
@@ -42,9 +51,14 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import me.grishka.appkit.Nav;
|
||||
@@ -54,6 +68,7 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
@@ -63,34 +78,45 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
private String accountID;
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
private SpannableStringBuilder parsedName;
|
||||
public final Status status;
|
||||
private boolean hasVisibilityToggle;
|
||||
public boolean hasVisibilityToggle;
|
||||
boolean needBottomPadding;
|
||||
private String extraText;
|
||||
private CharSequence extraText;
|
||||
private Notification notification;
|
||||
private ScheduledStatus scheduledStatus;
|
||||
private Announcement announcement;
|
||||
private Consumer<String> consumeReadAnnouncement;
|
||||
|
||||
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText){
|
||||
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, CharSequence extraText, Notification notification, ScheduledStatus scheduledStatus){
|
||||
super(parentID, parentFragment);
|
||||
AccountSession session = AccountSessionManager.get(accountID);
|
||||
user=scheduledStatus != null ? session.self : user;
|
||||
this.user=user;
|
||||
this.createdAt=createdAt;
|
||||
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? user.avatar : user.avatarStatic, V.dp(50), V.dp(50));
|
||||
avaRequest=new UrlImageLoaderRequest(
|
||||
TextUtils.isEmpty(user.avatar) ? session.getDefaultAvatarUrl() :
|
||||
GlobalUserPreferences.playGifs ? user.avatar : user.avatarStatic,
|
||||
V.dp(50), V.dp(50));
|
||||
this.accountID=accountID;
|
||||
parsedName=new SpannableStringBuilder(user.displayName);
|
||||
parsedName=new SpannableStringBuilder(user.getDisplayName());
|
||||
this.status=status;
|
||||
this.notification=notification;
|
||||
this.scheduledStatus=scheduledStatus;
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
|
||||
emojiHelper.setText(parsedName);
|
||||
if(status!=null){
|
||||
hasVisibilityToggle=status.sensitive || !TextUtils.isEmpty(status.spoilerText);
|
||||
if(!hasVisibilityToggle && !status.mediaAttachments.isEmpty()){
|
||||
for(Attachment att:status.mediaAttachments){
|
||||
if(att.type!=Attachment.Type.AUDIO){
|
||||
hasVisibilityToggle=true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// visibility toggle can't do much for non-"image" attachments
|
||||
hasVisibilityToggle=status.mediaAttachments.stream().anyMatch(m -> m.type.isImage());
|
||||
}
|
||||
this.extraText=extraText;
|
||||
emojiHelper.addText(extraText);
|
||||
}
|
||||
|
||||
public static HeaderStatusDisplayItem fromAnnouncement(Announcement a, Status fakeStatus, Account instanceUser, BaseStatusListFragment parentFragment, String accountID, Consumer<String> consumeReadID) {
|
||||
HeaderStatusDisplayItem item = new HeaderStatusDisplayItem(a.id, instanceUser, a.startsAt, parentFragment, accountID, fakeStatus, null, null, null);
|
||||
item.announcement = a;
|
||||
item.consumeReadAnnouncement = consumeReadID;
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -112,9 +138,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView name, timeAndUsername, extraText;
|
||||
private final ImageView avatar, more;
|
||||
private final TextView name, time, username, extraText;
|
||||
private final View collapseBtn, timeUsernameSeparator;
|
||||
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator, markAsRead, collapseBtnIcon, botIcon;
|
||||
private final PopupMenu optionsMenu;
|
||||
private Relationship relationship;
|
||||
private APIRequest<?> currentRelationshipRequest;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
this(activity, R.layout.display_item_header, parent);
|
||||
@@ -123,14 +152,30 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
protected Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){
|
||||
super(activity, layout, parent);
|
||||
name=findViewById(R.id.name);
|
||||
timeAndUsername=findViewById(R.id.time_and_username);
|
||||
time=findViewById(R.id.time);
|
||||
username=findViewById(R.id.username);
|
||||
botIcon=findViewById(R.id.bot_icon);
|
||||
timeUsernameSeparator=findViewById(R.id.separator);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
more=findViewById(R.id.more);
|
||||
visibility=findViewById(R.id.visibility);
|
||||
deleteNotification=findViewById(R.id.delete_notification);
|
||||
unreadIndicator=findViewById(R.id.unread_indicator);
|
||||
markAsRead=findViewById(R.id.mark_as_read);
|
||||
collapseBtn=findViewById(R.id.collapse_btn);
|
||||
collapseBtnIcon=findViewById(R.id.collapse_btn_icon);
|
||||
extraText=findViewById(R.id.extra_text);
|
||||
avatar.setOnClickListener(this::onAvaClick);
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
avatar.setClipToOutline(true);
|
||||
more.setOnClickListener(this::onMoreClick);
|
||||
visibility.setOnClickListener(v->item.parentFragment.onVisibilityIconClick(this));
|
||||
deleteNotification.setOnClickListener(v->UiUtils.confirmDeleteNotification(activity, item.parentFragment.getAccountID(), item.notification, ()->{
|
||||
if (item.parentFragment instanceof NotificationsListFragment fragment) {
|
||||
fragment.removeNotification(item.notification);
|
||||
}
|
||||
}));
|
||||
collapseBtn.setOnClickListener(l -> item.parentFragment.onToggleExpanded(item.status, getItemID()));
|
||||
|
||||
optionsMenu=new PopupMenu(activity, more);
|
||||
optionsMenu.inflate(R.menu.post);
|
||||
@@ -138,13 +183,33 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
optionsMenu.getMenu().setGroupDividerEnabled(true);
|
||||
optionsMenu.setOnMenuItemClickListener(menuItem->{
|
||||
Account account=item.user;
|
||||
Relationship relationship=item.parentFragment.getRelationship(account.id);
|
||||
int id=menuItem.getItemId();
|
||||
if(id==R.id.edit){
|
||||
if(id==R.id.edit || id==R.id.delete_and_redraft){
|
||||
final Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("editStatus", Parcels.wrap(item.status));
|
||||
if(TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
|
||||
boolean redraft = id == R.id.delete_and_redraft;
|
||||
if (redraft) {
|
||||
args.putBoolean("redraftStatus", true);
|
||||
if (item.parentFragment instanceof ThreadFragment thread && !thread.isItemEnabled(item.status.id)) {
|
||||
// ("enabled" = clickable; opened status is not clickable)
|
||||
// request navigation to the re-drafted status if status is currently opened
|
||||
args.putBoolean("navigateToStatus", true);
|
||||
}
|
||||
}
|
||||
boolean isPixelfed = item.parentFragment.isInstancePixelfed();
|
||||
boolean textEmpty = TextUtils.isEmpty(item.status.content) && !item.status.hasSpoiler();
|
||||
if(!redraft && (isPixelfed || textEmpty)){
|
||||
// pixelfed doesn't support /statuses/:id/source :/
|
||||
if (isPixelfed) {
|
||||
args.putString("sourceText", HtmlParser.text(item.status.content));
|
||||
args.putString("sourceSpoiler", item.status.spoilerText);
|
||||
}
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}else if(item.scheduledStatus!=null){
|
||||
args.putString("sourceText", item.status.text);
|
||||
args.putString("sourceSpoiler", item.status.spoilerText);
|
||||
args.putParcelable("scheduledStatus", Parcels.wrap(item.scheduledStatus));
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}else{
|
||||
new GetStatusSourceText(item.status.id)
|
||||
@@ -153,7 +218,16 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
public void onSuccess(GetStatusSourceText.Response result){
|
||||
args.putString("sourceText", result.text);
|
||||
args.putString("sourceSpoiler", result.spoilerText);
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
if(result.contentType!=null){
|
||||
args.putString("sourceContentType", result.contentType.name());
|
||||
}
|
||||
if(redraft){
|
||||
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}, true);
|
||||
}else{
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -165,9 +239,17 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
.exec(item.parentFragment.getAccountID());
|
||||
}
|
||||
}else if(id==R.id.delete){
|
||||
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
|
||||
if (item.scheduledStatus != null) {
|
||||
UiUtils.confirmDeleteScheduledPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.scheduledStatus, ()->{});
|
||||
} else {
|
||||
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{}, false);
|
||||
}
|
||||
}else if(id==R.id.pin || id==R.id.unpin) {
|
||||
UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{});
|
||||
}else if(id==R.id.mute){
|
||||
UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{});
|
||||
}else if (id==R.id.mute_conversation || id==R.id.unmute_conversation) {
|
||||
UiUtils.confirmToggleMuteConversation(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, ()->{});
|
||||
}else if(id==R.id.block){
|
||||
UiUtils.confirmToggleBlockUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.blocking, r->{});
|
||||
}else if(id==R.id.report){
|
||||
@@ -179,6 +261,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
Nav.go(item.parentFragment.getActivity(), ReportReasonChoiceFragment.class, args);
|
||||
}else if(id==R.id.open_in_browser){
|
||||
UiUtils.launchWebBrowser(activity, item.status.url);
|
||||
}else if(id==R.id.copy_link){
|
||||
UiUtils.copyText(parent, item.status.url);
|
||||
}else if(id==R.id.follow){
|
||||
if(relationship==null)
|
||||
return true;
|
||||
@@ -191,49 +275,143 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
else
|
||||
progress.dismiss();
|
||||
}, rel->{
|
||||
item.parentFragment.putRelationship(account.id, rel);
|
||||
relationship=rel;
|
||||
Toast.makeText(activity, activity.getString(rel.following ? R.string.followed_user : rel.requested ? R.string.following_user_requested : R.string.unfollowed_user, account.getDisplayUsername()), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}else if(id==R.id.block_domain){
|
||||
UiUtils.confirmToggleBlockDomain(activity, item.parentFragment.getAccountID(), account.getDomain(), relationship!=null && relationship.domainBlocking, ()->{});
|
||||
}else if(id==R.id.bookmark){
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked);
|
||||
}else if(id==R.id.manage_user_lists){
|
||||
final Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putString("profileAccount", account.id);
|
||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||
Nav.go(item.parentFragment.getActivity(), ListsFragment.class, args);
|
||||
}else if(id==R.id.share){
|
||||
UiUtils.openSystemShareSheet(activity, item.status.url);
|
||||
}else if(id==R.id.translate){
|
||||
item.parentFragment.togglePostTranslation(item.status, item.parentID);
|
||||
}else if(id==R.id.add_to_list){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(activity, AddAccountToListsFragment.class, args);
|
||||
}else if(id==R.id.copy_link){
|
||||
activity.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, item.status.url));
|
||||
UiUtils.maybeShowTextCopiedToast(activity);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
UiUtils.enablePopupMenuIcons(activity, optionsMenu);
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void onBind(HeaderStatusDisplayItem item){
|
||||
name.setText(item.parsedName);
|
||||
String time;
|
||||
if(item.status==null || item.status.editedAt==null)
|
||||
String time = null;
|
||||
if (item.scheduledStatus!=null) {
|
||||
if (item.scheduledStatus.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT)) {
|
||||
time = item.parentFragment.getString(R.string.sk_draft);
|
||||
} else {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
|
||||
time = item.scheduledStatus.scheduledAt.atZone(ZoneId.systemDefault()).format(formatter);
|
||||
}
|
||||
} else if(item.status==null || item.status.editedAt==null)
|
||||
time=UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt);
|
||||
else
|
||||
else if (item.status != null && item.status.editedAt != null)
|
||||
time=item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt));
|
||||
|
||||
timeAndUsername.setText(time+" · @"+item.user.acct);
|
||||
this.username.setText(item.user.getDisplayUsername());
|
||||
this.timeUsernameSeparator.setVisibility(time==null ? View.GONE : View.VISIBLE);
|
||||
this.time.setVisibility(time==null ? View.GONE : View.VISIBLE);
|
||||
if(time!=null) this.time.setText(time);
|
||||
|
||||
botIcon.setVisibility(item.user.bot ? View.VISIBLE : View.GONE);
|
||||
botIcon.setColorFilter(username.getCurrentTextColor());
|
||||
|
||||
deleteNotification.setVisibility(GlobalUserPreferences.enableDeleteNotifications && item.notification!=null && !item.inset ? View.VISIBLE : View.GONE);
|
||||
visibility.setVisibility(item.hasVisibilityToggle ? View.VISIBLE : View.GONE);
|
||||
if (item.hasVisibilityToggle){
|
||||
boolean visible = item.status.sensitiveRevealed && (!item.status.hasSpoiler() || item.status.spoilerRevealed);
|
||||
visibility.setAlpha(visible ? 1 : 0f);
|
||||
visibility.setScaleY(visible ? 1 : 0.8f);
|
||||
visibility.setScaleX(visible ? 1 : 0.8f);
|
||||
visibility.setEnabled(visible);
|
||||
}
|
||||
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
|
||||
if(TextUtils.isEmpty(item.extraText)){
|
||||
extraText.setVisibility(View.GONE);
|
||||
if (item.status != null) {
|
||||
boolean displayPronouns=item.parentFragment instanceof ThreadFragment ? GlobalUserPreferences.displayPronounsInThreads : GlobalUserPreferences.displayPronounsInTimelines;
|
||||
UiUtils.setExtraTextInfo(item.parentFragment.getContext(), extraText, displayPronouns, item.status.visibility==StatusPrivacy.DIRECT, item.status.localOnly || item.status.visibility==StatusPrivacy.LOCAL, item.status.account);
|
||||
}
|
||||
}else{
|
||||
extraText.setVisibility(View.VISIBLE);
|
||||
extraText.setText(item.extraText);
|
||||
}
|
||||
more.setVisibility(item.inset ? View.GONE : View.VISIBLE);
|
||||
more.setVisibility(item.announcement != null || item.inset ||
|
||||
(item.notification != null && item.notification.report != null)
|
||||
? View.GONE : View.VISIBLE);
|
||||
more.setOnClickListener(this::onMoreClick);
|
||||
avatar.setClickable(!item.inset);
|
||||
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
|
||||
if(currentRelationshipRequest!=null){
|
||||
currentRelationshipRequest.cancel();
|
||||
}
|
||||
relationship=null;
|
||||
|
||||
if (item.announcement != null) {
|
||||
int vis = item.announcement.read ? View.GONE : View.VISIBLE;
|
||||
V.setVisibilityAnimated(unreadIndicator, vis);
|
||||
V.setVisibilityAnimated(markAsRead, vis);
|
||||
|
||||
markAsRead.setEnabled(!item.announcement.read);
|
||||
markAsRead.setOnClickListener(v -> {
|
||||
if (item.announcement.read) return;
|
||||
new DismissAnnouncement(item.announcement.id).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {
|
||||
item.consumeReadAnnouncement.accept(item.announcement.id);
|
||||
item.announcement.read = true;
|
||||
if (item.parentFragment.getActivity() == null) return;
|
||||
rebind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(item.parentFragment.getActivity());
|
||||
}
|
||||
}).exec(item.accountID);
|
||||
});
|
||||
} else {
|
||||
markAsRead.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
bindCollapseButton();
|
||||
|
||||
itemView.setPaddingRelative(itemView.getPaddingStart(), itemView.getPaddingTop(),
|
||||
item.inset ? V.dp(10) : V.dp(4), itemView.getPaddingBottom());
|
||||
}
|
||||
|
||||
public void bindCollapseButton(){
|
||||
boolean expandable=item.status!=null && item.status.textExpandable;
|
||||
collapseBtn.setVisibility(expandable ? View.VISIBLE : View.GONE);
|
||||
if(expandable) {
|
||||
bindCollapseButtonText();
|
||||
collapseBtnIcon.setScaleY(item.status.textExpanded ? -1 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindCollapseButtonText(){
|
||||
String collapseText = item.parentFragment.getString(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
|
||||
collapseBtn.setContentDescription(collapseText);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) collapseBtn.setTooltipText(collapseText);
|
||||
}
|
||||
|
||||
public void animateExpandToggle(){
|
||||
bindCollapseButtonText();
|
||||
collapseBtnIcon.animate().scaleY(item.status.textExpanded ? -1 : 1).start();
|
||||
}
|
||||
|
||||
public void animateVisibilityToggle(boolean visible){
|
||||
visibility.animate()
|
||||
.alpha(visible ? 1 : 0)
|
||||
.scaleX(visible ? 1 : 0.8f)
|
||||
.scaleY(visible ? 1 : 0.8f)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
.start();
|
||||
visibility.setEnabled(visible);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -258,63 +436,180 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onAvaClick(View v){
|
||||
if (TextUtils.isEmpty(item.user.url))
|
||||
return;
|
||||
if (item.announcement != null) {
|
||||
UiUtils.openURL(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.user.url);
|
||||
return;
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
if(item.status != null && item.status.isRemote){
|
||||
UiUtils.lookupAccount(v.getContext(), item.status.account, item.accountID, null, account -> {
|
||||
args.putString("account", item.accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(account));
|
||||
Nav.go(item.parentFragment.getActivity(), ProfileFragment.class, args);
|
||||
});
|
||||
return;
|
||||
}
|
||||
args.putString("account", item.accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.user));
|
||||
Nav.go(item.parentFragment.getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
private void onMoreClick(View v){
|
||||
if(item.status.preview) return;
|
||||
updateOptionsMenu();
|
||||
optionsMenu.show();
|
||||
if(relationship==null && currentRelationshipRequest==null){
|
||||
currentRelationshipRequest=new GetAccountRelationships(Collections.singletonList(item.user.id))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Relationship> result){
|
||||
if(!result.isEmpty()){
|
||||
relationship=result.get(0);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
currentRelationshipRequest=null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
currentRelationshipRequest=null;
|
||||
}
|
||||
})
|
||||
.exec(item.parentFragment.getAccountID());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateOptionsMenu(){
|
||||
if(item.parentFragment.getActivity()==null)
|
||||
return;
|
||||
if (item.announcement != null) return;
|
||||
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
|
||||
Account account=item.user;
|
||||
Relationship relationship=item.parentFragment.getRelationship(account.id);
|
||||
Menu menu=optionsMenu.getMenu();
|
||||
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
|
||||
boolean canTranslate=item.status!=null && item.status.getContentStatus().isEligibleForTranslation();
|
||||
MenuItem translate=menu.findItem(R.id.translate);
|
||||
translate.setVisible(canTranslate);
|
||||
if(canTranslate){
|
||||
if(item.status.translationState==Status.TranslationState.SHOWN)
|
||||
translate.setTitle(R.string.translation_show_original);
|
||||
else
|
||||
translate.setTitle(item.parentFragment.getString(R.string.translate_post, Locale.forLanguageTag(item.status.getContentStatus().language).getDisplayLanguage()));
|
||||
|
||||
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
|
||||
SubMenu accountsMenu = openWithAccounts != null ? openWithAccounts.getSubMenu() : null;
|
||||
if (hasMultipleAccounts && accountsMenu != null) {
|
||||
openWithAccounts.setVisible(true);
|
||||
accountsMenu.clear();
|
||||
UiUtils.populateAccountsMenu(item.accountID, accountsMenu, s-> UiUtils.openURL(
|
||||
item.parentFragment.getActivity(), s.getID(), item.status.url, false
|
||||
));
|
||||
} else if (openWithAccounts != null) {
|
||||
openWithAccounts.setVisible(false);
|
||||
}
|
||||
|
||||
String username = account.getShortUsername();
|
||||
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
|
||||
boolean isPostScheduled=item.scheduledStatus!=null;
|
||||
menu.findItem(R.id.open_with_account).setVisible(!isPostScheduled && hasMultipleAccounts);
|
||||
menu.findItem(R.id.edit).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.open_in_browser).setVisible(item.status!=null);
|
||||
menu.findItem(R.id.delete_and_redraft).setVisible(!isPostScheduled && item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.pin).setVisible(!isPostScheduled && item.status!=null && isOwnPost && !item.status.pinned);
|
||||
menu.findItem(R.id.unpin).setVisible(!isPostScheduled && item.status!=null && isOwnPost && item.status.pinned);
|
||||
menu.findItem(R.id.mute_conversation).setVisible((item.status!=null && !item.status.muted && !isPostScheduled) && (isOwnPost || item.status.mentions.stream().anyMatch(m->{
|
||||
if(m==null)
|
||||
return false;
|
||||
return AccountSessionManager.get(item.parentFragment.getAccountID()).self.id.equals(m.id) ||
|
||||
AccountSessionManager.get(item.parentFragment.getAccountID()).self.getFullyQualifiedName().equals(m.username) ||
|
||||
AccountSessionManager.get(item.parentFragment.getAccountID()).self.acct.equals(m.acct);
|
||||
})));
|
||||
menu.findItem(R.id.unmute_conversation).setVisible(item.status!=null && item.status.muted);
|
||||
menu.findItem(R.id.open_in_browser).setVisible(!isPostScheduled && item.status!=null);
|
||||
menu.findItem(R.id.copy_link).setVisible(!isPostScheduled && item.status!=null);
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
MenuItem mute=menu.findItem(R.id.mute);
|
||||
MenuItem block=menu.findItem(R.id.block);
|
||||
MenuItem report=menu.findItem(R.id.report);
|
||||
MenuItem follow=menu.findItem(R.id.follow);
|
||||
MenuItem manageUserLists = menu.findItem(R.id.manage_user_lists);
|
||||
/* disabled in megalodon: add/remove bookmark is already available through status footer
|
||||
MenuItem bookmark=menu.findItem(R.id.bookmark);
|
||||
bookmark.setVisible(false);
|
||||
if(item.status!=null){
|
||||
bookmark.setVisible(true);
|
||||
bookmark.setTitle(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark);
|
||||
}else{
|
||||
bookmark.setVisible(false);
|
||||
}
|
||||
if(isOwnPost){
|
||||
*/
|
||||
if(isPostScheduled || isOwnPost){
|
||||
mute.setVisible(false);
|
||||
block.setVisible(false);
|
||||
report.setVisible(false);
|
||||
follow.setVisible(false);
|
||||
blockDomain.setVisible(false);
|
||||
manageUserLists.setVisible(false);
|
||||
}else{
|
||||
mute.setVisible(true);
|
||||
block.setVisible(true);
|
||||
// hiding when following to keep menu item count equal (trading it for user lists)
|
||||
block.setVisible(relationship == null || !relationship.following);
|
||||
report.setVisible(true);
|
||||
follow.setVisible(relationship==null || relationship.following || (!relationship.blocking && !relationship.blockedBy && !relationship.domainBlocking && !relationship.muting));
|
||||
mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, account.displayName));
|
||||
block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, account.displayName));
|
||||
report.setTitle(item.parentFragment.getString(R.string.report_user, account.displayName));
|
||||
follow.setTitle(item.parentFragment.getString(relationship!=null && relationship.following ? R.string.unfollow_user : R.string.follow_user, account.displayName));
|
||||
mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, username));
|
||||
mute.setIcon(relationship!=null && relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), mute);
|
||||
block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, username));
|
||||
report.setTitle(item.parentFragment.getString(R.string.report_user, username));
|
||||
// disabled in megalodon. domain blocks from a post clutters the context menu and looks out of place
|
||||
// if(!account.isLocal()){
|
||||
// blockDomain.setVisible(true);
|
||||
// blockDomain.setTitle(item.parentFragment.getString(relationship!=null && relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
// }else{
|
||||
blockDomain.setVisible(false);
|
||||
// }
|
||||
boolean following = relationship!=null && relationship.following;
|
||||
follow.setTitle(item.parentFragment.getString(following ? R.string.unfollow_user : R.string.follow_user, username));
|
||||
follow.setIcon(following ? R.drawable.ic_fluent_person_delete_24_regular : R.drawable.ic_fluent_person_add_24_regular);
|
||||
manageUserLists.setVisible(relationship != null && relationship.following);
|
||||
manageUserLists.setTitle(item.parentFragment.getString(R.string.sk_lists_with_user, username));
|
||||
// ic_fluent_person_add_24_regular actually has a width of 25dp -.-
|
||||
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), follow, following ? 0 : V.dp(-1));
|
||||
}
|
||||
|
||||
workaroundChangingMenuItemWidths(menu, username);
|
||||
}
|
||||
|
||||
// ugliest piece of code you'll see in a while: i measure the menu items' text widths to
|
||||
// determine the biggest one, because it's probably not being displayed at first
|
||||
// (before the relationship loaded). i take the largest one's size and add a space to the
|
||||
// last item ("open in browser") until it takes up as much space as the largest item.
|
||||
// goal: no more ugly ellipsis after the relationship loads in when opening the context menu
|
||||
// of a post
|
||||
private void workaroundChangingMenuItemWidths(Menu menu, String username) {
|
||||
String openInBrowserText = item.parentFragment.getString(R.string.open_in_browser);
|
||||
if (relationship == null) {
|
||||
float largestWidth = 0;
|
||||
Paint paint = new Paint();
|
||||
paint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL));
|
||||
String[] otherStrings = new String[] {
|
||||
item.parentFragment.getString(R.string.unfollow_user, username),
|
||||
item.parentFragment.getString(R.string.unblock_user, username),
|
||||
item.parentFragment.getString(R.string.unmute_user, username),
|
||||
item.parentFragment.getString(R.string.sk_lists_with_user, username),
|
||||
};
|
||||
for (int i = 0; i < menu.size(); i++) {
|
||||
MenuItem item = menu.getItem(i);
|
||||
if (item.getItemId() == R.id.open_in_browser || !item.isVisible()) continue;
|
||||
float width = paint.measureText(menu.getItem(i).getTitle().toString());
|
||||
if (width > largestWidth) largestWidth = width;
|
||||
}
|
||||
for (String str : otherStrings) {
|
||||
float width = paint.measureText(str);
|
||||
if (width > largestWidth) largestWidth = width;
|
||||
}
|
||||
float textWidth = paint.measureText(openInBrowserText);
|
||||
float missingWidth = Math.max(0, largestWidth - textWidth);
|
||||
float singleSpaceWidth = paint.measureText(" ");
|
||||
int howManySpaces = (int) Math.ceil(missingWidth / singleSpaceWidth);
|
||||
String enlargedText = openInBrowserText + " ".repeat(howManySpaces);
|
||||
menu.findItem(R.id.open_in_browser).setTitle(enlargedText);
|
||||
} else {
|
||||
menu.findItem(R.id.open_in_browser).setTitle(openInBrowserText);
|
||||
}
|
||||
menu.findItem(R.id.add_to_list).setVisible(relationship!=null && relationship.following);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -22,15 +23,15 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class LinkCardStatusDisplayItem extends StatusDisplayItem{
|
||||
private final Status status;
|
||||
private final UrlImageLoaderRequest imgRequest;
|
||||
|
||||
public LinkCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){
|
||||
public LinkCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status, boolean showImagePreview){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
if(status.card.image!=null)
|
||||
if(status.card.image!=null && showImagePreview)
|
||||
imgRequest=new UrlImageLoaderRequest(status.card.image, 1000, 1000);
|
||||
else
|
||||
imgRequest=null;
|
||||
@@ -101,23 +102,36 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
|
||||
photo.setBackground(null);
|
||||
photo.setImageTintList(null);
|
||||
crossfadeDrawable.setSize(card.width, card.height);
|
||||
if (card.width > 0) {
|
||||
// akkoma servers don't provide width and height
|
||||
crossfadeDrawable.setSize(card.width, card.height);
|
||||
} else {
|
||||
crossfadeDrawable.setSize(itemView.getWidth(), itemView.getHeight());
|
||||
}
|
||||
crossfadeDrawable.setBlurhashDrawable(card.blurhashPlaceholder);
|
||||
crossfadeDrawable.setCrossfadeAlpha(0f);
|
||||
photo.setImageDrawable(null);
|
||||
photo.setImageDrawable(crossfadeDrawable);
|
||||
photo.setVisibility(View.VISIBLE);
|
||||
didClear=false;
|
||||
}else{
|
||||
photo.setBackgroundColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3SurfaceVariant));
|
||||
photo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3Outline)));
|
||||
photo.setScaleType(ImageView.ScaleType.CENTER);
|
||||
photo.setImageResource(R.drawable.ic_feed_48px);
|
||||
} else {
|
||||
photo.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// if there's no image, we don't want to cover the inset borders
|
||||
FrameLayout.LayoutParams params=(FrameLayout.LayoutParams) inner.getLayoutParams();
|
||||
int margin=item.inset && item.imgRequest == null ? V.dp(1) : 0;
|
||||
params.setMargins(margin, 0, margin, margin);
|
||||
|
||||
boolean insetAndLast=item.inset && isLastDisplayItemForStatus();
|
||||
inner.setClipToOutline(insetAndLast);
|
||||
inner.setOutlineProvider(insetAndLast ? OutlineProviders.bottomRoundedRect(12) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable drawable){
|
||||
crossfadeDrawable.setImageDrawable(drawable);
|
||||
if(didClear)
|
||||
if(didClear && item.status.spoilerRevealed)
|
||||
crossfadeDrawable.animateAlpha(0f);
|
||||
Card card=item.status.card;
|
||||
// Make sure the image is not stretched if the server returned wrong dimensions
|
||||
@@ -134,7 +148,7 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onClick(View v){
|
||||
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), item.status.card.url, item.status);
|
||||
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), item.status.card.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.*;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
@@ -13,9 +16,11 @@ import android.util.Pair;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewPropertyAnimator;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -29,6 +34,7 @@ import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
|
||||
import org.joinmastodon.android.ui.photoviewer.AltTextSheet;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FrameLayoutThatOnlyMeasuresFirstChild;
|
||||
import org.joinmastodon.android.ui.views.MaxWidthFrameLayout;
|
||||
import org.joinmastodon.android.ui.views.MediaGridLayout;
|
||||
@@ -51,13 +57,11 @@ import me.grishka.appkit.utils.V;
|
||||
public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
private static final String TAG="MediaGridDisplayItem";
|
||||
|
||||
private final PhotoLayoutHelper.TiledLayoutResult tiledLayout;
|
||||
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
|
||||
private final TypedObjectPool<GridItemType, MediaAttachmentViewController> viewPool;
|
||||
private final List<Attachment> attachments;
|
||||
private final Map<String, Pair<String, String>> translatedAttachments = new HashMap<>();
|
||||
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
|
||||
public final Status status;
|
||||
public boolean sensitiveRevealed;
|
||||
public String sensitiveTitle;
|
||||
|
||||
public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List<Attachment> attachments, Status status){
|
||||
@@ -66,12 +70,11 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
this.viewPool=parentFragment.getAttachmentViewsPool();
|
||||
this.attachments=attachments;
|
||||
this.status=status;
|
||||
sensitiveRevealed=!status.sensitive;
|
||||
for(Attachment att:attachments){
|
||||
requests.add(new UrlImageLoaderRequest(switch(att.type){
|
||||
case IMAGE -> att.url;
|
||||
case VIDEO, GIFV -> att.previewUrl;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+att.type);
|
||||
case VIDEO, GIFV -> att.previewUrl == null ? att.url : att.previewUrl;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+att.url);
|
||||
}, 1000, 1000));
|
||||
}
|
||||
}
|
||||
@@ -104,13 +107,22 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
private final ArrayList<MediaAttachmentViewController> controllers=new ArrayList<>();
|
||||
|
||||
private final MaxWidthFrameLayout overlays;
|
||||
private final FrameLayout altTextWrapper;
|
||||
private final TextView altTextButton;
|
||||
private final ImageView noAltTextButton;
|
||||
private final View altTextScroller;
|
||||
private final ImageButton altTextClose;
|
||||
private final TextView altText, noAltText;
|
||||
|
||||
private final View sensitiveOverlay;
|
||||
private final LayerDrawable sensitiveOverlayBG;
|
||||
private static final ColorDrawable drawableForWhenThereIsNoBlurhash=new ColorDrawable(0xffffffff);
|
||||
private final TextView hideSensitiveButton;
|
||||
// private final FrameLayout hideSensitiveButton;
|
||||
private final TextView sensitiveText;
|
||||
|
||||
private int altTextIndex=-1;
|
||||
private Animator altTextAnimator;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(new FrameLayoutThatOnlyMeasuresFirstChild(activity));
|
||||
wrapper=(FrameLayout)itemView;
|
||||
@@ -119,15 +131,23 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
wrapper.setClipToPadding(false);
|
||||
|
||||
overlays=new MaxWidthFrameLayout(activity);
|
||||
overlays.setMaxWidth(V.dp(MediaGridLayout.MAX_WIDTH));
|
||||
overlays.setMaxWidth(UiUtils.MAX_WIDTH);
|
||||
wrapper.addView(overlays, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER_HORIZONTAL));
|
||||
|
||||
hideSensitiveButton=(TextView) activity.getLayoutInflater().inflate(R.layout.alt_text_badge, overlays, false);
|
||||
hideSensitiveButton.setText(R.string.hide);
|
||||
FrameLayout.LayoutParams lp;
|
||||
overlays.addView(hideSensitiveButton, lp=new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, V.dp(24), Gravity.END | Gravity.BOTTOM));
|
||||
int margin=V.dp(8);
|
||||
lp.setMargins(margin, margin, margin, margin);
|
||||
activity.getLayoutInflater().inflate(R.layout.overlay_image_alt_text, overlays);
|
||||
altTextWrapper=findViewById(R.id.alt_text_wrapper);
|
||||
altTextButton=findViewById(R.id.alt_button);
|
||||
noAltTextButton=findViewById(R.id.no_alt_button);
|
||||
altTextScroller=findViewById(R.id.alt_text_scroller);
|
||||
altTextClose=findViewById(R.id.alt_text_close);
|
||||
altText=findViewById(R.id.alt_text);
|
||||
noAltText=findViewById(R.id.no_alt_text);
|
||||
altTextClose.setOnClickListener(this::onAltTextCloseClick);
|
||||
|
||||
// megalodon: no sensitive hide button because the visibility toggle looks prettier imo
|
||||
// hideSensitiveButton=(FrameLayout) activity.getLayoutInflater().inflate(R.layout.alt_text_badge, overlays, false);
|
||||
// ((TextView) hideSensitiveButton.findViewById(R.id.alt_button)).setText(R.string.hide);
|
||||
// overlays.addView(hideSensitiveButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.END | Gravity.TOP));
|
||||
|
||||
activity.getLayoutInflater().inflate(R.layout.overlay_image_sensitive, overlays);
|
||||
sensitiveOverlay=findViewById(R.id.sensitive_overlay);
|
||||
@@ -136,14 +156,17 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
sensitiveOverlayBG.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable(true));
|
||||
sensitiveOverlay.setBackground(sensitiveOverlayBG);
|
||||
sensitiveOverlay.setOnClickListener(v->revealSensitive());
|
||||
hideSensitiveButton.setOnClickListener(v->hideSensitive());
|
||||
// hideSensitiveButton.setOnClickListener(v->hideSensitive());
|
||||
|
||||
sensitiveText=findViewById(R.id.sensitive_text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(MediaGridStatusDisplayItem item){
|
||||
wrapper.setPadding(0, 0, 0, item.inset ? 0 : V.dp(8));
|
||||
wrapper.setPadding(0, 0, 0, 0); // item.inset ? 0 : V.dp(8));
|
||||
|
||||
if(altTextAnimator!=null)
|
||||
altTextAnimator.cancel();
|
||||
|
||||
layout.setTiledLayout(item.tiledLayout);
|
||||
for(MediaAttachmentViewController c:controllers){
|
||||
@@ -151,7 +174,9 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
layout.removeAllViews();
|
||||
controllers.clear();
|
||||
|
||||
int i=0;
|
||||
if (!item.attachments.isEmpty()) updateBlurhashInSensitiveOverlay();
|
||||
for(Attachment att:item.attachments){
|
||||
MediaAttachmentViewController c=item.viewPool.obtain(switch(att.type){
|
||||
case IMAGE -> GridItemType.PHOTO;
|
||||
@@ -166,10 +191,10 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
layout.addView(c.view);
|
||||
c.view.setOnClickListener(clickListener);
|
||||
c.view.setTag(i);
|
||||
if(c.altButton!=null){
|
||||
c.altButton.setOnClickListener(altTextClickListener);
|
||||
c.altButton.setTag(i);
|
||||
c.altButton.setAlpha(1f);
|
||||
if(c.btnsWrap!=null){
|
||||
c.btnsWrap.setOnClickListener(altTextClickListener);
|
||||
c.btnsWrap.setTag(i);
|
||||
c.btnsWrap.setAlpha(1f);
|
||||
}
|
||||
controllers.add(c);
|
||||
|
||||
@@ -194,24 +219,44 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
c.bind(att, item.status);
|
||||
i++;
|
||||
}
|
||||
altTextButton.setVisibility(View.VISIBLE);
|
||||
noAltTextButton.setVisibility(View.VISIBLE);
|
||||
altTextWrapper.setVisibility(View.GONE);
|
||||
altTextIndex=-1;
|
||||
|
||||
if(!item.sensitiveRevealed){
|
||||
if(!item.status.sensitiveRevealed){
|
||||
sensitiveOverlay.setVisibility(View.VISIBLE);
|
||||
layout.setVisibility(View.INVISIBLE);
|
||||
updateBlurhashInSensitiveOverlay();
|
||||
}else{
|
||||
sensitiveOverlay.setVisibility(View.GONE);
|
||||
layout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
hideSensitiveButton.setVisibility(item.status.sensitive ? View.VISIBLE : View.GONE);
|
||||
// hideSensitiveButton.setVisibility(item.status.sensitive ? View.VISIBLE : View.GONE);
|
||||
if(!TextUtils.isEmpty(item.sensitiveTitle))
|
||||
sensitiveText.setText(item.sensitiveTitle);
|
||||
else if (!item.status.sensitive)
|
||||
sensitiveText.setText(R.string.media_hidden);
|
||||
else
|
||||
sensitiveText.setText(R.string.sensitive_content_explain);
|
||||
|
||||
boolean insetAndLast=item.inset && isLastDisplayItemForStatus();
|
||||
wrapper.setClipToOutline(insetAndLast);
|
||||
wrapper.setOutlineProvider(insetAndLast ? OutlineProviders.bottomRoundedRect(12) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable drawable){
|
||||
if(item.attachments.get(index).meta==null){
|
||||
Rect bounds=drawable.getBounds();
|
||||
drawable.setBounds(bounds.left, bounds.top, bounds.left+drawable.getIntrinsicWidth(), bounds.top+drawable.getIntrinsicHeight());
|
||||
Attachment.Metadata metadata = new Attachment.Metadata();
|
||||
metadata.width=drawable.getIntrinsicWidth();
|
||||
metadata.height=drawable.getIntrinsicHeight();
|
||||
item.attachments.get(index).meta=metadata;
|
||||
item.tiledLayout=PhotoLayoutHelper.processThumbs(item.attachments);
|
||||
UiUtils.beginLayoutTransition((ViewGroup) itemView);
|
||||
rebind();
|
||||
}
|
||||
controllers.get(index).setImage(drawable);
|
||||
}
|
||||
|
||||
@@ -226,13 +271,145 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onAltTextClick(View v){
|
||||
if(altTextAnimator!=null)
|
||||
altTextAnimator.cancel();
|
||||
// V.setVisibilityAnimated(hideSensitiveButton, View.GONE);
|
||||
V.cancelVisibilityAnimation(altTextWrapper);
|
||||
v.setVisibility(View.INVISIBLE);
|
||||
int index=(Integer)v.getTag();
|
||||
altTextIndex=index;
|
||||
Attachment att=item.attachments.get(index);
|
||||
new AltTextSheet(v.getContext(), att).show();
|
||||
boolean hasAltText = !TextUtils.isEmpty(att.description);
|
||||
if ((hasAltText && !showAltIndicator) || (!hasAltText && !showNoAltIndicator)) return;
|
||||
altTextButton.setVisibility(hasAltText && showAltIndicator ? View.VISIBLE : View.GONE);
|
||||
noAltTextButton.setVisibility(!hasAltText && showNoAltIndicator ? View.VISIBLE : View.GONE);
|
||||
altText.setVisibility(hasAltText && showAltIndicator ? View.VISIBLE : View.GONE);
|
||||
noAltText.setVisibility(!hasAltText && showNoAltIndicator ? View.VISIBLE : View.GONE);
|
||||
altText.setText(att.description);
|
||||
altTextWrapper.setVisibility(View.VISIBLE);
|
||||
altTextWrapper.setBackgroundResource(hasAltText ? R.drawable.bg_image_alt_text_overlay : R.drawable.bg_image_no_alt_overlay);
|
||||
altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
int[] loc={0, 0};
|
||||
v.getLocationInWindow(loc);
|
||||
int btnL=loc[0], btnT=loc[1];
|
||||
overlays.getLocationInWindow(loc);
|
||||
btnL-=loc[0];
|
||||
btnT-=loc[1];
|
||||
|
||||
ViewGroup.MarginLayoutParams margins = (ViewGroup.MarginLayoutParams) altTextWrapper.getLayoutParams();
|
||||
ArrayList<Animator> anims=new ArrayList<>();
|
||||
anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1, 0));
|
||||
anims.add(ObjectAnimator.ofFloat(noAltTextButton, View.ALPHA, 1, 0));
|
||||
anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0, 1));
|
||||
anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0, 1));
|
||||
anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL+margins.leftMargin, altTextWrapper.getLeft()));
|
||||
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+margins.topMargin, altTextWrapper.getTop()));
|
||||
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+v.getWidth()-margins.rightMargin, altTextWrapper.getRight()));
|
||||
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+v.getHeight()-margins.bottomMargin, altTextWrapper.getBottom()));
|
||||
for(Animator a:anims)
|
||||
a.setDuration(300);
|
||||
|
||||
for(MediaAttachmentViewController c:controllers){
|
||||
if(c.btnsWrap!=null && c.btnsWrap!=v){
|
||||
anims.add(ObjectAnimator.ofFloat(c.btnsWrap, View.ALPHA, 1, 0).setDuration(150));
|
||||
}
|
||||
if (c.extraBadge != null) {
|
||||
anims.add(ObjectAnimator.ofFloat(c.extraBadge, View.ALPHA, 1, 0).setDuration(150));
|
||||
}
|
||||
}
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(anims);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
altTextAnimator=null;
|
||||
for(MediaAttachmentViewController c:controllers){
|
||||
if(c.btnsWrap!=null){
|
||||
c.btnsWrap.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
if (c.extraBadge != null) c.extraBadge.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
altTextAnimator=set;
|
||||
set.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onAltTextCloseClick(View v){
|
||||
if(altTextAnimator!=null)
|
||||
altTextAnimator.cancel();
|
||||
|
||||
// V.setVisibilityAnimated(hideSensitiveButton, item.status.sensitive ? View.VISIBLE : View.GONE);
|
||||
V.cancelVisibilityAnimation(altTextWrapper);
|
||||
View btn=controllers.get(altTextIndex).btnsWrap;
|
||||
int i=0;
|
||||
for(MediaAttachmentViewController c:controllers){
|
||||
boolean hasAltText = !TextUtils.isEmpty(item.attachments.get(i).description);
|
||||
if(c.btnsWrap!=null
|
||||
&& c.btnsWrap!=btn
|
||||
&& ((hasAltText && showAltIndicator) || (!hasAltText && showNoAltIndicator))
|
||||
) c.btnsWrap.setVisibility(View.VISIBLE);
|
||||
if (c.extraBadge != null) c.extraBadge.setVisibility(View.VISIBLE);
|
||||
i++;
|
||||
}
|
||||
|
||||
int[] loc={0, 0};
|
||||
btn.getLocationInWindow(loc);
|
||||
int btnL=loc[0], btnT=loc[1];
|
||||
overlays.getLocationInWindow(loc);
|
||||
btnL-=loc[0];
|
||||
btnT-=loc[1];
|
||||
|
||||
ViewGroup.MarginLayoutParams margins = (ViewGroup.MarginLayoutParams) altTextWrapper.getLayoutParams();
|
||||
ArrayList<Animator> anims=new ArrayList<>();
|
||||
anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1));
|
||||
anims.add(ObjectAnimator.ofFloat(noAltTextButton, View.ALPHA, 1));
|
||||
anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0));
|
||||
anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0));
|
||||
anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL+margins.leftMargin));
|
||||
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+margins.topMargin));
|
||||
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+btn.getWidth()-margins.rightMargin));
|
||||
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+btn.getHeight()-margins.bottomMargin));
|
||||
for(Animator a:anims)
|
||||
a.setDuration(300);
|
||||
|
||||
for(MediaAttachmentViewController c:controllers){
|
||||
if(c.btnsWrap!=null && c.btnsWrap!=btn){
|
||||
anims.add(ObjectAnimator.ofFloat(c.btnsWrap, View.ALPHA, 1).setDuration(150));
|
||||
}
|
||||
if (c.extraBadge != null) {
|
||||
anims.add(ObjectAnimator.ofFloat(c.extraBadge, View.ALPHA, 1).setDuration(150));
|
||||
}
|
||||
}
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(anims);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
altTextAnimator=null;
|
||||
V.setVisibilityAnimated(altTextWrapper, View.GONE);
|
||||
V.setVisibilityAnimated(btn, View.VISIBLE);
|
||||
btn.setAlpha(1);
|
||||
}
|
||||
});
|
||||
altTextAnimator=set;
|
||||
set.start();
|
||||
}
|
||||
|
||||
public MediaAttachmentViewController getViewController(int index){
|
||||
return controllers.get(index);
|
||||
return index<controllers.size() ? controllers.get(index) : null;
|
||||
}
|
||||
|
||||
public void setClipChildren(boolean clip){
|
||||
@@ -241,23 +418,25 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void updateBlurhashInSensitiveOverlay(){
|
||||
Drawable d=item.attachments.get(0).blurhashPlaceholder;
|
||||
Drawable d = item.attachments.get(0).blurhashPlaceholder;
|
||||
sensitiveOverlayBG.setDrawableByLayerId(R.id.blurhash, d==null ? drawableForWhenThereIsNoBlurhash : d.mutate());
|
||||
sensitiveOverlay.setBackground(sensitiveOverlayBG);
|
||||
}
|
||||
|
||||
private void revealSensitive(){
|
||||
if(item.sensitiveRevealed)
|
||||
public void revealSensitive(){
|
||||
if(item.status.sensitiveRevealed)
|
||||
return;
|
||||
item.sensitiveRevealed=true;
|
||||
item.status.sensitiveRevealed=true;
|
||||
V.setVisibilityAnimated(sensitiveOverlay, View.GONE);
|
||||
layout.setVisibility(View.VISIBLE);
|
||||
item.parentFragment.onSensitiveRevealed(this);
|
||||
}
|
||||
|
||||
private void hideSensitive(){
|
||||
if(!item.sensitiveRevealed)
|
||||
public void hideSensitive(){
|
||||
if(!item.status.sensitiveRevealed)
|
||||
return;
|
||||
updateBlurhashInSensitiveOverlay();
|
||||
item.sensitiveRevealed=false;
|
||||
item.status.sensitiveRevealed=false;
|
||||
V.setVisibilityAnimated(sensitiveOverlay, View.VISIBLE, ()->layout.setVisibility(View.INVISIBLE));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import static org.joinmastodon.android.MastodonApp.context;
|
||||
import static org.joinmastodon.android.ui.utils.UiUtils.generateFormattedString;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
@@ -14,8 +19,12 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.NotificationsListFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
@@ -23,6 +32,8 @@ import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
@@ -32,41 +43,54 @@ import me.grishka.appkit.utils.V;
|
||||
public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Notification notification;
|
||||
private ImageLoaderRequest avaRequest;
|
||||
private String accountID;
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
private CharSequence text;
|
||||
private final String accountID;
|
||||
private final CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
private final CharSequence text;
|
||||
private final CharSequence timestamp;
|
||||
|
||||
public NotificationHeaderStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Notification notification, String accountID){
|
||||
super(parentID, parentFragment);
|
||||
this.notification=notification;
|
||||
this.accountID=accountID;
|
||||
this.timestamp=notification.createdAt==null ? null : UiUtils.formatRelativeTimestamp(context, notification.createdAt);
|
||||
|
||||
if(notification.type==Notification.Type.POLL){
|
||||
text=parentFragment.getString(R.string.poll_ended);
|
||||
}else{
|
||||
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? notification.account.avatar : notification.account.avatarStatic, V.dp(50), V.dp(50));
|
||||
SpannableStringBuilder parsedName=new SpannableStringBuilder(notification.account.displayName);
|
||||
AccountSession session = AccountSessionManager.get(accountID);
|
||||
avaRequest=new UrlImageLoaderRequest(
|
||||
TextUtils.isEmpty(notification.account.avatar) ? session.getDefaultAvatarUrl() :
|
||||
GlobalUserPreferences.playGifs ? notification.account.avatar : notification.account.avatarStatic,
|
||||
V.dp(50), V.dp(50));
|
||||
SpannableStringBuilder parsedName=new SpannableStringBuilder(notification.account.getDisplayName());
|
||||
HtmlParser.parseCustomEmoji(parsedName, notification.account.emojis);
|
||||
emojiHelper.setText(parsedName);
|
||||
|
||||
String[] parts=parentFragment.getString(switch(notification.type){
|
||||
String str = parentFragment.getString(switch(notification.type){
|
||||
case FOLLOW -> R.string.user_followed_you;
|
||||
case FOLLOW_REQUEST -> R.string.user_sent_follow_request;
|
||||
case REBLOG -> R.string.notification_boosted;
|
||||
case FAVORITE -> R.string.user_favorited;
|
||||
case POLL -> R.string.poll_ended;
|
||||
case UPDATE -> R.string.sk_post_edited;
|
||||
case SIGN_UP -> R.string.sk_signed_up;
|
||||
case REPORT -> R.string.sk_reported;
|
||||
case REACTION, PLEROMA_EMOJI_REACTION ->
|
||||
!TextUtils.isEmpty(notification.emoji) ? R.string.sk_reacted_with : R.string.sk_reacted;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+notification.type);
|
||||
}).split("%s", 2);
|
||||
SpannableStringBuilder text=new SpannableStringBuilder();
|
||||
if(parts.length>1 && !TextUtils.isEmpty(parts[0]))
|
||||
text.append(parts[0]);
|
||||
text.append(parsedName, new TypefaceSpan("sans-serif-medium"), 0);
|
||||
if(parts.length==1){
|
||||
text.append(' ');
|
||||
text.append(parts[0]);
|
||||
}else if(!TextUtils.isEmpty(parts[1])){
|
||||
text.append(parts[1]);
|
||||
});
|
||||
|
||||
if (!TextUtils.isEmpty(notification.emoji)) {
|
||||
SpannableStringBuilder emoji = new SpannableStringBuilder(notification.emoji);
|
||||
if (!TextUtils.isEmpty(notification.emojiUrl)) {
|
||||
HtmlParser.parseCustomEmoji(emoji, Collections.singletonList(new Emoji(
|
||||
notification.emoji, notification.emojiUrl, notification.emojiUrl
|
||||
)));
|
||||
}
|
||||
this.text = generateFormattedString(str, parsedName, emoji);
|
||||
} else {
|
||||
this.text = generateFormattedString(str, parsedName);
|
||||
}
|
||||
this.text=text;
|
||||
|
||||
emojiHelper.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,18 +113,26 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<NotificationHeaderStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final ImageView icon, avatar;
|
||||
private final TextView text;
|
||||
private final ImageView icon, avatar, deleteNotification;
|
||||
private final TextView text, timestamp;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_notification_header, parent);
|
||||
icon=findViewById(R.id.icon);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
text=findViewById(R.id.text);
|
||||
timestamp=findViewById(R.id.timestamp);
|
||||
deleteNotification=findViewById(R.id.delete_notification);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
avatar.setClipToOutline(true);
|
||||
avatar.setOnClickListener(this::onAvaClick);
|
||||
deleteNotification.setOnClickListener(v->UiUtils.confirmDeleteNotification(activity, item.parentFragment.getAccountID(), item.notification, ()->{
|
||||
if (item.parentFragment instanceof NotificationsListFragment fragment) {
|
||||
fragment.removeNotification(item.notification);
|
||||
}
|
||||
}));
|
||||
|
||||
itemView.setOnClickListener(this::onItemClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,6 +143,8 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
item.emojiHelper.setImageDrawable(index-1, image);
|
||||
text.invalidate();
|
||||
}
|
||||
if(image instanceof Animatable)
|
||||
((Animatable) image).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,28 +155,38 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
ImageLoaderViewHolder.super.clearImage(index);
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
@Override
|
||||
public void onBind(NotificationHeaderStatusDisplayItem item){
|
||||
text.setText(item.text);
|
||||
timestamp.setText(item.timestamp);
|
||||
avatar.setVisibility(item.notification.type==Notification.Type.POLL ? View.GONE : View.VISIBLE);
|
||||
// TODO use real icons
|
||||
icon.setImageResource(switch(item.notification.type){
|
||||
case FAVORITE -> R.drawable.ic_star_fill1_24px;
|
||||
case REBLOG -> R.drawable.ic_repeat_fill1_24px;
|
||||
case FOLLOW, FOLLOW_REQUEST -> R.drawable.ic_person_add_fill1_24px;
|
||||
case POLL -> R.drawable.ic_insert_chart_fill1_24px;
|
||||
case FAVORITE -> GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_24_filled : R.drawable.ic_fluent_star_24_filled;
|
||||
case REBLOG -> R.drawable.ic_fluent_arrow_repeat_all_24_filled;
|
||||
case FOLLOW, FOLLOW_REQUEST -> R.drawable.ic_fluent_person_add_24_filled;
|
||||
case POLL -> R.drawable.ic_fluent_poll_24_filled;
|
||||
case REPORT -> R.drawable.ic_fluent_warning_24_filled;
|
||||
case SIGN_UP -> R.drawable.ic_fluent_person_available_24_filled;
|
||||
case UPDATE -> R.drawable.ic_fluent_edit_24_filled;
|
||||
case REACTION, PLEROMA_EMOJI_REACTION -> R.drawable.ic_fluent_add_24_filled;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+item.notification.type);
|
||||
});
|
||||
icon.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(item.parentFragment.getActivity(), switch(item.notification.type){
|
||||
case FAVORITE -> R.attr.colorFavorite;
|
||||
case FAVORITE -> GlobalUserPreferences.likeIcon ? R.attr.colorLike : R.attr.colorFavorite;
|
||||
case REBLOG -> R.attr.colorBoost;
|
||||
case FOLLOW, FOLLOW_REQUEST -> R.attr.colorFollow;
|
||||
case POLL -> R.attr.colorPoll;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+item.notification.type);
|
||||
default -> android.R.attr.colorAccent;
|
||||
})));
|
||||
deleteNotification.setVisibility(GlobalUserPreferences.enableDeleteNotifications && item.notification != null ? View.VISIBLE : View.GONE);
|
||||
itemView.setBackgroundResource(0);
|
||||
}
|
||||
|
||||
private void onAvaClick(View v){
|
||||
public void onItemClick(View v) {
|
||||
if (item.notification.type == Notification.Type.REPORT) {
|
||||
UiUtils.showFragmentForNotification(item.parentFragment.getContext(), item.notification, item.accountID, null);
|
||||
return;
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.notification.account));
|
||||
|
||||
@@ -9,14 +9,18 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
public class PollFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Poll poll;
|
||||
public boolean resultsVisible=false;
|
||||
public final Status status;
|
||||
|
||||
public PollFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Poll poll){
|
||||
public PollFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Poll poll, Status status){
|
||||
super(parentID, parentFragment);
|
||||
this.poll=poll;
|
||||
this.status=status;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -26,28 +30,40 @@ public class PollFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<PollFooterStatusDisplayItem>{
|
||||
private TextView text;
|
||||
private Button button;
|
||||
private Button voteButton, resultsButton;
|
||||
private ViewGroup wrapper;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_poll_footer, parent);
|
||||
text=findViewById(R.id.text);
|
||||
button=findViewById(R.id.vote_btn);
|
||||
button.setOnClickListener(v->item.parentFragment.onPollVoteButtonClick(this));
|
||||
voteButton=findViewById(R.id.vote_btn);
|
||||
voteButton.setOnClickListener(v->item.parentFragment.onPollVoteButtonClick(this));
|
||||
resultsButton=findViewById(R.id.results_btn);
|
||||
wrapper=findViewById(R.id.wrapper);
|
||||
resultsButton.setOnClickListener(v-> {
|
||||
item.resultsVisible = !item.resultsVisible;
|
||||
item.parentFragment.onPollViewResultsButtonClick(this, item.resultsVisible);
|
||||
rebind();
|
||||
UiUtils.beginLayoutTransition(wrapper);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(PollFooterStatusDisplayItem item){
|
||||
String text=item.parentFragment.getResources().getQuantityString(R.plurals.x_votes, item.poll.votesCount, item.poll.votesCount);
|
||||
if(item.poll.expiresAt!=null && !item.poll.isExpired()){
|
||||
text+=" · "+UiUtils.formatTimeLeft(itemView.getContext(), item.poll.expiresAt);
|
||||
if(item.poll.multiple)
|
||||
text+=" · "+item.parentFragment.getString(R.string.poll_multiple_choice);
|
||||
}else if(item.poll.isExpired()){
|
||||
text+=" · "+item.parentFragment.getString(R.string.poll_closed);
|
||||
}
|
||||
String sep=" "+item.parentFragment.getString(R.string.sk_separator)+" ";
|
||||
if(item.poll.expiresAt!=null && !item.poll.isExpired())
|
||||
text+=sep+UiUtils.formatTimeLeft(itemView.getContext(), item.poll.expiresAt).replaceAll(" ", " ");
|
||||
else if(item.poll.isExpired())
|
||||
text+=sep+item.parentFragment.getString(R.string.poll_closed).replaceAll(" ", " ");
|
||||
if(item.poll.multiple)
|
||||
text+=sep+item.parentFragment.getString(R.string.sk_poll_multiple_choice).replaceAll(" ", " ");
|
||||
this.text.setText(text);
|
||||
button.setVisibility(item.poll.isExpired() || item.poll.voted || !item.poll.multiple ? View.GONE : View.VISIBLE);
|
||||
button.setEnabled(item.poll.selectedOptions!=null && !item.poll.selectedOptions.isEmpty());
|
||||
resultsButton.setVisibility(item.poll.isExpired() || item.poll.voted ? View.GONE : View.VISIBLE);
|
||||
resultsButton.setText(item.resultsVisible ? R.string.sk_poll_hide_results : R.string.sk_poll_show_results);
|
||||
resultsButton.setSelected(item.resultsVisible);
|
||||
voteButton.setVisibility(item.poll.isExpired() || item.poll.voted ? View.GONE : View.VISIBLE);
|
||||
voteButton.setEnabled(item.poll.selectedOptions!=null && !item.poll.selectedOptions.isEmpty() && !item.resultsVisible);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -12,10 +13,12 @@ import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
@@ -43,6 +46,10 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
text=HtmlParser.parseCustomEmoji(option.title, poll.emojis);
|
||||
emojiHelper.setText(text);
|
||||
showResults=poll.isExpired() || poll.voted;
|
||||
calculateResults();
|
||||
}
|
||||
|
||||
private void calculateResults() {
|
||||
int total=poll.votersCount>0 ? poll.votersCount : poll.votesCount;
|
||||
if(showResults && option.votesCount!=null && total>0){
|
||||
votesFraction=(float)option.votesCount/(float)total;
|
||||
@@ -70,17 +77,17 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<PollOptionStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView text, percent;
|
||||
private final View check, button;
|
||||
private final Drawable progressBg, progressBgInset;
|
||||
private final View button;
|
||||
private final ImageView icon;
|
||||
private final Drawable progressBg;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_poll_option, parent);
|
||||
text=findViewById(R.id.text);
|
||||
percent=findViewById(R.id.percent);
|
||||
check=findViewById(R.id.checkbox);
|
||||
icon=findViewById(R.id.icon);
|
||||
button=findViewById(R.id.button);
|
||||
progressBg=activity.getResources().getDrawable(R.drawable.bg_poll_option_voted, activity.getTheme()).mutate();
|
||||
progressBgInset=activity.getResources().getDrawable(R.drawable.bg_poll_option_voted_inset, activity.getTheme()).mutate();
|
||||
itemView.setOnClickListener(this::onButtonClick);
|
||||
button.setOutlineProvider(OutlineProviders.roundedRect(20));
|
||||
button.setClipToOutline(true);
|
||||
@@ -98,24 +105,22 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
percent.setVisibility(item.showResults ? View.VISIBLE : View.GONE);
|
||||
itemView.setClickable(!item.showResults);
|
||||
icon.setImageDrawable(itemView.getContext().getDrawable(item.poll.multiple ?
|
||||
item.showResults ? R.drawable.ic_poll_checkbox_regular_selector : R.drawable.ic_poll_checkbox_filled_selector :
|
||||
item.showResults ? R.drawable.ic_poll_option_button : R.drawable.ic_fluent_radio_button_24_selector
|
||||
));
|
||||
if(item.showResults){
|
||||
Drawable bg=item.inset ? progressBgInset : progressBg;
|
||||
Drawable bg=progressBg;
|
||||
bg.setLevel(Math.round(10000f*item.votesFraction));
|
||||
button.setBackground(bg);
|
||||
itemView.setSelected(item.isMostVoted);
|
||||
check.setSelected(item.poll.ownVotes!=null && item.poll.ownVotes.contains(item.optionIndex));
|
||||
itemView.setSelected(item.poll.ownVotes!=null && item.poll.ownVotes.contains(item.optionIndex));
|
||||
percent.setText(String.format(Locale.getDefault(), "%d%%", Math.round(item.votesFraction*100f)));
|
||||
}else{
|
||||
itemView.setSelected(item.poll.selectedOptions!=null && item.poll.selectedOptions.contains(item.option));
|
||||
button.setBackgroundResource(item.inset ? R.drawable.bg_poll_option_clickable_inset : R.drawable.bg_poll_option_clickable);
|
||||
}
|
||||
if(item.inset){
|
||||
text.setTextColor(itemView.getContext().getColorStateList(R.color.poll_option_text_inset));
|
||||
percent.setTextColor(itemView.getContext().getColorStateList(R.color.poll_option_text_inset));
|
||||
}else{
|
||||
text.setTextColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3Primary));
|
||||
percent.setTextColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3OnSecondaryContainer));
|
||||
button.setBackgroundResource(R.drawable.bg_poll_option_clickable);
|
||||
}
|
||||
text.setTextColor(UiUtils.getThemeColor(itemView.getContext(), android.R.attr.textColorPrimary));
|
||||
percent.setTextColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3OnSecondaryContainer));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -136,5 +141,11 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
private void onButtonClick(View v){
|
||||
item.parentFragment.onPollOptionClick(this);
|
||||
}
|
||||
|
||||
public void showResults(boolean shown) {
|
||||
item.showResults = shown;
|
||||
item.calculateResults();
|
||||
rebind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.Translation;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.utils.PreviewlessMediaAttachmentViewController;
|
||||
import org.joinmastodon.android.ui.views.FrameLayoutThatOnlyMeasuresFirstChild;
|
||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class PreviewlessMediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
private static final String TAG="PreviewlessMediaGridDisplayItem";
|
||||
|
||||
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
|
||||
private final TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, PreviewlessMediaAttachmentViewController> viewPool;
|
||||
private final List<Attachment> attachments;
|
||||
private final Map<String, Pair<String, String>> translatedAttachments = new HashMap<>();
|
||||
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
|
||||
public final Status status;
|
||||
public String sensitiveTitle;
|
||||
|
||||
public PreviewlessMediaGridStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List<Attachment> attachments, Status status){
|
||||
super(parentID, parentFragment);
|
||||
this.tiledLayout=tiledLayout;
|
||||
this.viewPool=parentFragment.getPreviewlessAttachmentViewsPool();
|
||||
this.attachments=attachments;
|
||||
this.status=status;
|
||||
// for(Attachment att:attachments){
|
||||
// requests.add(new UrlImageLoaderRequest(switch(att.type){
|
||||
// case IMAGE -> att.url;
|
||||
// case VIDEO, GIFV -> att.previewUrl == null ? att.url : att.previewUrl;
|
||||
// default -> throw new IllegalStateException("Unexpected value: "+att.url);
|
||||
// }, 1000, 1000));
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.PREVIEWLESS_MEDIA_GRID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return attachments.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return requests.get(index);
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<PreviewlessMediaGridStatusDisplayItem> {
|
||||
private final FrameLayout wrapper;
|
||||
private final LinearLayout layout;
|
||||
private final View.OnClickListener clickListener=this::onViewClick;
|
||||
private final ArrayList<PreviewlessMediaAttachmentViewController> controllers=new ArrayList<>();
|
||||
|
||||
// private final FrameLayout hideSensitiveButton;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(new FrameLayoutThatOnlyMeasuresFirstChild(activity));
|
||||
wrapper=(FrameLayout)itemView;
|
||||
layout= new LinearLayout(activity);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
layout.setLayoutParams(params);
|
||||
wrapper.addView(layout);
|
||||
wrapper.setClipToPadding(false);
|
||||
|
||||
// megalodon: no sensitive hide button because the visibility toggle looks prettier imo
|
||||
// hideSensitiveButton=(FrameLayout) activity.getLayoutInflater().inflate(R.layout.alt_text_badge, overlays, false);
|
||||
// ((TextView) hideSensitiveButton.findViewById(R.id.alt_button)).setText(R.string.hide);
|
||||
// overlays.addView(hideSensitiveButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.END | Gravity.TOP));
|
||||
|
||||
// hideSensitiveButton.setOnClickListener(v->hideSensitive());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(PreviewlessMediaGridStatusDisplayItem item){
|
||||
wrapper.setPadding(0, 0, 0, 0); // item.inset ? 0 : V.dp(8));
|
||||
|
||||
// if(altTextAnimator!=null)
|
||||
// altTextAnimator.cancel();
|
||||
|
||||
for(PreviewlessMediaAttachmentViewController c:controllers){
|
||||
item.viewPool.reuse(c.type, c);
|
||||
}
|
||||
layout.removeAllViews();
|
||||
controllers.clear();
|
||||
|
||||
int i=0;
|
||||
// if (!item.attachments.isEmpty()) updateBlurhashInSensitiveOverlay();
|
||||
for(Attachment att:item.attachments){
|
||||
PreviewlessMediaAttachmentViewController c=item.viewPool.obtain(switch(att.type){
|
||||
case IMAGE -> MediaGridStatusDisplayItem.GridItemType.PHOTO;
|
||||
case VIDEO -> MediaGridStatusDisplayItem.GridItemType.VIDEO;
|
||||
case GIFV -> MediaGridStatusDisplayItem.GridItemType.GIFV;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+att.type);
|
||||
});
|
||||
if(c.view.getLayoutParams()==null)
|
||||
c.view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
layout.addView(c.view);
|
||||
c.view.setOnClickListener(clickListener);
|
||||
c.view.setTag(i);
|
||||
controllers.add(c);
|
||||
|
||||
if (item.status.translation != null){
|
||||
if(item.status.translationState==Status.TranslationState.SHOWN){
|
||||
if(!item.translatedAttachments.containsKey(att.id)){
|
||||
Optional<Translation.MediaAttachment> translatedAttachment=Arrays.stream(item.status.translation.mediaAttachments).filter(mediaAttachment->mediaAttachment.id.equals(att.id)).findFirst();
|
||||
translatedAttachment.ifPresent(mediaAttachment->{
|
||||
item.translatedAttachments.put(mediaAttachment.id, new Pair<>(att.description, mediaAttachment.description));
|
||||
att.description=mediaAttachment.description;
|
||||
});
|
||||
}else{
|
||||
//SAFETY: must be non-null, as we check if the map contains the attachment before
|
||||
att.description=Objects.requireNonNull(item.translatedAttachments.get(att.id)).second;
|
||||
}
|
||||
}else{
|
||||
if (item.translatedAttachments.containsKey(att.id)) {
|
||||
att.description=Objects.requireNonNull(item.translatedAttachments.get(att.id)).first;
|
||||
}
|
||||
}
|
||||
}
|
||||
c.bind(att, item.status);
|
||||
i++;
|
||||
}
|
||||
|
||||
boolean insetAndLast=item.inset && isLastDisplayItemForStatus();
|
||||
wrapper.setClipToOutline(insetAndLast);
|
||||
wrapper.setOutlineProvider(insetAndLast ? OutlineProviders.bottomRoundedRect(12) : null);
|
||||
}
|
||||
|
||||
private void onViewClick(View v){
|
||||
int index=(Integer)v.getTag();
|
||||
item.parentFragment.openPreviewlessMediaPhotoViewer(item.parentID, item.status, index, this);
|
||||
}
|
||||
|
||||
|
||||
public PreviewlessMediaAttachmentViewController getViewController(int index){
|
||||
return controllers.get(index);
|
||||
}
|
||||
|
||||
public void setClipChildren(boolean clip){
|
||||
layout.setClipChildren(clip);
|
||||
wrapper.setClipChildren(clip);
|
||||
}
|
||||
|
||||
public LinearLayout getLayout(){
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import static org.joinmastodon.android.MastodonApp.context;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -11,6 +16,8 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -18,23 +25,53 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
||||
private CharSequence text;
|
||||
@DrawableRes
|
||||
private int icon;
|
||||
private StatusPrivacy visibility;
|
||||
@DrawableRes
|
||||
private int iconEnd;
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
private View.OnClickListener handleClick;
|
||||
public boolean needBottomPadding;
|
||||
ReblogOrReplyLineStatusDisplayItem extra;
|
||||
CharSequence fullText;
|
||||
|
||||
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon){
|
||||
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, Status status) {
|
||||
this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, text, status);
|
||||
}
|
||||
|
||||
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, CharSequence fullText, Status status) {
|
||||
super(parentID, parentFragment);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(text);
|
||||
if(AccountSessionManager.get(parentFragment.getAccountID()).getLocalPreferences().customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(ssb, emojis);
|
||||
this.text=ssb;
|
||||
emojiHelper.setText(ssb);
|
||||
this.fullText=fullText;
|
||||
this.icon=icon;
|
||||
this.status=status;
|
||||
this.handleClick=handleClick;
|
||||
TypedValue outValue = new TypedValue();
|
||||
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
|
||||
updateVisibility(visibility);
|
||||
}
|
||||
|
||||
public void updateVisibility(StatusPrivacy visibility) {
|
||||
this.visibility = visibility;
|
||||
this.iconEnd = visibility != null ? switch (visibility) {
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_20sp_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_lock_open_20sp_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20sp_filled;
|
||||
default -> 0;
|
||||
} : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,33 +81,66 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return emojiHelper.getImageCount();
|
||||
return emojiHelper.getImageCount() + (extra!=null ? extra.emojiHelper.getImageCount() : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return emojiHelper.getImageRequest(index);
|
||||
int firstHelperCount=emojiHelper.getImageCount();
|
||||
CustomEmojiHelper helper=index<firstHelperCount ? emojiHelper : extra.emojiHelper;
|
||||
return helper.getImageRequest(firstHelperCount>0 ? index%firstHelperCount : index);
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<ReblogOrReplyLineStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView text;
|
||||
private final TextView text, extraText;
|
||||
private final View separator;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_reblog_or_reply_line, parent);
|
||||
text=findViewById(R.id.text);
|
||||
extraText=findViewById(R.id.extra_text);
|
||||
separator=findViewById(R.id.separator);
|
||||
}
|
||||
|
||||
private void bindLine(ReblogOrReplyLineStatusDisplayItem item, TextView text) {
|
||||
text.setText(item.text);
|
||||
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0);
|
||||
text.setOnClickListener(item.handleClick);
|
||||
text.setEnabled(!item.inset && item.handleClick != null);
|
||||
text.setClickable(!item.inset && item.handleClick != null);
|
||||
Context ctx = itemView.getContext();
|
||||
int visibilityText = item.visibility != null ? switch (item.visibility) {
|
||||
case PUBLIC -> R.string.visibility_public;
|
||||
case UNLISTED -> R.string.sk_visibility_unlisted;
|
||||
case PRIVATE -> R.string.visibility_followers_only;
|
||||
case LOCAL -> R.string.sk_local_only;
|
||||
default -> 0;
|
||||
} : 0;
|
||||
String visibilityDescription=visibilityText!=0 ? " (" + ctx.getString(visibilityText) + ")" : null;
|
||||
text.setContentDescription(item.fullText==null && visibilityDescription==null ? null :
|
||||
(item.fullText!=null ? item.fullText : item.text)
|
||||
+ (visibilityDescription!=null ? visibilityDescription : ""));
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(text);
|
||||
text.setCompoundDrawableTintList(text.getTextColors());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ReblogOrReplyLineStatusDisplayItem item){
|
||||
text.setText(item.text);
|
||||
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, 0, 0);
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(text);
|
||||
bindLine(item, text);
|
||||
if (item.extra != null) bindLine(item.extra, extraText);
|
||||
extraText.setVisibility(item.extra == null ? View.GONE : View.VISIBLE);
|
||||
separator.setVisibility(item.extra == null ? View.GONE : View.VISIBLE);
|
||||
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
item.emojiHelper.setImageDrawable(index, image);
|
||||
int firstHelperCount=item.emojiHelper.getImageCount();
|
||||
CustomEmojiHelper helper=index<firstHelperCount ? item.emojiHelper : item.extra.emojiHelper;
|
||||
helper.setImageDrawable(firstHelperCount>0 ? index%firstHelperCount : index, image);
|
||||
text.invalidate();
|
||||
extraText.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.graphics.drawable.LayerDrawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -23,17 +24,18 @@ import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
|
||||
public class SpoilerStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
public final ArrayList<StatusDisplayItem> contentItems=new ArrayList<>();
|
||||
private final CharSequence parsedTitle;
|
||||
private CharSequence translatedTitle;
|
||||
private final CustomEmojiHelper emojiHelper;
|
||||
private final Type type;
|
||||
private final int attachmentCount;
|
||||
|
||||
public SpoilerStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, String title, Status status, Status statusForContent, Type type){
|
||||
public SpoilerStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, String title, Status statusForContent, Type type){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
this.status=statusForContent;
|
||||
this.type=type;
|
||||
this.attachmentCount=statusForContent.mediaAttachments.size();
|
||||
if(TextUtils.isEmpty(title)){
|
||||
parsedTitle=HtmlParser.parseCustomEmoji(statusForContent.spoilerText, statusForContent.emojis);
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
@@ -62,12 +64,14 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
|
||||
public static class Holder extends StatusDisplayItem.Holder<SpoilerStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView title, action;
|
||||
private final View button;
|
||||
private final ImageView mediaIcon;
|
||||
|
||||
public Holder(Context context, ViewGroup parent, Type type){
|
||||
super(context, R.layout.display_item_spoiler, parent);
|
||||
title=findViewById(R.id.spoiler_title);
|
||||
action=findViewById(R.id.spoiler_action);
|
||||
button=findViewById(R.id.spoiler_button);
|
||||
mediaIcon=findViewById(R.id.media_icon);
|
||||
|
||||
button.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
button.setClipToOutline(true);
|
||||
@@ -94,7 +98,17 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
|
||||
}else{
|
||||
title.setText(item.parsedTitle);
|
||||
}
|
||||
action.setText(item.status.spoilerRevealed ? R.string.spoiler_hide : R.string.spoiler_show);
|
||||
action.setText(item.status.spoilerRevealed ? R.string.spoiler_hide : R.string.sk_spoiler_show);
|
||||
itemView.setPadding(
|
||||
itemView.getPaddingLeft(),
|
||||
itemView.getPaddingTop(),
|
||||
itemView.getPaddingRight(),
|
||||
item.inset ? itemView.getPaddingTop() : 0
|
||||
);
|
||||
mediaIcon.setVisibility(item.attachmentCount > 0 ? View.VISIBLE : View.GONE);
|
||||
mediaIcon.setImageResource(item.attachmentCount > 1
|
||||
? R.drawable.ic_fluent_image_multiple_24_regular
|
||||
: R.drawable.ic_fluent_image_24_regular);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,54 +1,111 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import static org.joinmastodon.android.api.session.AccountLocalPreferences.ShowEmojiReactions.ALWAYS;
|
||||
import static org.joinmastodon.android.api.session.AccountLocalPreferences.ShowEmojiReactions.ONLY_OPENED;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.HomeTabFragment;
|
||||
import org.joinmastodon.android.fragments.ListTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class StatusDisplayItem{
|
||||
public final String parentID;
|
||||
public final BaseStatusListFragment parentFragment;
|
||||
public final BaseStatusListFragment<?> parentFragment;
|
||||
public Status status;
|
||||
public boolean inset;
|
||||
public int index;
|
||||
public boolean
|
||||
hasDescendantNeighbor=false,
|
||||
hasAncestoringNeighbor=false,
|
||||
isMainStatus=true,
|
||||
isDirectDescendant=false,
|
||||
isForQuote=false;
|
||||
|
||||
public static final int FLAG_INSET=1;
|
||||
public static final int FLAG_NO_FOOTER=1 << 1;
|
||||
public static final int FLAG_CHECKABLE=1 << 2;
|
||||
public static final int FLAG_MEDIA_FORCE_HIDDEN=1 << 3;
|
||||
public static final int FLAG_NO_HEADER=1 << 4;
|
||||
public static final int FLAG_NO_TRANSLATE=1 << 5;
|
||||
public static final int FLAG_NO_EMOJI_REACTIONS=1 << 6;
|
||||
public static final int FLAG_IS_FOR_QUOTE=1 << 7;
|
||||
public static final int FLAG_NO_MEDIA_PREVIEW=1 << 8;
|
||||
|
||||
public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
|
||||
public void setAncestryInfo(
|
||||
boolean hasDescendantNeighbor,
|
||||
boolean hasAncestoringNeighbor,
|
||||
boolean isMainStatus,
|
||||
boolean isDirectDescendant
|
||||
) {
|
||||
this.hasDescendantNeighbor = hasDescendantNeighbor;
|
||||
this.hasAncestoringNeighbor = hasAncestoringNeighbor;
|
||||
this.isMainStatus = isMainStatus;
|
||||
this.isDirectDescendant = isDirectDescendant;
|
||||
}
|
||||
|
||||
public StatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment){
|
||||
this.parentID=parentID;
|
||||
this.parentFragment=parentFragment;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getContentStatusID(){
|
||||
if(parentFragment instanceof StatusListFragment slf){
|
||||
Status s=slf.getContentStatusByID(parentID);
|
||||
return s!=null ? s.id : parentID;
|
||||
}else{
|
||||
return parentID;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Type getType();
|
||||
|
||||
public int getImageCount(){
|
||||
@@ -70,127 +127,254 @@ public abstract class StatusDisplayItem{
|
||||
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
|
||||
case CARD_LARGE -> new LinkCardStatusDisplayItem.Holder(activity, parent, true);
|
||||
case CARD_COMPACT -> new LinkCardStatusDisplayItem.Holder(activity, parent, false);
|
||||
case EMOJI_REACTIONS -> new EmojiReactionsStatusDisplayItem.Holder(activity, parent);
|
||||
case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent);
|
||||
case ACCOUNT_CARD -> new AccountCardStatusDisplayItem.Holder(activity, parent);
|
||||
case ACCOUNT -> new AccountStatusDisplayItem.Holder(new AccountViewHolder(parentFragment, parent, null));
|
||||
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
|
||||
case GAP -> new GapStatusDisplayItem.Holder(activity, parent);
|
||||
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
|
||||
case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent);
|
||||
case PREVIEWLESS_MEDIA_GRID -> new PreviewlessMediaGridStatusDisplayItem.Holder(activity, parent);
|
||||
case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent);
|
||||
case FILE -> new FileStatusDisplayItem.Holder(activity, parent);
|
||||
case SPOILER, FILTER_SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent, type);
|
||||
case SECTION_HEADER -> new SectionHeaderStatusDisplayItem.Holder(activity, parent);
|
||||
case SECTION_HEADER -> null; // new SectionHeaderStatusDisplayItem.Holder(activity, parent);
|
||||
case NOTIFICATION_HEADER -> new NotificationHeaderStatusDisplayItem.Holder(activity, parent);
|
||||
case DUMMY -> new DummyStatusDisplayItem.Holder(activity);
|
||||
};
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter){
|
||||
int flags=0;
|
||||
if(inset)
|
||||
flags|=FLAG_INSET;
|
||||
if(!addFooter)
|
||||
flags|=FLAG_NO_FOOTER;
|
||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, flags);
|
||||
public static ReblogOrReplyLineStatusDisplayItem buildReplyLine(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parent, Account account, boolean threadReply) {
|
||||
String parentID = parent.getID();
|
||||
String text = threadReply ? fragment.getString(R.string.sk_show_thread)
|
||||
: account == null ? fragment.getString(R.string.sk_in_reply)
|
||||
: status.reblog != null ? account.getDisplayName()
|
||||
: fragment.getString(R.string.in_reply_to, account.getDisplayName());
|
||||
String fullText = threadReply ? fragment.getString(R.string.sk_show_thread)
|
||||
: account == null ? fragment.getString(R.string.sk_in_reply)
|
||||
: fragment.getString(R.string.in_reply_to, account.getDisplayName());
|
||||
return new ReblogOrReplyLineStatusDisplayItem(
|
||||
parentID, fragment, text, account == null ? List.of() : account.emojis,
|
||||
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText, status
|
||||
);
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, int flags){
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, FilterContext filterContext, int flags){
|
||||
String parentID=parentObject.getID();
|
||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||
Status statusForContent=status.getContentStatus();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus s ? s : null;
|
||||
|
||||
// Hide statuses that have a filter action of hide
|
||||
if(!new StatusFilterPredicate(accountID, filterContext, FilterAction.HIDE).test(status))
|
||||
return new ArrayList<StatusDisplayItem>() ;
|
||||
|
||||
HeaderStatusDisplayItem header=null;
|
||||
boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts;
|
||||
|
||||
if((flags & FLAG_NO_HEADER)==0){
|
||||
if(status.reblog!=null){
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_repeat_20px));
|
||||
}else if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)){
|
||||
Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId));
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_reply_20px));
|
||||
ReblogOrReplyLineStatusDisplayItem replyLine = null;
|
||||
boolean threadReply = statusForContent.inReplyToAccountId != null &&
|
||||
statusForContent.inReplyToAccountId.equals(statusForContent.account.id);
|
||||
|
||||
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
|
||||
Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
|
||||
replyLine = buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
|
||||
}
|
||||
|
||||
if(status.reblog!=null){
|
||||
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
||||
|
||||
statusForContent.rebloggedBy = status.account;
|
||||
|
||||
String text=fragment.getString(R.string.user_boosted, status.account.getDisplayName());
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
|
||||
args.putParcelable("profileAccount", Parcels.wrap(status.account));
|
||||
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
||||
}, null, status));
|
||||
} else if (!(status.tags.isEmpty() ||
|
||||
fragment instanceof HashtagTimelineFragment ||
|
||||
fragment instanceof ListTimelineFragment
|
||||
) && fragment.getParentFragment() instanceof HomeTabFragment home) {
|
||||
home.getHashtags().stream()
|
||||
.filter(followed -> status.tags.stream()
|
||||
.anyMatch(hashtag -> followed.name.equalsIgnoreCase(hashtag.name)))
|
||||
.findAny()
|
||||
// post contains a hashtag the user is following
|
||||
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
|
||||
parentID, fragment, hashtag.name, List.of(),
|
||||
R.drawable.ic_fluent_number_symbol_20sp_filled, null,
|
||||
i->UiUtils.openHashtagTimeline(fragment.getActivity(), accountID, hashtag),
|
||||
status
|
||||
)));
|
||||
}
|
||||
|
||||
if (replyLine != null) {
|
||||
Optional<ReblogOrReplyLineStatusDisplayItem> primaryLine = items.stream()
|
||||
.filter(i -> i instanceof ReblogOrReplyLineStatusDisplayItem)
|
||||
.map(ReblogOrReplyLineStatusDisplayItem.class::cast)
|
||||
.findFirst();
|
||||
|
||||
if (primaryLine.isPresent()) {
|
||||
primaryLine.get().extra = replyLine;
|
||||
} else {
|
||||
items.add(replyLine);
|
||||
}
|
||||
}
|
||||
|
||||
if((flags & FLAG_CHECKABLE)!=0)
|
||||
items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
|
||||
else
|
||||
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
|
||||
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, parentObject instanceof Notification n ? n : null, scheduledStatus));
|
||||
}
|
||||
|
||||
boolean filtered=false;
|
||||
LegacyFilter applyingFilter=null;
|
||||
if(status.filtered!=null){
|
||||
for(FilterResult filter:status.filtered){
|
||||
if(filter.filter.isActive()){
|
||||
filtered=true;
|
||||
LegacyFilter f=filter.filter;
|
||||
if(f.isActive() && filterContext != null && f.context.contains(filterContext)){
|
||||
applyingFilter=f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<StatusDisplayItem> contentItems;
|
||||
if(filtered){
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, fragment.getString(R.string.post_matches_filter_x, status.filtered.get(0).filter.title), status, statusForContent, Type.FILTER_SPOILER);
|
||||
if(statusForContent.hasSpoiler()){
|
||||
if (AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed = true;
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER);
|
||||
if((flags & FLAG_IS_FOR_QUOTE)!=0){
|
||||
for(StatusDisplayItem item:spoilerItem.contentItems){
|
||||
item.isForQuote=true;
|
||||
}
|
||||
}
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
status.spoilerRevealed=false;
|
||||
}else if(!TextUtils.isEmpty(statusForContent.spoilerText)){
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, status, statusForContent, Type.SPOILER);
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
status.spoilerRevealed=!AccountSessionManager.get(accountID).getLocalPreferences().showCWs;
|
||||
}else{
|
||||
contentItems=items;
|
||||
}
|
||||
|
||||
if(statusForContent.quote!=null) {
|
||||
int quoteInlineIndex=statusForContent.content.lastIndexOf("<span class=\"quote-inline\"><br/><br/>RE:");
|
||||
if (quoteInlineIndex!=-1)
|
||||
statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex);
|
||||
}
|
||||
|
||||
boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText);
|
||||
if(!TextUtils.isEmpty(statusForContent.content)){
|
||||
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, statusForContent);
|
||||
if(filtered){
|
||||
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
|
||||
}
|
||||
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, parsedText, fragment, statusForContent);
|
||||
text.reduceTopPadding=header==null;
|
||||
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext());
|
||||
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
|
||||
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext()), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0);
|
||||
contentItems.add(text);
|
||||
}else if(header!=null){
|
||||
}else if(!hasSpoiler && header!=null){
|
||||
header.needBottomPadding=true;
|
||||
}else if(hasSpoiler){
|
||||
contentItems.add(new DummyStatusDisplayItem(parentID, fragment));
|
||||
}
|
||||
|
||||
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
|
||||
if(!imageAttachments.isEmpty()){
|
||||
if(!imageAttachments.isEmpty() && (flags & FLAG_NO_MEDIA_PREVIEW)==0){
|
||||
int color = UiUtils.getThemeColor(fragment.getContext(), R.attr.colorM3SurfaceVariant);
|
||||
for (Attachment att : imageAttachments) {
|
||||
if (att.blurhashPlaceholder == null) {
|
||||
att.blurhashPlaceholder = new ColorDrawable(color);
|
||||
}
|
||||
}
|
||||
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
||||
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
|
||||
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0)
|
||||
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){
|
||||
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
|
||||
else if(statusForContent.sensitive && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
|
||||
mediaGrid.sensitiveRevealed=true;
|
||||
statusForContent.sensitiveRevealed=false;
|
||||
statusForContent.sensitive=true;
|
||||
} else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
|
||||
statusForContent.sensitiveRevealed=true;
|
||||
contentItems.add(mediaGrid);
|
||||
}
|
||||
if((flags & FLAG_NO_MEDIA_PREVIEW)!=0){
|
||||
contentItems.add(new PreviewlessMediaGridStatusDisplayItem(parentID, fragment, null, imageAttachments, statusForContent));
|
||||
|
||||
}
|
||||
for(Attachment att:statusForContent.mediaAttachments){
|
||||
if(att.type==Attachment.Type.AUDIO){
|
||||
contentItems.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
|
||||
}
|
||||
if(att.type==Attachment.Type.UNKNOWN){
|
||||
contentItems.add(new FileStatusDisplayItem(parentID, fragment, att));
|
||||
}
|
||||
}
|
||||
if(statusForContent.poll!=null){
|
||||
buildPollItems(parentID, fragment, statusForContent.poll, status, contentItems);
|
||||
}
|
||||
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && TextUtils.isEmpty(statusForContent.spoilerText)){
|
||||
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
|
||||
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && statusForContent.quote==null && !statusForContent.card.isHashtagUrl(statusForContent.url)){
|
||||
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent, (flags & FLAG_NO_MEDIA_PREVIEW)==0));
|
||||
}
|
||||
if(contentItems!=items && status.spoilerRevealed){
|
||||
if(statusForContent.quote!=null && !(parentObject instanceof Notification)){
|
||||
if(!statusForContent.mediaAttachments.isEmpty() && statusForContent.poll==null) // add spacing if immediately preceded by attachment
|
||||
contentItems.add(new DummyStatusDisplayItem(parentID, fragment));
|
||||
contentItems.addAll(buildItems(fragment, statusForContent.quote, accountID, parentObject, knownAccounts, filterContext, FLAG_NO_FOOTER | FLAG_INSET | FLAG_NO_EMOJI_REACTIONS | FLAG_IS_FOR_QUOTE));
|
||||
}
|
||||
if(contentItems!=items && statusForContent.spoilerRevealed){
|
||||
items.addAll(contentItems);
|
||||
}
|
||||
AccountLocalPreferences lp=fragment.getLocalPrefs();
|
||||
if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && !status.preview && lp.emojiReactionsEnabled &&
|
||||
(lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment) &&
|
||||
statusForContent.reactions!=null){
|
||||
boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id);
|
||||
boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus;
|
||||
items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false));
|
||||
}
|
||||
FooterStatusDisplayItem footer=null;
|
||||
if((flags & FLAG_NO_FOOTER)==0){
|
||||
FooterStatusDisplayItem footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID);
|
||||
footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID);
|
||||
footer.hideCounts=hideCounts;
|
||||
items.add(footer);
|
||||
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
|
||||
items.add(new GapStatusDisplayItem(parentID, fragment));
|
||||
}
|
||||
int i=1;
|
||||
boolean inset=(flags & FLAG_INSET)!=0;
|
||||
boolean isForQuote=(flags & FLAG_IS_FOR_QUOTE)!=0;
|
||||
// add inset dummy so last content item doesn't clip out of inset bounds
|
||||
if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0 && !isForQuote){
|
||||
items.add(new DummyStatusDisplayItem(parentID, fragment));
|
||||
// in case we ever need the dummy to display a margin for the media grid again:
|
||||
// (i forgot why we apparently don't need this anymore)
|
||||
// !contentItems.isEmpty() && contentItems
|
||||
// .get(contentItems.size() - 1) instanceof MediaGridStatusDisplayItem));
|
||||
}
|
||||
GapStatusDisplayItem gap=null;
|
||||
if((flags & FLAG_NO_FOOTER)==0 && status.hasGapAfter!=null && !(fragment instanceof ThreadFragment))
|
||||
items.add(gap=new GapStatusDisplayItem(parentID, fragment, status));
|
||||
int i=1;
|
||||
for(StatusDisplayItem item:items){
|
||||
item.inset=inset;
|
||||
if(inset)
|
||||
item.inset=true;
|
||||
if(isForQuote){
|
||||
item.status=statusForContent;
|
||||
item.isForQuote=true;
|
||||
}
|
||||
item.index=i++;
|
||||
}
|
||||
if(items!=contentItems){
|
||||
if(items!=contentItems && !statusForContent.spoilerRevealed){
|
||||
for(StatusDisplayItem item:contentItems){
|
||||
item.inset=inset;
|
||||
if(inset)
|
||||
item.inset=true;
|
||||
if(isForQuote){
|
||||
item.status=statusForContent;
|
||||
item.isForQuote=true;
|
||||
}
|
||||
item.index=i++;
|
||||
}
|
||||
}
|
||||
return items;
|
||||
|
||||
List<StatusDisplayItem> nonGapItems=gap!=null ? items.subList(0, items.size()-1) : items;
|
||||
WarningFilteredStatusDisplayItem warning=applyingFilter==null ? null :
|
||||
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, nonGapItems, applyingFilter);
|
||||
return applyingFilter==null ? items : new ArrayList<>(gap!=null
|
||||
? List.of(warning, gap)
|
||||
: Collections.singletonList(warning)
|
||||
);
|
||||
}
|
||||
|
||||
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, Status status, List<StatusDisplayItem> items){
|
||||
@@ -199,7 +383,7 @@ public abstract class StatusDisplayItem{
|
||||
items.add(new PollOptionStatusDisplayItem(parentID, poll, i, fragment, status));
|
||||
i++;
|
||||
}
|
||||
items.add(new PollFooterStatusDisplayItem(parentID, fragment, poll));
|
||||
items.add(new PollFooterStatusDisplayItem(parentID, fragment, poll, status));
|
||||
}
|
||||
|
||||
public enum Type{
|
||||
@@ -211,26 +395,35 @@ public abstract class StatusDisplayItem{
|
||||
POLL_FOOTER,
|
||||
CARD_LARGE,
|
||||
CARD_COMPACT,
|
||||
EMOJI_REACTIONS,
|
||||
FOOTER,
|
||||
ACCOUNT_CARD,
|
||||
ACCOUNT,
|
||||
HASHTAG,
|
||||
GAP,
|
||||
EXTENDED_FOOTER,
|
||||
MEDIA_GRID,
|
||||
PREVIEWLESS_MEDIA_GRID,
|
||||
WARNING,
|
||||
FILE,
|
||||
SPOILER,
|
||||
SECTION_HEADER,
|
||||
HEADER_CHECKABLE,
|
||||
NOTIFICATION_HEADER,
|
||||
FILTER_SPOILER
|
||||
FILTER_SPOILER,
|
||||
DUMMY
|
||||
}
|
||||
|
||||
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
||||
private Context context;
|
||||
|
||||
public Holder(View itemView){
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
public Holder(Context context, int layout, ViewGroup parent){
|
||||
super(context, layout, parent);
|
||||
this.context=context;
|
||||
}
|
||||
|
||||
public String getItemID(){
|
||||
@@ -239,12 +432,54 @@ public abstract class StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
if(item.isForQuote){
|
||||
item.status.filterRevealed=true;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("status", Parcels.wrap(item.status.clone()));
|
||||
args.putBoolean("refresh", true);
|
||||
Nav.go((Activity) context, ThreadFragment.class, args);
|
||||
return;
|
||||
}
|
||||
|
||||
item.parentFragment.onItemClick(item.parentID);
|
||||
}
|
||||
|
||||
public Optional<StatusDisplayItem> getNextVisibleDisplayItem(){
|
||||
return getNextVisibleDisplayItem(null);
|
||||
}
|
||||
public Optional<StatusDisplayItem> getNextVisibleDisplayItem(Predicate<StatusDisplayItem> predicate){
|
||||
Optional<StatusDisplayItem> next=getNextDisplayItem();
|
||||
for(int offset=1; next.isPresent(); next=getDisplayItemOffset(++offset)){
|
||||
boolean isHidden=next.map(n->(n instanceof EmojiReactionsStatusDisplayItem e && e.isHidden())
|
||||
|| (n instanceof DummyStatusDisplayItem)).orElse(false);
|
||||
if(!isHidden && (predicate==null || predicate.test(next.get()))) return next;
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Optional<StatusDisplayItem> getNextDisplayItem(){
|
||||
return getDisplayItemOffset(1);
|
||||
}
|
||||
|
||||
public Optional<StatusDisplayItem> getDisplayItemOffset(int offset){
|
||||
List<StatusDisplayItem> displayItems=item.parentFragment.getDisplayItems();
|
||||
int thisPos=displayItems.indexOf(item);
|
||||
int offsetPos=thisPos + offset;
|
||||
return displayItems.size() > offsetPos && thisPos >= 0 && offsetPos >= 0
|
||||
? Optional.of(displayItems.get(offsetPos))
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
public boolean isLastDisplayItemForStatus(){
|
||||
return getNextVisibleDisplayItem()
|
||||
.map(next->!next.parentID.equals(item.parentID) || item.inset && !next.inset)
|
||||
.orElse(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return item.parentFragment.isItemEnabled(item.parentID);
|
||||
return item.parentFragment.isItemEnabled(item.parentID) || item.isForQuote;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,27 +3,35 @@ package org.joinmastodon.android.ui.displayitems;
|
||||
import android.app.Activity;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewStub;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.Translation;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.MovieDrawable;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
@@ -33,12 +41,13 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
private CustomEmojiHelper translationEmojiHelper=new CustomEmojiHelper();
|
||||
public boolean textSelectable;
|
||||
public boolean reduceTopPadding;
|
||||
public final Status status;
|
||||
public boolean disableTranslate;
|
||||
|
||||
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status){
|
||||
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status, boolean disableTranslate){
|
||||
super(parentID, parentFragment);
|
||||
this.text=text;
|
||||
this.status=status;
|
||||
this.disableTranslate=disableTranslate;
|
||||
emojiHelper.setText(text);
|
||||
}
|
||||
|
||||
@@ -70,15 +79,29 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
public static class Holder extends StatusDisplayItem.Holder<TextStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final LinkedTextView text;
|
||||
private final ViewStub translationFooterStub;
|
||||
private View translationFooter;
|
||||
private View translationFooter, translationButtonWrap;
|
||||
private TextView translationInfo;
|
||||
private Button translationShowOriginal;
|
||||
private Button translationButton;
|
||||
private ProgressBar translationProgress;
|
||||
|
||||
private final float textMaxHeight;
|
||||
private final LinearLayout.LayoutParams collapseParams, wrapParams;
|
||||
private final ViewGroup parent;
|
||||
private final TextView readMore;
|
||||
private final ScrollView textScrollView;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_text, parent);
|
||||
this.parent=parent;
|
||||
text=findViewById(R.id.text);
|
||||
translationFooterStub=findViewById(R.id.translation_info);
|
||||
textScrollView=findViewById(R.id.text_scroll_view);
|
||||
readMore=findViewById(R.id.read_more);
|
||||
textMaxHeight=activity.getResources().getDimension(R.dimen.text_max_height);
|
||||
float textCollapsedHeight=activity.getResources().getDimension(R.dimen.text_collapsed_height);
|
||||
collapseParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, (int) textCollapsedHeight);
|
||||
wrapParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
readMore.setOnClickListener(v -> item.parentFragment.onToggleExpanded(item.status, getItemID()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,12 +114,60 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
}else{
|
||||
text.setText(item.text);
|
||||
}
|
||||
text.setTextIsSelectable(item.textSelectable);
|
||||
text.setTextIsSelectable(false);
|
||||
if(item.textSelectable && !item.isForQuote) itemView.post(() -> text.setTextIsSelectable(true));
|
||||
text.setInvalidateOnEveryFrame(false);
|
||||
itemView.setClickable(false);
|
||||
text.setPadding(text.getPaddingLeft(), item.reduceTopPadding ? V.dp(8) : V.dp(16), text.getPaddingRight(), text.getPaddingBottom());
|
||||
itemView.setPadding(itemView.getPaddingLeft(), item.reduceTopPadding ? V.dp(6) : V.dp(12), itemView.getPaddingRight(), itemView.getPaddingBottom());
|
||||
text.setTextColor(UiUtils.getThemeColor(text.getContext(), item.inset ? R.attr.colorM3OnSurfaceVariant : R.attr.colorM3OnSurface));
|
||||
updateTranslation(false);
|
||||
|
||||
readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
|
||||
|
||||
StatusDisplayItem next=getNextVisibleDisplayItem().orElse(null);
|
||||
if(next!=null && !next.parentID.equals(item.parentID)) next=null;
|
||||
int bottomPadding=item.inset ? V.dp(12)
|
||||
: next instanceof FooterStatusDisplayItem ? V.dp(6)
|
||||
: (next instanceof EmojiReactionsStatusDisplayItem || next==null) ? 0
|
||||
: V.dp(12);
|
||||
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding);
|
||||
|
||||
if (!GlobalUserPreferences.collapseLongPosts) {
|
||||
textScrollView.setLayoutParams(wrapParams);
|
||||
readMore.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// incredibly ugly workaround for https://github.com/sk22/megalodon/issues/520
|
||||
// i am so, so sorry. FIXME
|
||||
// attempts to use OnPreDrawListener, OnGlobalLayoutListener and .post have failed -
|
||||
// the view didn't want to reliably update after calling .setVisibility etc :(
|
||||
int width = parent.getWidth() != 0 ? parent.getWidth()
|
||||
: item.parentFragment.getView().getWidth() != 0
|
||||
? item.parentFragment.getView().getWidth()
|
||||
: item.parentFragment.getParentFragment() != null && item.parentFragment.getParentFragment().getView().getWidth() != 0
|
||||
? item.parentFragment.getParentFragment().getView().getWidth() // YIKES
|
||||
: UiUtils.MAX_WIDTH;
|
||||
|
||||
text.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
|
||||
|
||||
if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) {
|
||||
boolean tooBig = text.getMeasuredHeight() > textMaxHeight;
|
||||
boolean expandable = tooBig && !item.status.hasSpoiler();
|
||||
item.parentFragment.onEnableExpandable(Holder.this, expandable);
|
||||
}
|
||||
|
||||
boolean expandButtonShown=item.status.textExpandable && !item.status.textExpanded;
|
||||
if(translationFooter!=null)
|
||||
translationFooter.setPadding(0, V.dp(expandButtonShown ? 0 : 4), 0, 0);
|
||||
readMore.setVisibility(expandButtonShown ? View.VISIBLE : View.GONE);
|
||||
textScrollView.setLayoutParams(item.status.textExpandable && !item.status.textExpanded ? collapseParams : wrapParams);
|
||||
|
||||
// compensate for spoiler's bottom margin
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
|
||||
params.setMargins(params.leftMargin, item.inset && item.status.hasSpoiler() ? V.dp(-16) : 0,
|
||||
params.rightMargin, params.bottomMargin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -123,27 +194,45 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
public void updateTranslation(boolean updateText){
|
||||
if(item.status==null)
|
||||
return;
|
||||
boolean translateEnabled=!item.disableTranslate && item.status.isEligibleForTranslation(item.parentFragment.getSession()) && !item.isForQuote;
|
||||
if(translationFooter==null && translateEnabled){
|
||||
translationFooter=translationFooterStub.inflate();
|
||||
translationInfo=findViewById(R.id.translation_info_text);
|
||||
translationButton=findViewById(R.id.translation_btn);
|
||||
translationButtonWrap=findViewById(R.id.translation_btn_wrap);
|
||||
translationProgress=findViewById(R.id.translation_progress);
|
||||
translationButton.setOnClickListener(v->item.parentFragment.togglePostTranslation(item.status, item.parentID));
|
||||
}
|
||||
if(translationButton!=null) translationButton.animate().cancel();
|
||||
if(item.status.translationState==Status.TranslationState.HIDDEN){
|
||||
if(translationFooter!=null)
|
||||
translationFooter.setVisibility(View.GONE);
|
||||
if(updateText){
|
||||
text.setText(item.text);
|
||||
}
|
||||
if(updateText) text.setText(item.text);
|
||||
if(translationFooter==null) return;
|
||||
translationFooter.setVisibility(translateEnabled ? View.VISIBLE : View.GONE);
|
||||
translationProgress.setVisibility(View.GONE);
|
||||
Translation existingTrans=item.status.getContentStatus().translation;
|
||||
String existingTransLang=existingTrans!=null ? existingTrans.detectedSourceLanguage : null;
|
||||
String lang=existingTransLang!=null ? existingTransLang : item.status.getContentStatus().language;
|
||||
Locale locale=lang!=null ? Locale.forLanguageTag(lang) : null;
|
||||
String displayLang=locale==null || locale.getDisplayLanguage().isBlank() ? lang : locale.getDisplayLanguage();
|
||||
translationButton.setText(displayLang!=null
|
||||
? item.parentFragment.getString(R.string.translate_post, displayLang)
|
||||
: item.parentFragment.getString(R.string.sk_translate_post));
|
||||
translationButton.setClickable(true);
|
||||
translationButton.animate().alpha(1).setDuration(100).start();
|
||||
translationInfo.setVisibility(View.GONE);
|
||||
UiUtils.beginLayoutTransition((ViewGroup) translationButtonWrap);
|
||||
}else{
|
||||
if(translationFooter==null){
|
||||
translationFooter=translationFooterStub.inflate();
|
||||
translationInfo=findViewById(R.id.translation_info_text);
|
||||
translationShowOriginal=findViewById(R.id.translation_show_original);
|
||||
translationProgress=findViewById(R.id.translation_progress);
|
||||
translationShowOriginal.setOnClickListener(v->item.parentFragment.togglePostTranslation(item.status, item.parentID));
|
||||
}else{
|
||||
translationFooter.setVisibility(View.VISIBLE);
|
||||
}
|
||||
translationFooter.setVisibility(View.VISIBLE);
|
||||
if(item.status.translationState==Status.TranslationState.SHOWN){
|
||||
translationProgress.setVisibility(View.GONE);
|
||||
translationButton.setText(R.string.translation_show_original);
|
||||
translationButton.setClickable(true);
|
||||
translationButton.animate().alpha(1).setDuration(200).start();
|
||||
translationInfo.setVisibility(View.VISIBLE);
|
||||
translationShowOriginal.setVisibility(View.VISIBLE);
|
||||
translationInfo.setText(translationInfo.getContext().getString(R.string.post_translated, Locale.forLanguageTag(item.status.translation.detectedSourceLanguage).getDisplayLanguage(), item.status.translation.provider));
|
||||
translationButton.setVisibility(View.VISIBLE);
|
||||
String displayLang=Locale.forLanguageTag(item.status.translation.detectedSourceLanguage).getDisplayLanguage();
|
||||
translationInfo.setText(translationInfo.getContext().getString(R.string.post_translated, !displayLang.isBlank() ? displayLang : item.status.translation.detectedSourceLanguage, item.status.translation.provider));
|
||||
UiUtils.beginLayoutTransition((ViewGroup) translationButtonWrap);
|
||||
if(updateText){
|
||||
if(item.translatedText==null){
|
||||
item.setTranslatedText(item.status.translation.content);
|
||||
@@ -152,8 +241,10 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
}else{ // LOADING
|
||||
translationProgress.setVisibility(View.VISIBLE);
|
||||
translationButton.setClickable(false);
|
||||
translationButton.animate().alpha(UiUtils.ALPHA_PRESSED).setStartDelay(50).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
translationInfo.setVisibility(View.INVISIBLE);
|
||||
translationShowOriginal.setVisibility(View.INVISIBLE);
|
||||
UiUtils.beginLayoutTransition((ViewGroup) translationButton.getParent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.AltTextFilter;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// Mind the gap!
|
||||
public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
|
||||
public boolean loading;
|
||||
public List<StatusDisplayItem> filteredItems;
|
||||
public LegacyFilter applyingFilter;
|
||||
|
||||
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, List<StatusDisplayItem> filteredItems, LegacyFilter applyingFilter){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
this.filteredItems = filteredItems;
|
||||
this.applyingFilter = applyingFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.WARNING;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<WarningFilteredStatusDisplayItem>{
|
||||
public final View warningWrap;
|
||||
public final Button showBtn;
|
||||
public final TextView text;
|
||||
public List<StatusDisplayItem> filteredItems;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_warning, parent);
|
||||
warningWrap=findViewById(R.id.warning_wrap);
|
||||
showBtn=findViewById(R.id.reveal_btn);
|
||||
showBtn.setOnClickListener(i -> item.parentFragment.onWarningClick(this));
|
||||
itemView.setOnClickListener(v->item.parentFragment.onWarningClick(this));
|
||||
text=findViewById(R.id.text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(WarningFilteredStatusDisplayItem item) {
|
||||
filteredItems = item.filteredItems;
|
||||
String title = item.applyingFilter instanceof AltTextFilter ? item.parentFragment.getString(R.string.sk_no_alt_text) : item.applyingFilter.title;
|
||||
text.setText(item.parentFragment.getString(R.string.sk_filtered, title));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,9 @@ import me.grishka.appkit.utils.V;
|
||||
public class SawtoothTearDrawable extends Drawable{
|
||||
private final Paint topPaint, bottomPaint;
|
||||
|
||||
private static final int TOP_SAWTOOTH_HEIGHT=5;
|
||||
private static final int BOTTOM_SAWTOOTH_HEIGHT=3;
|
||||
private static final int STROKE_WIDTH=2;
|
||||
private static final int TOP_SAWTOOTH_HEIGHT=4;
|
||||
private static final int BOTTOM_SAWTOOTH_HEIGHT=4;
|
||||
private static final int STROKE_WIDTH=1;
|
||||
private static final int SAWTOOTH_PERIOD=14;
|
||||
|
||||
public SawtoothTearDrawable(Context context){
|
||||
|
||||
@@ -52,18 +52,22 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.ui.ImageDescriptionSheet;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.utils.FileProvider;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -81,6 +85,9 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okio.BufferedSink;
|
||||
import okio.Okio;
|
||||
import okio.Sink;
|
||||
@@ -110,6 +117,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
private TextView videoTimeView;
|
||||
private ImageButton videoPlayPauseButton;
|
||||
private View videoControls;
|
||||
private MenuItem imageDescriptionButton;
|
||||
private boolean uiVisible=true;
|
||||
private AudioManager.OnAudioFocusChangeListener audioFocusListener=this::onAudioFocusChanged;
|
||||
private Runnable uiAutoHider=()->{
|
||||
@@ -172,7 +180,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
toolbarWrap.setPadding(0, 0, 0, 0);
|
||||
videoControls.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
insets=insets.replaceSystemWindowInsets(tappable.left, tappable.top, tappable.right, tappable.bottom);
|
||||
insets=insets.replaceSystemWindowInsets(tappable.left, tappable.top, tappable.right, insets.getSystemWindowInsetBottom());
|
||||
}
|
||||
uiOverlay.dispatchApplyWindowInsets(insets);
|
||||
int bottomInset=insets.getSystemWindowInsetBottom();
|
||||
@@ -202,10 +210,36 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
toolbarWrap=uiOverlay.findViewById(R.id.toolbar_wrap);
|
||||
toolbar=uiOverlay.findViewById(R.id.toolbar);
|
||||
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
|
||||
imageDescriptionButton = toolbar.getMenu()
|
||||
.add(R.string.sk_image_description)
|
||||
.setIcon(R.drawable.ic_fluent_image_alt_text_24_regular)
|
||||
.setVisible(attachments.get(pager.getCurrentItem()).description != null
|
||||
&& !attachments.get(pager.getCurrentItem()).description.isEmpty())
|
||||
.setOnMenuItemClickListener(item -> {
|
||||
new ImageDescriptionSheet(activity,attachments.get(pager.getCurrentItem())).show();
|
||||
return true;
|
||||
});
|
||||
imageDescriptionButton.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
toolbar.getMenu()
|
||||
.add(R.string.download)
|
||||
.setIcon(R.drawable.ic_fluent_arrow_download_24_regular)
|
||||
.setOnMenuItemClickListener(item -> {
|
||||
saveCurrentFile();
|
||||
return true;
|
||||
})
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
toolbar.getMenu()
|
||||
.add(R.string.button_share)
|
||||
.setIcon(R.drawable.ic_fluent_share_24_regular)
|
||||
.setOnMenuItemClickListener(item -> {
|
||||
shareCurrentFile();
|
||||
return true;
|
||||
})
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
if(status!=null)
|
||||
toolbar.getMenu().add(R.string.info).setIcon(R.drawable.ic_info_24px).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
toolbar.getMenu().add(R.string.info).setIcon(R.drawable.ic_fluent_info_24_regular).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
else
|
||||
toolbar.getMenu().add(R.string.download).setIcon(R.drawable.ic_download_24px).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
toolbar.getMenu().add(R.string.download).setIcon(R.drawable.ic_fluent_arrow_download_24_regular).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
toolbar.setOnMenuItemClickListener(item->{
|
||||
if(status!=null)
|
||||
showInfoSheet();
|
||||
@@ -412,6 +446,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
private void onPageChanged(int index){
|
||||
currentIndex=index;
|
||||
Attachment att=attachments.get(index);
|
||||
imageDescriptionButton.setVisible(att.description != null && !att.description.isEmpty());
|
||||
V.setVisibilityAnimated(videoControls, att.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
|
||||
if(att.type==Attachment.Type.VIDEO){
|
||||
videoSeekBar.setSecondaryProgress(0);
|
||||
@@ -436,7 +471,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
||||
wlp.flags|=WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
||||
wm.updateViewLayout(windowView, wlp);
|
||||
activity.getSystemService(AudioManager.class).requestAudioFocus(audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
||||
int audiofocus = GlobalUserPreferences.overlayMedia ? AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK : AudioManager.AUDIOFOCUS_GAIN;
|
||||
activity.getSystemService(AudioManager.class).requestAudioFocus(audioFocusListener, AudioManager.STREAM_MUSIC, audiofocus);
|
||||
}
|
||||
screenOnRefCount++;
|
||||
}
|
||||
@@ -457,6 +493,44 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
pauseVideo();
|
||||
}
|
||||
|
||||
private void shareCurrentFile(){
|
||||
Attachment att=attachments.get(pager.getCurrentItem());
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
|
||||
if(att.type==Attachment.Type.IMAGE){
|
||||
UrlImageLoaderRequest req=new UrlImageLoaderRequest(att.url);
|
||||
try{
|
||||
File file=ImageCache.getInstance(activity).getFile(req);
|
||||
if(file==null){
|
||||
shareAfterDownloading(att);
|
||||
return;
|
||||
}
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
File imageDir = new File(activity.getCacheDir(), ".");
|
||||
File renamedFile;
|
||||
file.renameTo(renamedFile = new File(imageDir, Uri.parse(att.url).getLastPathSegment()));
|
||||
Uri outputUri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileprovider", renamedFile);
|
||||
|
||||
// setting type to image
|
||||
intent.setType(mimeTypeForFileName(outputUri.getLastPathSegment()));
|
||||
|
||||
intent.putExtra(Intent.EXTRA_STREAM, outputUri);
|
||||
|
||||
// calling startactivity() to share
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.button_share)));
|
||||
|
||||
});
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "shareCurrentFile: ", x);
|
||||
Toast.makeText(activity, R.string.error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}else{
|
||||
shareAfterDownloading(att);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void saveCurrentFile(){
|
||||
if(Build.VERSION.SDK_INT>=29){
|
||||
doSaveCurrentFile();
|
||||
@@ -561,6 +635,47 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
Toast.makeText(activity, R.string.downloading, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void shareAfterDownloading(Attachment att){
|
||||
Uri uri=Uri.parse(att.url);
|
||||
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
try {
|
||||
OkHttpClient client = new OkHttpClient();
|
||||
Request request = new Request.Builder().url(att.url).build();
|
||||
|
||||
Response response = client.newCall(request).execute();
|
||||
Toast.makeText(activity, R.string.downloading, Toast.LENGTH_SHORT);
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException("" + response);
|
||||
}
|
||||
|
||||
File imageDir = new File(activity.getCacheDir(), ".");
|
||||
InputStream inputStream = response.body().byteStream();
|
||||
File file = new File(imageDir, uri.getLastPathSegment());
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
|
||||
Uri outputUri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileprovider", file);
|
||||
|
||||
intent.setType(mimeTypeForFileName(outputUri.getLastPathSegment()));
|
||||
intent.putExtra(Intent.EXTRA_STREAM, outputUri);
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.button_share)));
|
||||
} catch(IOException e){
|
||||
Toast.makeText(activity, R.string.error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onAudioFocusChanged(int change){
|
||||
if(change==AudioManager.AUDIOFOCUS_LOSS || change==AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || change==AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK){
|
||||
pauseVideo();
|
||||
@@ -585,7 +700,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
if(holder==null || !holder.player.isPlaying())
|
||||
return;
|
||||
holder.player.pause();
|
||||
videoPlayPauseButton.setImageResource(R.drawable.ic_play_24);
|
||||
videoPlayPauseButton.setImageResource(R.drawable.ic_fluent_play_24_filled);
|
||||
videoPlayPauseButton.setContentDescription(activity.getString(R.string.play));
|
||||
stopUpdatingVideoPosition();
|
||||
windowView.removeCallbacks(uiAutoHider);
|
||||
@@ -599,7 +714,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
if(player==null || player.isPlaying())
|
||||
return;
|
||||
player.start();
|
||||
videoPlayPauseButton.setImageResource(R.drawable.ic_pause_24);
|
||||
videoPlayPauseButton.setImageResource(R.drawable.ic_fluent_pause_24_filled);
|
||||
videoPlayPauseButton.setContentDescription(activity.getString(R.string.pause));
|
||||
startUpdatingVideoPosition(player);
|
||||
}
|
||||
@@ -682,11 +797,11 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
public void onButtonClick(int id){
|
||||
if(id==R.id.btn_boost){
|
||||
if(status!=null){
|
||||
AccountSessionManager.get(accountID).getStatusInteractionController().setReblogged(status, !status.reblogged);
|
||||
AccountSessionManager.get(accountID).getStatusInteractionController().setReblogged(status, !status.reblogged, null, r->{});
|
||||
}
|
||||
}else if(id==R.id.btn_favorite){
|
||||
if(status!=null){
|
||||
AccountSessionManager.get(accountID).getStatusInteractionController().setFavorited(status, !status.favourited);
|
||||
AccountSessionManager.get(accountID).getStatusInteractionController().setFavorited(status, !status.favourited, r->{});
|
||||
}
|
||||
}else if(id==R.id.btn_share){
|
||||
if(status!=null){
|
||||
@@ -1053,7 +1168,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
|
||||
@Override
|
||||
public void onCompletion(MediaPlayer mp){
|
||||
videoPlayPauseButton.setImageResource(R.drawable.ic_play_24);
|
||||
videoPlayPauseButton.setImageResource(R.drawable.ic_fluent_play_24_filled);
|
||||
videoPlayPauseButton.setContentDescription(activity.getString(R.string.play));
|
||||
stopUpdatingVideoPosition();
|
||||
if(!uiVisible)
|
||||
|
||||
@@ -63,7 +63,7 @@ public class PhotoViewerInfoSheet extends BottomSheet{
|
||||
}
|
||||
|
||||
backButton=new ImageButton(context);
|
||||
backButton.setImageResource(R.drawable.ic_arrow_back);
|
||||
backButton.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular);
|
||||
backButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurfaceVariant)));
|
||||
backButton.setBackgroundResource(R.drawable.bg_button_m3_tonal_icon);
|
||||
backButton.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.InsetDrawable;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.drawables.EmptyDrawable;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.BottomSheet;
|
||||
|
||||
public abstract class AccountRestrictionConfirmationSheet extends BottomSheet{
|
||||
private LinearLayout contentWrap;
|
||||
protected Button cancelBtn;
|
||||
protected ProgressBarButton confirmBtn, secondaryBtn;
|
||||
protected TextView titleView, subtitleView;
|
||||
protected ImageView icon;
|
||||
protected boolean loading;
|
||||
|
||||
public AccountRestrictionConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback){
|
||||
super(context);
|
||||
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_restrict_account, null);
|
||||
setContentView(content);
|
||||
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
|
||||
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
|
||||
|
||||
contentWrap=findViewById(R.id.content_wrap);
|
||||
titleView=findViewById(R.id.title);
|
||||
subtitleView=findViewById(R.id.text);
|
||||
cancelBtn=findViewById(R.id.btn_cancel);
|
||||
confirmBtn=findViewById(R.id.btn_confirm);
|
||||
secondaryBtn=findViewById(R.id.btn_secondary);
|
||||
icon=findViewById(R.id.icon);
|
||||
|
||||
contentWrap.setDividerDrawable(new EmptyDrawable(1, V.dp(8)));
|
||||
contentWrap.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
|
||||
confirmBtn.setOnClickListener(v->{
|
||||
if(loading)
|
||||
return;
|
||||
loading=true;
|
||||
confirmBtn.setProgressBarVisible(true);
|
||||
confirmCallback.onConfirmed(this::dismiss, ()->{
|
||||
confirmBtn.setProgressBarVisible(false);
|
||||
loading=false;
|
||||
});
|
||||
});
|
||||
cancelBtn.setOnClickListener(v->{
|
||||
if(!loading)
|
||||
dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
protected void addRow(@DrawableRes int icon, CharSequence text){
|
||||
TextView tv=new TextView(getContext());
|
||||
tv.setTextAppearance(R.style.m3_body_large);
|
||||
tv.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurfaceVariant));
|
||||
tv.setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary)));
|
||||
tv.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
|
||||
tv.setText(text);
|
||||
InsetDrawable drawable=new InsetDrawable(getContext().getResources().getDrawable(icon, getContext().getTheme()), V.dp(8));
|
||||
drawable.setBounds(0, 0, V.dp(40), V.dp(40));
|
||||
tv.setCompoundDrawablesRelative(drawable, null, null, null);
|
||||
tv.setCompoundDrawablePadding(V.dp(16));
|
||||
contentWrap.addView(tv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
|
||||
protected void addRow(@DrawableRes int icon, @StringRes int text){
|
||||
addRow(icon, getContext().getString(text));
|
||||
}
|
||||
|
||||
public interface ConfirmCallback{
|
||||
void onConfirmed(Runnable onSuccess, Runnable onError);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class BlockAccountConfirmationSheet extends AccountRestrictionConfirmationSheet{
|
||||
public BlockAccountConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback){
|
||||
super(context, user, confirmCallback);
|
||||
titleView.setText(R.string.block_user_confirm_title);
|
||||
confirmBtn.setText(R.string.do_block);
|
||||
secondaryBtn.setVisibility(View.GONE);
|
||||
icon.setImageResource(R.drawable.ic_block_24px);
|
||||
subtitleView.setText(user.getDisplayUsername());
|
||||
addRow(R.drawable.ic_campaign_24px, R.string.user_can_see_blocked);
|
||||
addRow(R.drawable.ic_visibility_off_24px, R.string.user_cant_see_each_other_posts);
|
||||
addRow(R.drawable.ic_alternate_email_24px, R.string.you_wont_see_user_mentions);
|
||||
addRow(R.drawable.ic_reply_24px, R.string.user_cant_mention_or_follow_you);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class BlockDomainConfirmationSheet extends AccountRestrictionConfirmationSheet{
|
||||
public BlockDomainConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback, ConfirmCallback blockUserConfirmCallback){
|
||||
super(context, user, confirmCallback);
|
||||
titleView.setText(R.string.block_domain_confirm_title);
|
||||
confirmBtn.setText(R.string.do_block_server);
|
||||
secondaryBtn.setText(context.getString(R.string.block_user_x_instead, user.getDisplayUsername()));
|
||||
icon.setImageResource(R.drawable.ic_domain_disabled_24px);
|
||||
subtitleView.setText(user.getDomain());
|
||||
addRow(R.drawable.ic_campaign_24px, R.string.users_cant_see_blocked);
|
||||
addRow(R.drawable.ic_visibility_off_24px, R.string.you_wont_see_server_posts);
|
||||
addRow(R.drawable.ic_person_remove_24px, R.string.server_followers_will_be_removed);
|
||||
addRow(R.drawable.ic_reply_24px, R.string.server_cant_mention_or_follow_you);
|
||||
addRow(R.drawable.ic_history_24px, R.string.server_can_interact_with_older);
|
||||
|
||||
secondaryBtn.setOnClickListener(v->{
|
||||
if(loading)
|
||||
return;
|
||||
loading=true;
|
||||
secondaryBtn.setProgressBarVisible(true);
|
||||
blockUserConfirmCallback.onConfirmed(this::dismiss, ()->{
|
||||
secondaryBtn.setProgressBarVisible(false);
|
||||
loading=false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.Snackbar;
|
||||
import org.joinmastodon.android.ui.text.LinkSpan;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.RippleAnimationTextView;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.nodes.TextNode;
|
||||
import org.jsoup.select.NodeVisitor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.views.BottomSheet;
|
||||
|
||||
public class DecentralizationExplainerSheet extends BottomSheet{
|
||||
private final String handleStr;
|
||||
|
||||
public DecentralizationExplainerSheet(@NonNull Context context, String accountID, Account account){
|
||||
super(context);
|
||||
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_decentralization_info, null);
|
||||
setContentView(content);
|
||||
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
|
||||
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
|
||||
|
||||
TextView handleTitle=findViewById(R.id.handle_title);
|
||||
RippleAnimationTextView handle=findViewById(R.id.handle);
|
||||
TextView usernameExplanation=findViewById(R.id.username_text);
|
||||
TextView serverExplanation=findViewById(R.id.server_text);
|
||||
TextView handleExplanation=findViewById(R.id.handle_explanation);
|
||||
findViewById(R.id.btn_cancel).setOnClickListener(v->dismiss());
|
||||
|
||||
String domain=account.getDomain();
|
||||
if(TextUtils.isEmpty(domain))
|
||||
domain=AccountSessionManager.get(accountID).domain;
|
||||
handleStr="@"+account.username+"@"+domain;
|
||||
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
|
||||
handleTitle.setText(isSelf ? R.string.handle_title_own : R.string.handle_title);
|
||||
handle.setText(handleStr);
|
||||
usernameExplanation.setText(isSelf ? R.string.handle_username_explanation_own : R.string.handle_username_explanation);
|
||||
serverExplanation.setText(isSelf ? R.string.handle_server_explanation_own : R.string.handle_server_explanation);
|
||||
|
||||
String explanation=context.getString(isSelf ? R.string.handle_explanation_own : R.string.handle_explanation);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder();
|
||||
Jsoup.parseBodyFragment(explanation).body().traverse(new NodeVisitor(){
|
||||
private int spanStart;
|
||||
@Override
|
||||
public void head(Node node, int depth){
|
||||
if(node instanceof TextNode tn){
|
||||
ssb.append(tn.text());
|
||||
}else if(node instanceof Element){
|
||||
spanStart=ssb.length();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tail(Node node, int depth){
|
||||
if(node instanceof Element){
|
||||
ssb.setSpan(new LinkSpan("", DecentralizationExplainerSheet.this::showActivityPubAlert, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
});
|
||||
handleExplanation.setText(ssb);
|
||||
|
||||
findViewById(R.id.handle_wrap).setOnClickListener(v->{
|
||||
context.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, handleStr));
|
||||
if(UiUtils.needShowClipboardToast()){
|
||||
new Snackbar.Builder(context)
|
||||
.setText(R.string.handle_copied)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
String _domain=domain;
|
||||
findViewById(R.id.username_row).setOnClickListener(v->handle.animate(1, account.username.length()+1));
|
||||
findViewById(R.id.server_row).setOnClickListener(v->handle.animate(handleStr.length()-_domain.length(), handleStr.length()));
|
||||
}
|
||||
|
||||
private void showActivityPubAlert(LinkSpan s){
|
||||
new M3AlertDialogBuilder(getContext())
|
||||
.setTitle(R.string.what_is_activitypub_title)
|
||||
.setMessage(R.string.what_is_activitypub)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class MuteAccountConfirmationSheet extends AccountRestrictionConfirmationSheet{
|
||||
public MuteAccountConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback){
|
||||
super(context, user, confirmCallback);
|
||||
titleView.setText(R.string.mute_user_confirm_title);
|
||||
confirmBtn.setText(R.string.do_mute);
|
||||
secondaryBtn.setVisibility(View.GONE);
|
||||
icon.setImageResource(R.drawable.ic_volume_off_24px);
|
||||
subtitleView.setText(user.getDisplayUsername());
|
||||
addRow(R.drawable.ic_campaign_24px, R.string.user_wont_know_muted);
|
||||
addRow(R.drawable.ic_visibility_off_24px, R.string.user_can_still_see_your_posts);
|
||||
addRow(R.drawable.ic_alternate_email_24px, R.string.you_wont_see_user_mentions);
|
||||
addRow(R.drawable.ic_reply_24px, R.string.user_can_mention_and_follow_you);
|
||||
}
|
||||
}
|
||||
@@ -1715,7 +1715,9 @@ public class TabLayout extends HorizontalScrollView implements CustomViewHelper{
|
||||
child.getLayoutParams().height);
|
||||
|
||||
int childWidthMeasureSpec =
|
||||
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
|
||||
MeasureSpec.makeMeasureSpec(
|
||||
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
|
||||
MeasureSpec.EXACTLY);
|
||||
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.CornerPathEffect;
|
||||
import android.graphics.Paint;
|
||||
@@ -15,15 +11,14 @@ import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SoundEffectConstants;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
|
||||
public class ClickableLinksDelegate implements CustomViewHelper{
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ClickableLinksDelegate {
|
||||
|
||||
private final Paint hlPaint;
|
||||
private Path hlPath;
|
||||
@@ -36,9 +31,9 @@ public class ClickableLinksDelegate implements CustomViewHelper{
|
||||
this.view=view;
|
||||
hlPaint=new Paint();
|
||||
hlPaint.setAntiAlias(true);
|
||||
hlPaint.setPathEffect(new CornerPathEffect(dp(3)));
|
||||
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
|
||||
hlPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||
hlPaint.setStrokeWidth(dp(4));
|
||||
hlPaint.setStrokeWidth(V.dp(4));
|
||||
gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler());
|
||||
}
|
||||
|
||||
@@ -69,11 +64,6 @@ public class ClickableLinksDelegate implements CustomViewHelper{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resources getResources(){
|
||||
return view.getResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* GestureListener for spans that represent URLs.
|
||||
* onDown: on start of touch event, set highlighting
|
||||
@@ -125,13 +115,8 @@ public class ClickableLinksDelegate implements CustomViewHelper{
|
||||
|
||||
@Override
|
||||
public void onLongPress(@NonNull MotionEvent event) {
|
||||
//if target is not a link, don't copy
|
||||
if (selectedSpan == null) return;
|
||||
if (selectedSpan.getType() != LinkSpan.Type.URL) return;
|
||||
//copy link text to clipboard
|
||||
ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink()));
|
||||
UiUtils.maybeShowTextCopiedToast(view.getContext());
|
||||
UiUtils.copyText(view, selectedSpan.getType() == LinkSpan.Type.URL ? selectedSpan.getLink() : selectedSpan.getText());
|
||||
//reset view
|
||||
resetAndInvalidate();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.CharacterStyle;
|
||||
|
||||
public class DiffRemovedSpan extends CharacterStyle {
|
||||
|
||||
private final String text;
|
||||
private final int color;
|
||||
|
||||
public DiffRemovedSpan(String text, int color){
|
||||
this.text=text;
|
||||
this.color=color;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
tp.setStrikeThruText(true);
|
||||
tp.setColor(color);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.BulletSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.LeadingMarginSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StrikethroughSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.SubscriptSpan;
|
||||
import android.text.style.SuperscriptSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.twitter.twittertext.Regex;
|
||||
@@ -26,6 +37,7 @@ import org.jsoup.safety.Safelist;
|
||||
import org.jsoup.select.NodeVisitor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
@@ -35,6 +47,8 @@ import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HtmlParser{
|
||||
private static final String TAG="HtmlParser";
|
||||
private static final String VALID_URL_PATTERN_STRING =
|
||||
@@ -57,6 +71,18 @@ public class HtmlParser{
|
||||
|
||||
private HtmlParser(){}
|
||||
|
||||
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID){
|
||||
return parse(source, emojis, mentions, tags, accountID, null, null);
|
||||
}
|
||||
|
||||
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID, Context context){
|
||||
return parse(source, emojis, mentions, tags, accountID, null, context);
|
||||
}
|
||||
|
||||
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID, Object parentObject){
|
||||
return parse(source, emojis, mentions, tags, accountID, parentObject, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTML and custom emoji into a spanned string for display.
|
||||
* Supported tags: <ul>
|
||||
@@ -69,16 +95,22 @@ public class HtmlParser{
|
||||
* @param emojis Custom emojis that are present in source as <code>:code:</code>
|
||||
* @return a spanned string
|
||||
*/
|
||||
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID, Object parentObject){
|
||||
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID, Object parentObject, Context context){
|
||||
class SpanInfo{
|
||||
public Object span;
|
||||
public int start;
|
||||
public Element element;
|
||||
public boolean more;
|
||||
|
||||
public SpanInfo(Object span, int start, Element element){
|
||||
this(span, start, element, false);
|
||||
}
|
||||
|
||||
public SpanInfo(Object span, int start, Element element, boolean more){
|
||||
this.span=span;
|
||||
this.start=start;
|
||||
this.element=element;
|
||||
this.more=more;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +120,9 @@ public class HtmlParser{
|
||||
Map<String, Hashtag> tagsByTag=tags.stream().distinct().collect(Collectors.toMap(t->t.name.toLowerCase(), Function.identity()));
|
||||
|
||||
final SpannableStringBuilder ssb=new SpannableStringBuilder();
|
||||
int colorInsert=UiUtils.getThemeColor(context, R.attr.colorM3Success);
|
||||
int colorDelete=UiUtils.getThemeColor(context, R.attr.colorM3Error);
|
||||
|
||||
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
|
||||
private final ArrayList<SpanInfo> openSpans=new ArrayList<>();
|
||||
|
||||
@@ -129,24 +164,62 @@ public class HtmlParser{
|
||||
openSpans.add(new SpanInfo(new InvisibleSpan(), ssb.length(), el));
|
||||
}
|
||||
}
|
||||
case "li" -> openSpans.add(new SpanInfo(new BulletSpan(V.dp(8)), ssb.length(), el));
|
||||
case "em", "i" -> openSpans.add(new SpanInfo(new StyleSpan(Typeface.ITALIC), ssb.length(), el));
|
||||
case "h1", "h2", "h3", "h4", "h5", "h6" -> {
|
||||
// increase line height above heading (multiplying the margin)
|
||||
if (node.previousSibling()!=null) ssb.setSpan(new RelativeSizeSpan(2), ssb.length() - 1, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
if (!node.nodeName().equals("h1")) {
|
||||
openSpans.add(new SpanInfo(new StyleSpan(Typeface.BOLD), ssb.length(), el));
|
||||
}
|
||||
openSpans.add(new SpanInfo(new RelativeSizeSpan(switch(node.nodeName()) {
|
||||
case "h1" -> 1.5f;
|
||||
case "h2" -> 1.25f;
|
||||
case "h3" -> 1.125f;
|
||||
default -> 1;
|
||||
}), ssb.length(), el, !node.nodeName().equals("h1")));
|
||||
}
|
||||
case "strong", "b" -> openSpans.add(new SpanInfo(new StyleSpan(Typeface.BOLD), ssb.length(), el));
|
||||
case "u" -> openSpans.add(new SpanInfo(new UnderlineSpan(), ssb.length(), el));
|
||||
case "s", "del" -> openSpans.add(new SpanInfo(new StrikethroughSpan(), ssb.length(), el));
|
||||
case "sub", "sup" -> {
|
||||
openSpans.add(new SpanInfo(node.nodeName().equals("sub") ? new SubscriptSpan() : new SuperscriptSpan(), ssb.length(), el));
|
||||
openSpans.add(new SpanInfo(new RelativeSizeSpan(0.8f), ssb.length(), el, true));
|
||||
}
|
||||
case "code", "pre" -> openSpans.add(new SpanInfo(new TypefaceSpan("monospace"), ssb.length(), el));
|
||||
case "blockquote" -> openSpans.add(new SpanInfo(new LeadingMarginSpan.Standard(V.dp(10)), ssb.length(), el));
|
||||
// fake elements for the edit history diff view
|
||||
case "edit-diff-insert" -> openSpans.add(new SpanInfo(new ForegroundColorSpan(colorInsert), ssb.length(), el));
|
||||
case "edit-diff-delete" -> openSpans.add(new SpanInfo(new DiffRemovedSpan(el.text(), colorDelete), ssb.length(), el));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final static List<String> blockElements = Arrays.asList("p", "ul", "ol", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6");
|
||||
|
||||
@Override
|
||||
public void tail(@NonNull Node node, int depth){
|
||||
if(node instanceof Element el){
|
||||
processOpenSpan(el);
|
||||
if("span".equals(el.nodeName()) && el.hasClass("ellipsis")){
|
||||
ssb.append("…", new DeleteWhenCopiedSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}else if("p".equals(el.nodeName())){
|
||||
if(node.nextSibling()!=null)
|
||||
ssb.append("\n\n");
|
||||
}else if(!openSpans.isEmpty()){
|
||||
SpanInfo si=openSpans.get(openSpans.size()-1);
|
||||
if(si.element==el){
|
||||
ssb.setSpan(si.span, si.start, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
openSpans.remove(openSpans.size()-1);
|
||||
}
|
||||
}else if(blockElements.contains(el.nodeName()) && node.nextSibling()!=null){
|
||||
ssb.append("\n"); // line end
|
||||
ssb.append("\n", new RelativeSizeSpan(0.65f), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // margin after block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processOpenSpan(Element el) {
|
||||
if(!openSpans.isEmpty()){
|
||||
SpanInfo si=openSpans.get(openSpans.size()-1);
|
||||
if(si.element==el){
|
||||
ssb.setSpan(si.span, si.start, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
openSpans.remove(openSpans.size()-1);
|
||||
if(si.more) processOpenSpan(el);
|
||||
}
|
||||
if("li".equals(el.nodeName()) && el.nextSibling()!=null) {
|
||||
ssb.append('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,6 +230,7 @@ public class HtmlParser{
|
||||
}
|
||||
|
||||
public static void parseCustomEmoji(SpannableStringBuilder ssb, List<Emoji> emojis){
|
||||
if(emojis==null) return;
|
||||
Map<String, Emoji> emojiByCode =
|
||||
emojis.stream()
|
||||
.collect(
|
||||
@@ -228,6 +302,10 @@ public class HtmlParser{
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String text(String html) {
|
||||
return Jsoup.parse(html).body().wholeText();
|
||||
}
|
||||
|
||||
public static CharSequence parseLinks(String text){
|
||||
Matcher matcher=URL_PATTERN.matcher(text);
|
||||
if(!matcher.find()) // Return the original string if there are no URLs
|
||||
@@ -243,6 +321,7 @@ public class HtmlParser{
|
||||
}
|
||||
|
||||
public static void applyFilterHighlights(Context context, SpannableStringBuilder text, List<FilterResult> filters){
|
||||
if (filters == null) return;
|
||||
int fgColor=UiUtils.getThemeColor(context, R.attr.colorM3Error);
|
||||
int bgColor=UiUtils.getThemeColor(context, R.attr.colorM3ErrorContainer);
|
||||
for(FilterResult filter:filters){
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.text.style.ImageSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class ImageSpanThatDoesNotBreakShitForNoGoodReason extends ImageSpan{
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Bitmap b){
|
||||
super(b);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Bitmap b, int verticalAlignment){
|
||||
super(b, verticalAlignment);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, @NonNull Bitmap bitmap){
|
||||
super(context, bitmap);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, @NonNull Bitmap bitmap, int verticalAlignment){
|
||||
super(context, bitmap, verticalAlignment);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Drawable drawable){
|
||||
super(drawable);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Drawable drawable, int verticalAlignment){
|
||||
super(drawable, verticalAlignment);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Drawable drawable, @NonNull String source){
|
||||
super(drawable, source);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Drawable drawable, @NonNull String source, int verticalAlignment){
|
||||
super(drawable, source, verticalAlignment);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, @NonNull Uri uri){
|
||||
super(context, uri);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, @NonNull Uri uri, int verticalAlignment){
|
||||
super(context, uri, verticalAlignment);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, int resourceId){
|
||||
super(context, resourceId);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, int resourceId, int verticalAlignment){
|
||||
super(context, resourceId, verticalAlignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
|
||||
// Purposefully not touching the font metrics
|
||||
return getDrawable().getBounds().right;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package org.joinmastodon.android.ui.text;
|
||||
import android.content.Context;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
@@ -33,7 +35,7 @@ public class LinkSpan extends CharacterStyle {
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
tp.setColor(color=tp.linkColor);
|
||||
tp.setUnderlineText(true);
|
||||
tp.setUnderlineText(GlobalUserPreferences.underlinedLinks);
|
||||
}
|
||||
|
||||
public void onClick(Context context){
|
||||
@@ -50,10 +52,21 @@ public class LinkSpan extends CharacterStyle {
|
||||
}
|
||||
}
|
||||
|
||||
public void onLongClick(View view) {
|
||||
if(linkObject instanceof Hashtag ht)
|
||||
UiUtils.copyText(view, ht.name);
|
||||
else
|
||||
UiUtils.copyText(view, link);
|
||||
}
|
||||
|
||||
public String getLink(){
|
||||
return link;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return parentObject.toString();
|
||||
}
|
||||
|
||||
public Type getType(){
|
||||
return type;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.ThemePreference;
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
|
||||
import static org.joinmastodon.android.api.session.AccountLocalPreferences.ColorPreference.*;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.annotation.StyleRes;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ColorPalette {
|
||||
public static final Map<AccountLocalPreferences.ColorPreference, ColorPalette> palettes = Map.of(
|
||||
MATERIAL3, new ColorPalette(R.style.ColorPalette_Material3)
|
||||
.dark(R.style.ColorPalette_Material3_Dark, R.style.ColorPalette_Material3_AutoLightDark),
|
||||
PINK, new ColorPalette(R.style.ColorPalette_Pink),
|
||||
PURPLE, new ColorPalette(R.style.ColorPalette_Purple),
|
||||
GREEN, new ColorPalette(R.style.ColorPalette_Green),
|
||||
BLUE, new ColorPalette(R.style.ColorPalette_Blue),
|
||||
BROWN, new ColorPalette(R.style.ColorPalette_Brown),
|
||||
RED, new ColorPalette(R.style.ColorPalette_Red),
|
||||
YELLOW, new ColorPalette(R.style.ColorPalette_Yellow),
|
||||
NORD, new ColorPalette(R.style.ColorPalette_Nord),
|
||||
WHITE, new ColorPalette(R.style.ColorPalette_White)
|
||||
|
||||
);
|
||||
|
||||
private @StyleRes int base;
|
||||
private @StyleRes int autoDark;
|
||||
private @StyleRes int light;
|
||||
private @StyleRes int dark;
|
||||
private @StyleRes int black;
|
||||
private @StyleRes int autoBlack;
|
||||
|
||||
public ColorPalette(@StyleRes int baseRes) { base = baseRes; }
|
||||
|
||||
public ColorPalette(@StyleRes int lightRes, @StyleRes int darkRes, @StyleRes int autoDarkRes, @StyleRes int blackRes, @StyleRes int autoBlackRes) {
|
||||
light = lightRes;
|
||||
dark = darkRes;
|
||||
autoDark = autoDarkRes;
|
||||
black = blackRes;
|
||||
autoBlack = autoBlackRes;
|
||||
}
|
||||
|
||||
public ColorPalette light(@StyleRes int res) { light = res; return this; }
|
||||
public ColorPalette dark(@StyleRes int res, @StyleRes int auto) { dark = res; autoDark = auto; return this; }
|
||||
public ColorPalette black(@StyleRes int res, @StyleRes int auto) { dark = res; autoBlack = auto; return this; }
|
||||
|
||||
public void apply(Context context) {
|
||||
apply(context, GlobalUserPreferences.theme);
|
||||
}
|
||||
|
||||
public void apply(Context context, ThemePreference theme) {
|
||||
if (!((dark != 0 && autoDark != 0) || (black != 0 && autoBlack != 0) || light != 0 || base != 0)) {
|
||||
throw new IllegalStateException("Invalid color scheme definition");
|
||||
}
|
||||
|
||||
Resources.Theme t = context.getTheme();
|
||||
t.applyStyle(R.style.ColorPalette_Fallback, true);
|
||||
if (base != 0) t.applyStyle(base, true);
|
||||
if (light != 0 && theme.equals(ThemePreference.LIGHT)) {
|
||||
t.applyStyle(light, true);
|
||||
} else if (theme.equals(ThemePreference.DARK)) {
|
||||
t.applyStyle(R.style.ColorPalette_Dark, true);
|
||||
if (trueBlackTheme) t.applyStyle(R.style.ColorPalette_Dark_TrueBlack, true);
|
||||
if (dark != 0 && !trueBlackTheme) t.applyStyle(dark, true);
|
||||
else if (black != 0 && trueBlackTheme) t.applyStyle(black, true);
|
||||
} else if (theme.equals(ThemePreference.AUTO)) {
|
||||
t.applyStyle(R.style.ColorPalette_AutoLightDark, true);
|
||||
if (trueBlackTheme) t.applyStyle(R.style.ColorPalette_AutoLightDark_TrueBlack, true);
|
||||
if (autoDark != 0 && !trueBlackTheme) t.applyStyle(autoDark, true);
|
||||
else if (autoBlack != 0 && trueBlackTheme) t.applyStyle(autoBlack, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,16 @@ public class CustomEmojiHelper{
|
||||
}
|
||||
}
|
||||
|
||||
public void addText(CharSequence text) {
|
||||
if(!(text instanceof Spanned))
|
||||
return;
|
||||
CustomEmojiSpan[] spans=((Spanned) text).getSpans(0, text.length(), CustomEmojiSpan.class);
|
||||
for(List<CustomEmojiSpan> group:Arrays.stream(spans).collect(Collectors.groupingBy(s->s.emoji)).values()){
|
||||
this.spans.add(group);
|
||||
requests.add(group.get(0).createImageLoaderRequest());
|
||||
}
|
||||
}
|
||||
|
||||
public int getImageCount(){
|
||||
return requests.size();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
@@ -48,17 +49,23 @@ public class DiscoverInfoBannerHelper{
|
||||
banner=((Activity)list.getContext()).getLayoutInflater().inflate(R.layout.discover_info_banner, list, false);
|
||||
TextView text=banner.findViewById(R.id.banner_text);
|
||||
text.setText(switch(type){
|
||||
case TRENDING_POSTS -> list.getResources().getString(R.string.trending_posts_info_banner);
|
||||
case TRENDING_LINKS -> list.getResources().getString(R.string.trending_links_info_banner);
|
||||
case TRENDING_POSTS -> list.getResources().getString(R.string.sk_trending_posts_info_banner);
|
||||
case TRENDING_LINKS -> list.getResources().getString(R.string.sk_trending_links_info_banner);
|
||||
case FEDERATED_TIMELINE -> list.getResources().getString(R.string.sk_federated_timeline_info_banner);
|
||||
case POST_NOTIFICATIONS -> list.getResources().getString(R.string.sk_notify_posts_info_banner);
|
||||
case BUBBLE_TIMELINE -> list.getResources().getString(R.string.sk_bubble_timeline_info_banner);
|
||||
case LOCAL_TIMELINE -> list.getResources().getString(R.string.local_timeline_info_banner, AccountSessionManager.get(accountID).domain);
|
||||
case ACCOUNTS -> list.getResources().getString(R.string.recommended_accounts_info_banner);
|
||||
});
|
||||
ImageView icon=banner.findViewById(R.id.icon);
|
||||
icon.setImageResource(switch(type){
|
||||
case TRENDING_POSTS -> R.drawable.ic_whatshot_24px;
|
||||
case TRENDING_LINKS -> R.drawable.ic_feed_24px;
|
||||
case LOCAL_TIMELINE -> R.drawable.ic_stream_24px;
|
||||
case ACCOUNTS -> R.drawable.ic_group_add_24px;
|
||||
case TRENDING_POSTS -> R.drawable.ic_fluent_arrow_trending_24_regular;
|
||||
case TRENDING_LINKS -> R.drawable.ic_fluent_news_24_regular;
|
||||
case ACCOUNTS -> R.drawable.ic_fluent_people_add_24_regular;
|
||||
case LOCAL_TIMELINE -> TimelineDefinition.LOCAL_TIMELINE.getDefaultIcon().iconRes;
|
||||
case FEDERATED_TIMELINE -> TimelineDefinition.FEDERATED_TIMELINE.getDefaultIcon().iconRes;
|
||||
case BUBBLE_TIMELINE -> TimelineDefinition.BUBBLE_TIMELINE.getDefaultIcon().iconRes;
|
||||
case POST_NOTIFICATIONS -> TimelineDefinition.POSTS_TIMELINE.getDefaultIcon().iconRes;
|
||||
});
|
||||
adapter.addAdapter(0, bannerAdapter=new SingleViewRecyclerAdapter(banner));
|
||||
added=true;
|
||||
@@ -89,6 +96,9 @@ public class DiscoverInfoBannerHelper{
|
||||
TRENDING_POSTS,
|
||||
TRENDING_LINKS,
|
||||
LOCAL_TIMELINE,
|
||||
ACCOUNTS
|
||||
FEDERATED_TIMELINE,
|
||||
POST_NOTIFICATIONS,
|
||||
ACCOUNTS,
|
||||
BUBBLE_TIMELINE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,9 @@ import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -26,7 +27,7 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
|
||||
|
||||
public InsetStatusItemDecoration(BaseStatusListFragment<?> listFragment){
|
||||
this.listFragment=listFragment;
|
||||
bgColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorM3SurfaceVariant);
|
||||
bgColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorM3Surface);
|
||||
borderColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorM3OutlineVariant);
|
||||
}
|
||||
|
||||
@@ -41,13 +42,17 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
|
||||
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> sdi) && sdi.getItem().inset;
|
||||
if(inset){
|
||||
if(rect.isEmpty()){
|
||||
float childY=child.getY();
|
||||
if(pos>0 && displayItems.get(pos-1).getType()==StatusDisplayItem.Type.REBLOG_OR_REPLY_LINE){
|
||||
childY+=V.dp(8);
|
||||
if(holder instanceof MediaGridStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder){
|
||||
rect.set(child.getX(), i == 0 && pos > 0 && displayItems.get(pos - 1).inset ? V.dp(-10) : child.getY(), child.getX() + child.getWidth(), child.getY() + child.getHeight() + V.dp(4));
|
||||
}else {
|
||||
rect.set(child.getX(), i == 0 && pos > 0 && displayItems.get(pos - 1).inset ? V.dp(-10) : child.getY(), child.getX() + child.getWidth(), child.getY() + child.getHeight());
|
||||
}
|
||||
rect.set(child.getX(), i==0 && pos>0 && displayItems.get(pos-1).inset ? V.dp(-10) : childY, child.getX()+child.getWidth(), child.getY()+child.getHeight());
|
||||
}else{
|
||||
rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight());
|
||||
if(holder instanceof MediaGridStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder){
|
||||
rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight()) + V.dp(4);
|
||||
}else {
|
||||
rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight());
|
||||
}
|
||||
}
|
||||
}else if(!rect.isEmpty()){
|
||||
drawInsetBackground(parent, c);
|
||||
@@ -66,35 +71,40 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
|
||||
private void drawInsetBackground(RecyclerView list, Canvas c){
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(bgColor);
|
||||
rect.left=V.dp(16);
|
||||
rect.right=list.getWidth()-V.dp(16);
|
||||
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
||||
rect.left=V.dp(12);
|
||||
rect.right=list.getWidth()-V.dp(12);
|
||||
rect.intersect(V.dp(4), V.dp(4), V.dp(4), V.dp(-4));
|
||||
c.drawRoundRect(rect, V.dp(12), V.dp(12), paint);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(V.dp(1));
|
||||
paint.setColor(borderColor);
|
||||
rect.inset(paint.getStrokeWidth()/2f, paint.getStrokeWidth()/2f);
|
||||
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
||||
c.drawRoundRect(rect, V.dp(12), V.dp(12), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
List<StatusDisplayItem> displayItems=listFragment.getDisplayItems();
|
||||
// List<StatusDisplayItem> displayItems=listFragment.getDisplayItems();
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> sdi){
|
||||
boolean inset=sdi.getItem().inset;
|
||||
int pos=holder.getAbsoluteAdapterPosition();
|
||||
// int pos=holder.getAbsoluteAdapterPosition();
|
||||
if(inset){
|
||||
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
|
||||
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
|
||||
StatusDisplayItem.Type type=sdi.getItem().getType();
|
||||
if(type==StatusDisplayItem.Type.CARD_LARGE || type==StatusDisplayItem.Type.MEDIA_GRID)
|
||||
outRect.left=outRect.right=V.dp(16);
|
||||
else
|
||||
outRect.left=outRect.right=V.dp(8);
|
||||
if(!bottomSiblingInset)
|
||||
outRect.bottom=V.dp(16);
|
||||
if(!topSiblingInset && pos > 1 && displayItems.get(pos-1) instanceof NotificationHeaderStatusDisplayItem)
|
||||
outRect.top=V.dp(-8);
|
||||
// boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
|
||||
// boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
|
||||
// if(holder instanceof MediaGridStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
|
||||
int pad=V.dp(16);
|
||||
// else pad=V.dp(12);
|
||||
outRect.left=pad;
|
||||
outRect.right=pad;
|
||||
|
||||
// had to comment this out because animations with offsets aren't handled properly.
|
||||
// can be worked around by manually applying top margins to items
|
||||
// see InsetDummyStatusDisplayItem#onBind
|
||||
// if(!topSiblingInset)
|
||||
// outRect.top=pad;
|
||||
// if(!bottomSiblingInset)
|
||||
// outRect.bottom=pad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
@@ -20,7 +21,7 @@ public class MediaAttachmentViewController{
|
||||
public final View view;
|
||||
public final MediaGridStatusDisplayItem.GridItemType type;
|
||||
public final ImageView photo;
|
||||
public final View altButton;
|
||||
public final View altButton, noAltButton, btnsWrap, extraBadge;
|
||||
public final TextView duration;
|
||||
public final View playButton;
|
||||
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
|
||||
@@ -37,8 +38,11 @@ public class MediaAttachmentViewController{
|
||||
}, null);
|
||||
photo=view.findViewById(R.id.photo);
|
||||
altButton=view.findViewById(R.id.alt_button);
|
||||
noAltButton=view.findViewById(R.id.no_alt_button);
|
||||
btnsWrap=view.findViewById(R.id.alt_badges);
|
||||
duration=view.findViewById(R.id.duration);
|
||||
playButton=view.findViewById(R.id.play_button);
|
||||
extraBadge=view.findViewById(R.id.extra_badge);
|
||||
this.type=type;
|
||||
this.context=context;
|
||||
if(playButton!=null){
|
||||
@@ -57,9 +61,12 @@ public class MediaAttachmentViewController{
|
||||
crossfadeDrawable.setCrossfadeAlpha(0f);
|
||||
photo.setImageDrawable(null);
|
||||
photo.setImageDrawable(crossfadeDrawable);
|
||||
photo.setContentDescription(TextUtils.isEmpty(attachment.description) ? context.getString(R.string.media_no_description) : attachment.description);
|
||||
if(altButton!=null){
|
||||
altButton.setVisibility(TextUtils.isEmpty(attachment.description) ? View.GONE : View.VISIBLE);
|
||||
boolean hasAltText = !TextUtils.isEmpty(attachment.description);
|
||||
photo.setContentDescription(!hasAltText ? context.getString(R.string.media_no_description) : attachment.description);
|
||||
if(btnsWrap!=null){
|
||||
btnsWrap.setVisibility(View.VISIBLE);
|
||||
altButton.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE);
|
||||
noAltButton.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
if(type==MediaGridStatusDisplayItem.GridItemType.VIDEO){
|
||||
duration.setText(UiUtils.formatMediaDuration((int)attachment.getDuration()));
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
|
||||
import org.joinmastodon.android.ui.drawables.PlayIconDrawable;
|
||||
|
||||
public class PreviewlessMediaAttachmentViewController{
|
||||
public final View view;
|
||||
public final MediaGridStatusDisplayItem.GridItemType type;
|
||||
private final TextView title, domain;
|
||||
public final View inner;
|
||||
private final ImageView icon;
|
||||
private final Context context;
|
||||
private Status status;
|
||||
|
||||
public PreviewlessMediaAttachmentViewController(Context context, MediaGridStatusDisplayItem.GridItemType type){
|
||||
view=context.getSystemService(LayoutInflater.class).inflate(R.layout.display_item_file, null);
|
||||
title=view.findViewById(R.id.title);
|
||||
domain=view.findViewById(R.id.domain);
|
||||
icon=view.findViewById(R.id.imageView);
|
||||
inner=view.findViewById(R.id.inner);
|
||||
this.context=context;
|
||||
this.type=type;
|
||||
}
|
||||
|
||||
public void bind(Attachment attachment, Status status){
|
||||
this.status=status;
|
||||
title.setText(attachment.description != null
|
||||
? attachment.description
|
||||
: context.getString(R.string.sk_no_alt_text));
|
||||
title.setSingleLine(false);
|
||||
|
||||
domain.setText(status.sensitive ? context.getString(R.string.sensitive_content_explain) : null);
|
||||
domain.setVisibility(status.sensitive ? View.VISIBLE : View.GONE);
|
||||
|
||||
if(attachment.type == Attachment.Type.IMAGE)
|
||||
icon.setImageDrawable(context.getDrawable(R.drawable.ic_fluent_image_24_regular));
|
||||
if(attachment.type == Attachment.Type.VIDEO)
|
||||
icon.setImageDrawable(context.getDrawable(R.drawable.ic_fluent_video_clip_24_regular));
|
||||
if(attachment.type == Attachment.Type.GIFV)
|
||||
icon.setImageDrawable(context.getDrawable(R.drawable.ic_fluent_gif_24_regular));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
/*
|
||||
* Copyright 2016 Ali Muzaffar
|
||||
* <p/>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p/>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p/>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class TextDrawable extends Drawable implements TextWatcher {
|
||||
private WeakReference<TextView> ref;
|
||||
private String mText;
|
||||
private Paint mPaint;
|
||||
private Rect mHeightBounds;
|
||||
private boolean mBindToViewPaint = false;
|
||||
private float mPrevTextSize = 0;
|
||||
private boolean mInitFitText = false;
|
||||
private boolean mFitTextEnabled = false;
|
||||
|
||||
/**
|
||||
* Create a TextDrawable using the given paint object and string
|
||||
*
|
||||
* @param paint
|
||||
* @param s
|
||||
*/
|
||||
public TextDrawable(Paint paint, String s) {
|
||||
mText = s;
|
||||
mPaint = new Paint(paint);
|
||||
mHeightBounds = new Rect();
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a TextDrawable. This uses the given TextView to initialize paint and has initial text
|
||||
* that will be drawn. Initial text can also be useful for reserving space that may otherwise
|
||||
* not be available when setting compound drawables.
|
||||
*
|
||||
* @param tv The TextView / EditText using to initialize this drawable
|
||||
* @param initialText Optional initial text to display
|
||||
* @param bindToViewsText Should this drawable mirror the text in the TextView
|
||||
* @param bindToViewsPaint Should this drawable mirror changes to Paint in the TextView, like textColor, typeface, alpha etc.
|
||||
* Note, this will override any changes made using setColorFilter or setAlpha.
|
||||
*/
|
||||
public TextDrawable(TextView tv, String initialText, boolean bindToViewsText, boolean bindToViewsPaint) {
|
||||
this(tv.getPaint(), initialText);
|
||||
ref = new WeakReference<>(tv);
|
||||
if (bindToViewsText || bindToViewsPaint) {
|
||||
if (bindToViewsText) {
|
||||
tv.addTextChangedListener(this);
|
||||
}
|
||||
mBindToViewPaint = bindToViewsPaint;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a TextDrawable. This uses the given TextView to initialize paint and the text that
|
||||
* will be drawn.
|
||||
*
|
||||
* @param tv The TextView / EditText using to initialize this drawable
|
||||
* @param bindToViewsText Should this drawable mirror the text in the TextView
|
||||
* @param bindToViewsPaint Should this drawable mirror changes to Paint in the TextView, like textColor, typeface, alpha etc.
|
||||
* Note, this will override any changes made using setColorFilter or setAlpha.
|
||||
*/
|
||||
public TextDrawable(TextView tv, boolean bindToViewsText, boolean bindToViewsPaint) {
|
||||
this(tv, tv.getText().toString(), bindToViewsText, bindToViewsPaint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the provided TextView/EditText to initialize the drawable.
|
||||
* The Drawable will copy the Text and the Paint properties, however it will from that
|
||||
* point on be independant of the TextView.
|
||||
*
|
||||
* @param tv a TextView or EditText or any of their children.
|
||||
*/
|
||||
public TextDrawable(TextView tv) {
|
||||
this(tv, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the provided TextView/EditText to initialize the drawable.
|
||||
* The Drawable will copy the Paint properties, and use the provided text to initialise itself.
|
||||
*
|
||||
* @param tv a TextView or EditText or any of their children.
|
||||
* @param s The String to draw
|
||||
*/
|
||||
public TextDrawable(TextView tv, String s) {
|
||||
this(tv, s, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
if (mBindToViewPaint && ref.get() != null) {
|
||||
Paint p = ref.get().getPaint();
|
||||
canvas.drawText(mText, 0, getBounds().height(), p);
|
||||
} else {
|
||||
if (mInitFitText) {
|
||||
fitTextAndInit();
|
||||
}
|
||||
canvas.drawText(mText, 0, getBounds().height(), mPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
mPaint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter colorFilter) {
|
||||
mPaint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
int alpha = mPaint.getAlpha();
|
||||
if (alpha == 0) {
|
||||
return PixelFormat.TRANSPARENT;
|
||||
} else if (alpha == 255) {
|
||||
return PixelFormat.OPAQUE;
|
||||
} else {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
}
|
||||
|
||||
private void init() {
|
||||
Rect bounds = getBounds();
|
||||
//We want to use some character to determine the max height of the text.
|
||||
//Otherwise if we draw something like "..." they will appear centered
|
||||
//Here I'm just going to use the entire alphabet to determine max height.
|
||||
mPaint.getTextBounds("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 0, 1, mHeightBounds);
|
||||
//This doesn't account for leading or training white spaces.
|
||||
//mPaint.getTextBounds(mText, 0, mText.length(), bounds);
|
||||
float width = mPaint.measureText(mText);
|
||||
bounds.top = mHeightBounds.top;
|
||||
bounds.bottom = mHeightBounds.bottom;
|
||||
bounds.right = (int) width;
|
||||
bounds.left = 0;
|
||||
setBounds(bounds);
|
||||
}
|
||||
|
||||
public void setPaint(Paint paint) {
|
||||
mPaint = new Paint(paint);
|
||||
//Since this can change the font used, we need to recalculate bounds.
|
||||
if (mFitTextEnabled && !mInitFitText) {
|
||||
fitTextAndInit();
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
public Paint getPaint() {
|
||||
return mPaint;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
mText = text;
|
||||
//Since this can change the bounds of the text, we need to recalculate.
|
||||
if (mFitTextEnabled && !mInitFitText) {
|
||||
fitTextAndInit();
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return mText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
setText(s.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the TextDrawable match the width of the View it's associated with.
|
||||
* <p/>
|
||||
* Note: While this option will not work if bindToViewPaint is true.
|
||||
*
|
||||
* @param fitText
|
||||
*/
|
||||
public void setFillText(boolean fitText) {
|
||||
mFitTextEnabled = fitText;
|
||||
if (fitText) {
|
||||
mPrevTextSize = mPaint.getTextSize();
|
||||
if (ref.get() != null) {
|
||||
if (ref.get().getWidth() > 0) {
|
||||
fitTextAndInit();
|
||||
} else {
|
||||
mInitFitText = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (mPrevTextSize > 0) {
|
||||
mPaint.setTextSize(mPrevTextSize);
|
||||
}
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
private void fitTextAndInit() {
|
||||
float fitWidth = ref.get().getWidth();
|
||||
float textWidth = mPaint.measureText(mText);
|
||||
float multi = fitWidth / textWidth;
|
||||
mPaint.setTextSize(mPaint.getTextSize() * multi);
|
||||
mInitFitText = false;
|
||||
init();
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -83,7 +83,7 @@ public class ComposeAutocompleteViewController{
|
||||
list=new UsableRecyclerView(activity);
|
||||
list.setLayoutManager(new LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false));
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
list.setPadding(V.dp(16), V.dp(12), V.dp(16), V.dp(12));
|
||||
list.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(4));
|
||||
list.setClipToPadding(false);
|
||||
list.setSelector(null);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@@ -162,7 +162,7 @@ public class ComposeAutocompleteViewController{
|
||||
usersMergeAdapter.addAdapter(usersAdapter);
|
||||
}
|
||||
emptyButton.setText(R.string.compose_autocomplete_users_empty);
|
||||
emptyButton.setDrawableStartTinted(R.drawable.ic_search_20px);
|
||||
emptyButton.setDrawableStartTinted(R.drawable.ic_fluent_search_20_regular);
|
||||
yield usersMergeAdapter;
|
||||
}
|
||||
case EMOJIS -> {
|
||||
@@ -173,7 +173,7 @@ public class ComposeAutocompleteViewController{
|
||||
emojisMergeAdapter.addAdapter(emojisAdapter);
|
||||
}
|
||||
emptyButton.setText(R.string.compose_autocomplete_emoji_empty);
|
||||
emptyButton.setDrawableStartTinted(R.drawable.ic_mood_20px);
|
||||
emptyButton.setDrawableStartTinted(R.drawable.ic_fluent_emoji_20_regular);
|
||||
yield emojisMergeAdapter;
|
||||
}
|
||||
case HASHTAGS -> {
|
||||
|
||||
@@ -15,15 +15,19 @@ import android.widget.CheckedTextView;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CheckableLinearLayout;
|
||||
import org.joinmastodon.android.utils.MastodonLanguage;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -45,38 +49,39 @@ public class ComposeLanguageAlertViewController{
|
||||
private List<LocaleInfo> allLocales;
|
||||
private List<SpecialLocaleInfo> specialLocales=new ArrayList<>();
|
||||
private int selectedIndex=0;
|
||||
private Locale selectedLocale;
|
||||
private MastodonLanguage selectedLocale;
|
||||
private String selectedEncoding;
|
||||
private MastodonLanguage.LanguageResolver resolver;
|
||||
|
||||
public ComposeLanguageAlertViewController(Context context, String preferred, SelectedOption previouslySelected, String postText){
|
||||
public ComposeLanguageAlertViewController(Context context, String preferred, SelectedOption previouslySelected, String postText, MastodonLanguage.LanguageResolver resolver){
|
||||
this(context, preferred, previouslySelected, postText, resolver, null);
|
||||
}
|
||||
|
||||
public ComposeLanguageAlertViewController(Context context, String preferred, SelectedOption previouslySelected, String postText, MastodonLanguage.LanguageResolver resolver, AccountSession session){
|
||||
this.context=context;
|
||||
this.resolver=resolver;
|
||||
|
||||
allLocales=Arrays.stream(Locale.getAvailableLocales())
|
||||
.map(Locale::getLanguage)
|
||||
.distinct()
|
||||
.map(code->{
|
||||
Locale l=Locale.forLanguageTag(code);
|
||||
String name=l.getDisplayLanguage(Locale.getDefault());
|
||||
return new LocaleInfo(l, capitalizeLanguageName(name));
|
||||
})
|
||||
allLocales=MastodonLanguage.allLanguages.stream()
|
||||
.map(l -> new LocaleInfo(l, l.getDisplayName(context)))
|
||||
.sorted(Comparator.comparing(a->a.displayName))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if(!TextUtils.isEmpty(preferred)){
|
||||
Locale l=Locale.forLanguageTag(preferred);
|
||||
SpecialLocaleInfo pref=new SpecialLocaleInfo();
|
||||
pref.locale=l;
|
||||
pref.displayName=capitalizeLanguageName(l.getDisplayLanguage(Locale.getDefault()));
|
||||
pref.title=context.getString(R.string.language_default);
|
||||
specialLocales.add(pref);
|
||||
MastodonLanguage lang=resolver.fromOrFallback(preferred);
|
||||
specialLocales.add(new SpecialLocaleInfo(
|
||||
lang,
|
||||
lang.getDisplayName(context),
|
||||
context.getString(R.string.language_default)
|
||||
));
|
||||
}
|
||||
|
||||
Locale def=Locale.forLanguageTag(Locale.getDefault().getLanguage());
|
||||
if(!def.getLanguage().equals(preferred)){
|
||||
SpecialLocaleInfo d=new SpecialLocaleInfo();
|
||||
d.locale=def;
|
||||
d.displayName=capitalizeLanguageName(def.getDisplayName());
|
||||
d.title=context.getString(R.string.language_system);
|
||||
specialLocales.add(d);
|
||||
if(!Locale.getDefault().getLanguage().equals(preferred)){
|
||||
MastodonLanguage lang=resolver.getDefault();
|
||||
specialLocales.add(new SpecialLocaleInfo(
|
||||
lang,
|
||||
lang.getDisplayName(context),
|
||||
context.getString(R.string.language_system)
|
||||
));
|
||||
}
|
||||
|
||||
if(Build.VERSION.SDK_INT>=29 && !TextUtils.isEmpty(postText)){
|
||||
@@ -87,11 +92,25 @@ public class ComposeLanguageAlertViewController{
|
||||
detectLanguage(detected, postText);
|
||||
}
|
||||
|
||||
AccountLocalPreferences lp=session==null ? null : session.getLocalPreferences();
|
||||
if(lp!=null){
|
||||
for(String tag : lp.recentLanguages){
|
||||
if(specialLocales.stream().anyMatch(l->l.language!=null && l.language.languageTag!=null
|
||||
&& l.language.languageTag.equals(tag))) continue;
|
||||
resolver.from(tag).ifPresent(lang->specialLocales.add(new SpecialLocaleInfo(
|
||||
lang, lang.getDisplayName(context), null
|
||||
)));
|
||||
}
|
||||
if(lp.bottomEncoding) {
|
||||
specialLocales.add(new SpecialLocaleInfo(null, "\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48", "bottom"));
|
||||
}
|
||||
}
|
||||
|
||||
if(previouslySelected!=null){
|
||||
if(previouslySelected.index!=-1 && ((previouslySelected.index<specialLocales.size() && Objects.equals(previouslySelected.locale, specialLocales.get(previouslySelected.index).locale)) ||
|
||||
(previouslySelected.index<specialLocales.size()+allLocales.size() && previouslySelected.index>-1 && Objects.equals(previouslySelected.locale, allLocales.get(previouslySelected.index-specialLocales.size()).locale)))){
|
||||
if(previouslySelected.index!=-1 && ((previouslySelected.index<specialLocales.size() && Objects.equals(previouslySelected.language, specialLocales.get(previouslySelected.index).language)) ||
|
||||
(previouslySelected.index<specialLocales.size()+allLocales.size() && Objects.equals(previouslySelected.language, allLocales.get(previouslySelected.index-specialLocales.size()).language)))){
|
||||
selectedIndex=previouslySelected.index;
|
||||
selectedLocale=previouslySelected.locale;
|
||||
selectedLocale=previouslySelected.language;
|
||||
}else{
|
||||
int i=0;
|
||||
boolean found=false;
|
||||
@@ -106,8 +125,8 @@ public class ComposeLanguageAlertViewController{
|
||||
}
|
||||
if(!found){
|
||||
for(LocaleInfo li:allLocales){
|
||||
if(li.locale.equals(previouslySelected.locale)){
|
||||
selectedLocale=li.locale;
|
||||
if(li.language.equals(previouslySelected.language)){
|
||||
selectedLocale=li.language;
|
||||
selectedIndex=i;
|
||||
break;
|
||||
}
|
||||
@@ -116,7 +135,7 @@ public class ComposeLanguageAlertViewController{
|
||||
}
|
||||
}
|
||||
}else{
|
||||
selectedLocale=specialLocales.get(0).locale;
|
||||
selectedLocale=specialLocales.get(0).language;
|
||||
}
|
||||
|
||||
list=new UsableRecyclerView(context);
|
||||
@@ -126,12 +145,12 @@ public class ComposeLanguageAlertViewController{
|
||||
list.setAdapter(adapter);
|
||||
list.setLayoutManager(new LinearLayoutManager(context));
|
||||
|
||||
list.addItemDecoration(new DividerItemDecoration(context, R.attr.colorM3OutlineVariant, 1, 16, 16, vh->vh.getAbsoluteAdapterPosition()==specialLocales.size()-1));
|
||||
list.addItemDecoration(new DividerItemDecoration(context, R.attr.colorM3Outline, 1, 0, 0, vh->vh.getAbsoluteAdapterPosition()==specialLocales.size()-1));
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private Paint paint=new Paint();
|
||||
|
||||
{
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorM3OutlineVariant));
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorM3Outline));
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(V.dp(1));
|
||||
}
|
||||
@@ -173,9 +192,9 @@ public class ComposeLanguageAlertViewController{
|
||||
if(lang.getLocaleHypothesisCount()==0 || lang.getConfidenceScore(lang.getLocale(0))<0.75f){
|
||||
info.displayName=context.getString(R.string.language_cant_detect);
|
||||
}else{
|
||||
Locale locale=lang.getLocale(0).toLocale();
|
||||
info.locale=locale;
|
||||
info.displayName=capitalizeLanguageName(locale.getDisplayName(Locale.getDefault()));
|
||||
MastodonLanguage language = resolver.fromOrFallback(lang.getLocale(0).toLanguageTag());
|
||||
info.language=language;
|
||||
info.displayName=language.getDisplayName(context);
|
||||
info.title=context.getString(R.string.language_detected);
|
||||
info.enabled=true;
|
||||
if(holder!=null)
|
||||
@@ -197,7 +216,7 @@ public class ComposeLanguageAlertViewController{
|
||||
}
|
||||
|
||||
public SelectedOption getSelectedOption(){
|
||||
return new SelectedOption(selectedIndex, selectedLocale);
|
||||
return new SelectedOption(selectedIndex, selectedLocale, selectedEncoding);
|
||||
}
|
||||
|
||||
private void selectItem(int index){
|
||||
@@ -256,7 +275,8 @@ public class ComposeLanguageAlertViewController{
|
||||
@Override
|
||||
public void onClick(){
|
||||
selectItem(getAbsoluteAdapterPosition());
|
||||
selectedLocale=item.locale;
|
||||
selectedLocale=item.language;
|
||||
selectedEncoding=null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +333,8 @@ public class ComposeLanguageAlertViewController{
|
||||
@Override
|
||||
public void onClick(){
|
||||
selectItem(getAbsoluteAdapterPosition());
|
||||
selectedLocale=item.locale;
|
||||
selectedLocale=item.language;
|
||||
selectedEncoding=item.title != null && item.title.equals("bottom") ? "bottom" : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -323,32 +344,50 @@ public class ComposeLanguageAlertViewController{
|
||||
}
|
||||
|
||||
private static class LocaleInfo{
|
||||
public final Locale locale;
|
||||
public final MastodonLanguage language;
|
||||
public final String displayName;
|
||||
|
||||
private LocaleInfo(Locale locale, String displayName){
|
||||
this.locale=locale;
|
||||
private LocaleInfo(MastodonLanguage language, String displayName){
|
||||
this.language=language;
|
||||
this.displayName=displayName;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SpecialLocaleInfo{
|
||||
public Locale locale;
|
||||
public SpecialLocaleInfo() {}
|
||||
|
||||
public SpecialLocaleInfo(MastodonLanguage lang, String displayName, String title) {
|
||||
this.language = lang;
|
||||
this.displayName = displayName;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public MastodonLanguage language;
|
||||
public String displayName;
|
||||
public String title;
|
||||
public boolean enabled=true;
|
||||
}
|
||||
|
||||
@Parcel
|
||||
public static class SelectedOption{
|
||||
public static class SelectedOption {
|
||||
public int index;
|
||||
public Locale locale;
|
||||
public MastodonLanguage language;
|
||||
public String encoding;
|
||||
|
||||
public SelectedOption(){}
|
||||
|
||||
public SelectedOption(int index, Locale locale){
|
||||
this.index=index;
|
||||
this.locale=locale;
|
||||
public SelectedOption(int index, MastodonLanguage language, String encoding) {
|
||||
this.index = index;
|
||||
this.language = language;
|
||||
this.encoding = encoding;
|
||||
}
|
||||
|
||||
public SelectedOption(int index, MastodonLanguage language) {
|
||||
this(index, language, null);
|
||||
}
|
||||
|
||||
public SelectedOption(MastodonLanguage language) {
|
||||
this(-1, language, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
@@ -19,6 +20,7 @@ import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
@@ -27,8 +29,8 @@ import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -51,11 +53,8 @@ import org.parceler.Parcel;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
@@ -174,6 +173,7 @@ public class ComposeMediaViewController{
|
||||
}
|
||||
fragment.updatePublishButtonState();
|
||||
fragment.updateMediaPollStates();
|
||||
fragment.updateSensitive();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -203,6 +203,15 @@ public class ComposeMediaViewController{
|
||||
}
|
||||
}
|
||||
|
||||
private void updateButton(ImageButton btn, @DrawableRes int drawableId, @StringRes int labelId){
|
||||
btn.setImageResource(drawableId);
|
||||
String label=fragment.getContext().getString(labelId);
|
||||
btn.setContentDescription(label);
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
|
||||
btn.setTooltipText(label);
|
||||
}
|
||||
}
|
||||
|
||||
private View createMediaAttachmentView(DraftMediaAttachment draft){
|
||||
View thumb=fragment.getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
|
||||
ImageView img=thumb.findViewById(R.id.thumb);
|
||||
@@ -230,12 +239,11 @@ public class ComposeMediaViewController{
|
||||
draft.removeButton.setOnClickListener(this::onRemoveMediaAttachmentClick);
|
||||
draft.editButton.setTag(draft);
|
||||
|
||||
thumb.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
thumb.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
|
||||
thumb.setClipToOutline(true);
|
||||
img.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
img.setClipToOutline(true);
|
||||
|
||||
thumb.setBackgroundColor(UiUtils.getThemeColor(fragment.getActivity(), R.attr.colorM3Surface));
|
||||
thumb.setOnLongClickListener(v->{
|
||||
if(!v.hasTransientState() && attachments.size()>1){
|
||||
attachmentsView.startDragging(v);
|
||||
@@ -265,11 +273,11 @@ public class ComposeMediaViewController{
|
||||
draft.subtitleView.setText(subtitleRes);
|
||||
}
|
||||
draft.titleView.setText(fragment.getString(R.string.attachment_x_percent_uploaded, 0));
|
||||
draft.removeButton.setImageResource(R.drawable.ic_baseline_close_24);
|
||||
updateButton(draft.removeButton, R.drawable.ic_fluent_dismiss_24_regular, R.string.delete);
|
||||
|
||||
if(draft.state==AttachmentUploadState.ERROR){
|
||||
draft.titleView.setText(R.string.upload_failed);
|
||||
draft.editButton.setImageResource(R.drawable.ic_restart_alt_24px);
|
||||
updateButton(draft.removeButton, R.drawable.ic_fluent_arrow_counterclockwise_24_regular, R.string.retry);
|
||||
draft.editButton.setOnClickListener(this::onRetryOrCancelMediaUploadClick);
|
||||
draft.progressBar.setVisibility(View.GONE);
|
||||
draft.setUseErrorColors(true);
|
||||
@@ -279,7 +287,7 @@ public class ComposeMediaViewController{
|
||||
draft.editButton.setOnClickListener(this::onEditMediaDescriptionClick);
|
||||
}else{
|
||||
draft.editButton.setVisibility(View.GONE);
|
||||
draft.removeButton.setImageResource(R.drawable.ic_baseline_close_24);
|
||||
updateButton(draft.removeButton, R.drawable.ic_fluent_dismiss_24_regular, R.string.delete);
|
||||
if(draft.state==AttachmentUploadState.PROCESSING){
|
||||
draft.titleView.setText(R.string.upload_processing);
|
||||
}else{
|
||||
@@ -373,7 +381,7 @@ public class ComposeMediaViewController{
|
||||
// attachment.retryButton.setContentDescription(fragment.getString(R.string.retry_upload));
|
||||
|
||||
V.setVisibilityAnimated(attachment.editButton, View.VISIBLE);
|
||||
attachment.editButton.setImageResource(R.drawable.ic_restart_alt_24px);
|
||||
updateButton(attachment.editButton, R.drawable.ic_fluent_arrow_counterclockwise_24_regular, R.string.retry);
|
||||
attachment.editButton.setOnClickListener(ComposeMediaViewController.this::onRetryOrCancelMediaUploadClick);
|
||||
attachment.setUseErrorColors(true);
|
||||
V.setVisibilityAnimated(attachment.progressBar, View.GONE);
|
||||
@@ -402,6 +410,7 @@ public class ComposeMediaViewController{
|
||||
}
|
||||
fragment.updatePublishButtonState();
|
||||
fragment.updateMediaPollStates();
|
||||
fragment.updateSensitive();
|
||||
}
|
||||
|
||||
private void onRetryOrCancelMediaUploadClick(View v){
|
||||
@@ -476,8 +485,8 @@ public class ComposeMediaViewController{
|
||||
throw new IllegalStateException("Unexpected state "+attachment.state);
|
||||
attachment.uploadRequest=null;
|
||||
attachment.state=AttachmentUploadState.DONE;
|
||||
attachment.editButton.setImageResource(R.drawable.ic_edit_24px);
|
||||
attachment.removeButton.setImageResource(R.drawable.ic_delete_24px);
|
||||
updateButton(attachment.editButton, R.drawable.ic_fluent_edit_24_regular, R.string.sk_edit_alt_text);
|
||||
updateButton(attachment.removeButton, R.drawable.ic_fluent_dismiss_24_regular, R.string.delete);
|
||||
attachment.editButton.setOnClickListener(this::onEditMediaDescriptionClick);
|
||||
V.setVisibilityAnimated(attachment.progressBar, View.GONE);
|
||||
V.setVisibilityAnimated(attachment.editButton, View.VISIBLE);
|
||||
@@ -504,6 +513,14 @@ public class ComposeMediaViewController{
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean areAnyAttachmentsNotDone() {
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.state!=AttachmentUploadState.DONE)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void onEditMediaDescriptionClick(View v){
|
||||
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
||||
if(att.serverAttachment==null)
|
||||
@@ -698,18 +715,21 @@ public class ComposeMediaViewController{
|
||||
if(errorTransitionAnimator!=null)
|
||||
errorTransitionAnimator.cancel();
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
int color1, color2, color3;
|
||||
int defaultBg=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3Surface);
|
||||
int errorBg=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3ErrorContainer);
|
||||
int color2, color3;
|
||||
if(use){
|
||||
color1=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3ErrorContainer);
|
||||
color2=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3Error);
|
||||
color3=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnErrorContainer);
|
||||
}else{
|
||||
color1=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3Surface);
|
||||
color2=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurface);
|
||||
color3=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurfaceVariant);
|
||||
}
|
||||
GradientDrawable bg=(GradientDrawable) view.getBackground().mutate();
|
||||
ValueAnimator bgAnim=ValueAnimator.ofArgb(use ? defaultBg : errorBg, use ? errorBg : defaultBg);
|
||||
bgAnim.addUpdateListener(anim->bg.setColor((Integer) anim.getAnimatedValue()));
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofArgb(view, "backgroundColor", ((ColorDrawable)view.getBackground()).getColor(), color1),
|
||||
bgAnim,
|
||||
ObjectAnimator.ofArgb(titleView, "textColor", titleView.getCurrentTextColor(), color2),
|
||||
ObjectAnimator.ofArgb(subtitleView, "textColor", subtitleView.getCurrentTextColor(), color3),
|
||||
ObjectAnimator.ofArgb(removeButton.getDrawable(), "tint", subtitleView.getCurrentTextColor(), color3)
|
||||
|
||||
@@ -42,6 +42,7 @@ public class ComposePollViewController{
|
||||
30*60,
|
||||
3600,
|
||||
6*3600,
|
||||
12*3600,
|
||||
24*3600,
|
||||
3*24*3600,
|
||||
7*24*3600,
|
||||
@@ -74,10 +75,17 @@ public class ComposePollViewController{
|
||||
pollWrap=view.findViewById(R.id.poll_wrap);
|
||||
|
||||
Instance instance=fragment.instance;
|
||||
if(instance!=null && instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0)
|
||||
maxPollOptions=instance.configuration.polls.maxOptions;
|
||||
if(instance!=null && instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0)
|
||||
maxPollOptionLength=instance.configuration.polls.maxCharactersPerOption;
|
||||
if (!instance.isAkkoma()) {
|
||||
if(instance!=null && instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0)
|
||||
maxPollOptions=instance.configuration.polls.maxOptions;
|
||||
if(instance!=null && instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0)
|
||||
maxPollOptionLength=instance.configuration.polls.maxCharactersPerOption;
|
||||
} else {
|
||||
if(instance!=null && instance.pollLimits!=null && instance.pollLimits.maxOptions>0)
|
||||
maxPollOptions= (int) instance.pollLimits.maxOptions;
|
||||
if(instance!=null && instance.pollLimits!=null && instance.pollLimits.maxOptionChars>0)
|
||||
maxPollOptionLength= (int) instance.pollLimits.maxOptionChars;
|
||||
}
|
||||
|
||||
pollOptionsView=pollWrap.findViewById(R.id.poll_options);
|
||||
addPollOptionBtn=pollWrap.findViewById(R.id.add_poll_option);
|
||||
@@ -127,7 +135,10 @@ public class ComposePollViewController{
|
||||
DraftPollOption opt=createDraftPollOption(false);
|
||||
opt.edit.setText(eopt.title);
|
||||
}
|
||||
pollDuration=(int)fragment.editingStatus.poll.expiresAt.minus(fragment.editingStatus.createdAt.toEpochMilli(), ChronoUnit.MILLIS).getEpochSecond();
|
||||
if(fragment.scheduledStatus!=null && fragment.scheduledStatus.params.poll!=null)
|
||||
pollDuration=Integer.parseInt(fragment.scheduledStatus.params.poll.expiresIn);
|
||||
else if(fragment.editingStatus.poll.expiresAt!=null)
|
||||
pollDuration=(int)fragment.editingStatus.poll.expiresAt.minus(fragment.editingStatus.createdAt.toEpochMilli(), ChronoUnit.MILLIS).getEpochSecond();
|
||||
updatePollOptionHints();
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration));
|
||||
pollIsMultipleChoice=fragment.editingStatus.poll.multiple;
|
||||
|
||||
@@ -53,7 +53,7 @@ public abstract class DropdownSubmenuController{
|
||||
backItem=(TextView) dropdownController.getActivity().getLayoutInflater().inflate(R.layout.item_dropdown_menu, contentView, false);
|
||||
((LinearLayout.LayoutParams) backItem.getLayoutParams()).topMargin=V.dp(8);
|
||||
backItem.setText(backTitle);
|
||||
backItem.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_arrow_back, 0, 0, 0);
|
||||
backItem.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_fluent_arrow_left_24_regular, 0, 0, 0);
|
||||
backItem.setBackground(UiUtils.getThemeDrawable(dropdownController.getActivity(), android.R.attr.selectableItemBackground));
|
||||
backItem.setOnClickListener(v->dropdownController.popSubmenuController());
|
||||
backItem.setAccessibilityDelegate(new View.AccessibilityDelegate(){
|
||||
|
||||
@@ -40,7 +40,7 @@ public class HomeTimelineHashtagsMenuController extends DropdownSubmenuControlle
|
||||
@Override
|
||||
protected void createView(){
|
||||
super.createView();
|
||||
emptyAdapter=createEmptyView(R.drawable.ic_tag_24px, R.string.no_followed_hashtags_title, R.string.no_followed_hashtags_subtitle);
|
||||
emptyAdapter=createEmptyView(R.drawable.ic_fluent_tag_24_regular, R.string.no_followed_hashtags_title, R.string.no_followed_hashtags_subtitle);
|
||||
FrameLayout largeProgressView=new FrameLayout(dropdownController.getActivity());
|
||||
int pad=V.dp(32);
|
||||
largeProgressView.setPadding(0, pad, 0, pad);
|
||||
|
||||
@@ -3,11 +3,10 @@ package org.joinmastodon.android.ui.viewholders;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.TypefaceSpan;
|
||||
@@ -15,8 +14,8 @@ import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
@@ -24,9 +23,11 @@ import android.widget.ProgressBar;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ListsFragment;
|
||||
import org.joinmastodon.android.fragments.AddAccountToListsFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
@@ -34,6 +35,7 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
@@ -41,6 +43,7 @@ import org.parceler.Parcels;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -53,8 +56,9 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class AccountViewHolder extends BindableViewHolder<AccountViewModel> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
|
||||
private final TextView name, username, followers, verifiedLink, bio;
|
||||
private final TextView name, username, followers, pronouns, bio;
|
||||
public final ImageView avatar;
|
||||
private final FrameLayout accessory;
|
||||
private final ProgressBarButton button;
|
||||
private final PopupMenu contextMenu;
|
||||
private final View menuAnchor;
|
||||
@@ -89,10 +93,11 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
accessory=findViewById(R.id.accessory);
|
||||
button=findViewById(R.id.button);
|
||||
menuAnchor=findViewById(R.id.menu_anchor);
|
||||
followers=findViewById(R.id.followers_count);
|
||||
verifiedLink=findViewById(R.id.verified_link);
|
||||
pronouns=findViewById(R.id.pronouns);
|
||||
bio=findViewById(R.id.bio);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
actionProgress=findViewById(R.id.action_progress);
|
||||
@@ -102,11 +107,15 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
avatar.setClipToOutline(true);
|
||||
|
||||
button.setOnClickListener(this::onButtonClick);
|
||||
accessory.setOnClickListener(v -> button.performClick());
|
||||
|
||||
contextMenu=new PopupMenu(fragment.getActivity(), menuAnchor);
|
||||
contextMenu.inflate(R.menu.profile);
|
||||
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
|
||||
menuButton.setOnClickListener(v->showMenuFromButton());
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI())
|
||||
contextMenu.getMenu().setGroupDividerEnabled(true);
|
||||
UiUtils.enablePopupMenuIcons(fragment.getContext(), contextMenu);
|
||||
|
||||
setStyle(AccessoryType.BUTTON, false);
|
||||
}
|
||||
@@ -116,28 +125,40 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
public void onBind(AccountViewModel item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText("@"+item.account.acct);
|
||||
if(followers!=null){
|
||||
String followersStr=fragment.getResources().getQuantityString(R.plurals.x_followers, item.account.followersCount>1000 ? 999 : (int)item.account.followersCount);
|
||||
String followersNum=UiUtils.abbreviateNumber(item.account.followersCount);
|
||||
int index=followersStr.indexOf("%,d");
|
||||
followersStr=followersStr.replace("%,d", followersNum);
|
||||
SpannableStringBuilder followersFormatted=new SpannableStringBuilder(followersStr);
|
||||
if(index!=-1){
|
||||
followersFormatted.setSpan(mediumSpan, index, index+followersNum.length(), 0);
|
||||
}
|
||||
long num=item.account.followersCount > -1 ? item.account.followersCount : item.account.followingCount;
|
||||
String followersStr = fragment.getResources().getQuantityString(item.account.followersCount > -1
|
||||
? R.plurals.x_followers : R.plurals.x_following, num>1000 ? 999 : (int)num);
|
||||
String followersNum=UiUtils.abbreviateNumber(num);
|
||||
int index=followersStr.indexOf("%,d");
|
||||
followersStr=followersStr.replace("%,d", followersNum);
|
||||
SpannableStringBuilder followersFormatted=new SpannableStringBuilder(followersStr);
|
||||
if(index!=-1){
|
||||
followersFormatted.setSpan(mediumSpan, index, index+followersNum.length(), 0);
|
||||
}
|
||||
if (item.account.followingCount > -1 || item.account.followersCount > -1) {
|
||||
followers.setVisibility(View.VISIBLE);
|
||||
followers.setText(followersFormatted);
|
||||
} else {
|
||||
followers.setVisibility(View.GONE);
|
||||
}
|
||||
if(verifiedLink!=null){
|
||||
boolean hasVerifiedLink=item.verifiedLink!=null;
|
||||
if(!hasVerifiedLink)
|
||||
verifiedLink.setText(R.string.no_verified_link);
|
||||
else
|
||||
verifiedLink.setText(item.verifiedLink);
|
||||
verifiedLink.setCompoundDrawablesRelativeWithIntrinsicBounds(hasVerifiedLink ? R.drawable.ic_check_small_16px : R.drawable.ic_help_16px, 0, 0, 0);
|
||||
int tintColor=UiUtils.getThemeColor(fragment.getActivity(), hasVerifiedLink ? R.attr.colorM3Primary : R.attr.colorM3Secondary);
|
||||
verifiedLink.setTextColor(tintColor);
|
||||
verifiedLink.setCompoundDrawableTintList(ColorStateList.valueOf(tintColor));
|
||||
}
|
||||
|
||||
// you know what's cooler than followers or verified links? yep. pronouns
|
||||
Optional<String> pronounsString=GlobalUserPreferences.displayPronounsInUserListings
|
||||
? UiUtils.extractPronouns(itemView.getContext(), item.account) : Optional.empty();
|
||||
pronouns.setVisibility(pronounsString.isPresent() ? View.VISIBLE : View.GONE);
|
||||
pronounsString.ifPresent(p -> HtmlParser.setTextWithCustomEmoji(pronouns, p, item.account.emojis));
|
||||
|
||||
/* unused in megalodon
|
||||
boolean hasVerifiedLink=item.verifiedLink!=null;
|
||||
if(!hasVerifiedLink)
|
||||
verifiedLink.setText(R.string.no_verified_link);
|
||||
else
|
||||
verifiedLink.setText(item.verifiedLink);
|
||||
verifiedLink.setCompoundDrawablesRelativeWithIntrinsicBounds(hasVerifiedLink ? R.drawable.ic_fluent_checkmark_16_filled : R.drawable.ic_help_16px, 0, 0, 0);
|
||||
int tintColor=UiUtils.getThemeColor(fragment.getActivity(), hasVerifiedLink ? R.attr.colorM3Primary : R.attr.colorM3Secondary);
|
||||
verifiedLink.setTextColor(tintColor);
|
||||
verifiedLink.setCompoundDrawableTintList(ColorStateList.valueOf(tintColor));
|
||||
*/
|
||||
bindRelationship();
|
||||
if(showBio){
|
||||
bio.setText(item.parsedBio);
|
||||
@@ -187,7 +208,10 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
if (item.account.isRemote)
|
||||
args.putParcelable("remoteAccount", Parcels.wrap(item.account));
|
||||
else
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
@@ -202,6 +226,41 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
return true;
|
||||
if(accessoryType==AccessoryType.MENU || !prepareMenu())
|
||||
return false;
|
||||
if(relationships==null)
|
||||
return false;
|
||||
Relationship relationship=relationships.get(item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Menu menu=contextMenu.getMenu();
|
||||
Account account=item.account;
|
||||
|
||||
menu.findItem(R.id.edit_note).setVisible(false);
|
||||
menu.findItem(R.id.manage_user_lists).setTitle(fragment.getString(R.string.sk_lists_with_user, account.getShortUsername()));
|
||||
MenuItem mute=menu.findItem(R.id.mute);
|
||||
mute.setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
|
||||
mute.setIcon(relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(fragment.getContext(), mute);
|
||||
menu.findItem(R.id.block).setTitle(fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
|
||||
menu.findItem(R.id.report).setTitle(fragment.getString(R.string.report_user, account.getShortUsername()));
|
||||
menu.findItem(R.id.manage_user_lists).setVisible(relationship.following);
|
||||
menu.findItem(R.id.soft_block).setVisible(relationship.followedBy && !relationship.following);
|
||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||
if(relationship.following){
|
||||
hideBoosts.setTitle(fragment.getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getShortUsername()));
|
||||
hideBoosts.setIcon(relationship.showingReblogs ? R.drawable.ic_fluent_arrow_repeat_all_off_24_regular : R.drawable.ic_fluent_arrow_repeat_all_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(fragment.getContext(), hideBoosts);
|
||||
hideBoosts.setVisible(true);
|
||||
}else{
|
||||
hideBoosts.setVisible(false);
|
||||
}
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
if(!account.isLocal()){
|
||||
blockDomain.setTitle(fragment.getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
blockDomain.setVisible(true);
|
||||
}else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
|
||||
menuAnchor.setTranslationX(x);
|
||||
menuAnchor.setTranslationY(y);
|
||||
contextMenu.show();
|
||||
@@ -243,6 +302,8 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
UiUtils.confirmToggleMuteUser(fragment.getActivity(), accountID, account, relationship.muting, this::updateRelationship);
|
||||
}else if(id==R.id.block){
|
||||
UiUtils.confirmToggleBlockUser(fragment.getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
|
||||
}else if(id==R.id.soft_block){
|
||||
UiUtils.confirmSoftBlockUser(fragment.getActivity(), accountID, account, this::updateRelationship);
|
||||
}else if(id==R.id.report){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -252,10 +313,10 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
}else if(id==R.id.open_in_browser){
|
||||
UiUtils.launchWebBrowser(fragment.getActivity(), account.url);
|
||||
}else if(id==R.id.block_domain){
|
||||
UiUtils.confirmToggleBlockDomain(fragment.getActivity(), accountID, account, relationship.domainBlocking, ()->{
|
||||
UiUtils.confirmToggleBlockDomain(fragment.getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
|
||||
relationship.domainBlocking=!relationship.domainBlocking;
|
||||
bindRelationship();
|
||||
}, this::updateRelationship);
|
||||
});
|
||||
}else if(id==R.id.hide_boosts){
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
|
||||
.setCallback(new Callback<>(){
|
||||
|
||||
@@ -40,11 +40,9 @@ public abstract class ListItemViewHolder<T extends ListItem<?>> extends Bindable
|
||||
|
||||
if(TextUtils.isEmpty(item.subtitle) && item.subtitleRes==0){
|
||||
subtitle.setVisibility(View.GONE);
|
||||
title.setMaxLines(2);
|
||||
view.setMinimumHeight(V.dp(56));
|
||||
}else{
|
||||
subtitle.setVisibility(View.VISIBLE);
|
||||
title.setMaxLines(1);
|
||||
view.setMinimumHeight(V.dp(72));
|
||||
if(TextUtils.isEmpty(item.subtitle))
|
||||
subtitle.setText(item.subtitleRes);
|
||||
|
||||
@@ -3,13 +3,13 @@ package org.joinmastodon.android.ui.views;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeMediaLayout extends ViewGroup{
|
||||
private static final int MAX_WIDTH_DP=400;
|
||||
private static final int GAP_DP=8;
|
||||
private static final float ASPECT_RATIO=0.5625f;
|
||||
|
||||
@@ -30,7 +30,7 @@ public class ComposeMediaLayout extends ViewGroup{
|
||||
int mode=MeasureSpec.getMode(widthMeasureSpec);
|
||||
@SuppressLint("SwitchIntDef")
|
||||
int width=switch(mode){
|
||||
case MeasureSpec.AT_MOST -> Math.min(V.dp(MAX_WIDTH_DP), MeasureSpec.getSize(widthMeasureSpec));
|
||||
case MeasureSpec.AT_MOST -> Math.min(UiUtils.MAX_WIDTH, MeasureSpec.getSize(widthMeasureSpec));
|
||||
case MeasureSpec.EXACTLY -> MeasureSpec.getSize(widthMeasureSpec);
|
||||
default -> throw new IllegalArgumentException("unsupported measure mode");
|
||||
};
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class EmojiReactionsRecyclerView extends UsableRecyclerView{
|
||||
public EmojiReactionsRecyclerView(Context context){
|
||||
super(context);
|
||||
}
|
||||
|
||||
public EmojiReactionsRecyclerView(Context context, AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public EmojiReactionsRecyclerView(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent e){
|
||||
super.onTouchEvent(e);
|
||||
// to pass through touch events (i.e. clicking the status) to the parent view
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/55372837/is-there-a-way-to-make-recyclerview-requiresfadingedge-unaffected-by-paddingtop
|
||||
@Override
|
||||
protected boolean isPaddingOffsetRequired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLeftPaddingOffset(){
|
||||
return -getPaddingLeft();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getRightPaddingOffset() {
|
||||
return getPaddingRight();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android.ui.views;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -25,6 +26,8 @@ public class FilterChipView extends CheckIconSelectableTextView{
|
||||
setCompoundDrawablePadding(V.dp(8));
|
||||
setBackgroundResource(R.drawable.bg_filter_chip);
|
||||
setTextAppearance(R.style.m3_label_large);
|
||||
setHeight(V.dp(48));
|
||||
setGravity(Gravity.CENTER_VERTICAL);
|
||||
setTextColor(getResources().getColorStateList(R.color.filter_chip_text, context.getTheme()));
|
||||
updatePadding();
|
||||
}
|
||||
@@ -36,7 +39,7 @@ public class FilterChipView extends CheckIconSelectableTextView{
|
||||
}
|
||||
|
||||
private void updatePadding(){
|
||||
int vertical=V.dp(6);
|
||||
int vertical=0;
|
||||
Drawable[] drawables=getCompoundDrawablesRelative();
|
||||
setPaddingRelative(V.dp(drawables[0]==null ? 16 : 8), vertical, V.dp(drawables[2]==null ? 16 : 8), vertical);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,40 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
/**
|
||||
* A LinearLayout for TextViews. First child TextView will get truncated if it doesn't fit, remaining will always wrap content.
|
||||
*/
|
||||
public class HeaderSubtitleLinearLayout extends LinearLayout{
|
||||
private float firstFraction;
|
||||
|
||||
public HeaderSubtitleLinearLayout(Context context){
|
||||
super(context);
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public HeaderSubtitleLinearLayout(Context context, AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public HeaderSubtitleLinearLayout(Context context, AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.HeaderSubtitleLinearLayout);
|
||||
firstFraction=ta.getFraction(R.styleable.HeaderSubtitleLinearLayout_firstFraction, 1, 1, 0.5f);
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
if(getLayoutChildCount()>1){
|
||||
int remainingWidth=MeasureSpec.getSize(widthMeasureSpec);
|
||||
int fullWidth=MeasureSpec.getSize(widthMeasureSpec);
|
||||
int remainingWidth=fullWidth;
|
||||
for(int i=1;i<getChildCount();i++){
|
||||
View v=getChildAt(i);
|
||||
if(v.getVisibility()==GONE)
|
||||
@@ -36,7 +45,7 @@ public class HeaderSubtitleLinearLayout extends LinearLayout{
|
||||
}
|
||||
View first=getChildAt(0);
|
||||
if(first instanceof TextView){
|
||||
((TextView) first).setMaxWidth(remainingWidth);
|
||||
((TextView) first).setMaxWidth(Math.max(remainingWidth, (int)(firstFraction*fullWidth)));
|
||||
}
|
||||
}else{
|
||||
View first=getChildAt(0);
|
||||
@@ -55,4 +64,12 @@ public class HeaderSubtitleLinearLayout extends LinearLayout{
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setFirstFraction(float firstFraction){
|
||||
this.firstFraction=firstFraction;
|
||||
}
|
||||
|
||||
public float getFirstFraction(){
|
||||
return firstFraction;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.Switch;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class ListEditor extends LinearLayout {
|
||||
private ListTimeline.RepliesPolicy policy = null;
|
||||
private final TextInputFrameLayout input;
|
||||
private final Button button;
|
||||
private final Switch exclusiveSwitch;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public ListEditor(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
LayoutInflater.from(context).inflate(R.layout.list_timeline_editor, this);
|
||||
|
||||
button = findViewById(R.id.button);
|
||||
input = findViewById(R.id.input);
|
||||
exclusiveSwitch = findViewById(R.id.exclusive_checkbox);
|
||||
|
||||
PopupMenu popupMenu = new PopupMenu(context, button, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.list_reply_policies);
|
||||
popupMenu.setOnMenuItemClickListener(this::onMenuItemClick);
|
||||
|
||||
button.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||
button.setOnClickListener(v->popupMenu.show());
|
||||
input.getEditText().setHint(context.getString(R.string.sk_list_name_hint));
|
||||
findViewById(R.id.exclusive)
|
||||
.setOnClickListener(v -> exclusiveSwitch.setChecked(!exclusiveSwitch.isChecked()));
|
||||
|
||||
setRepliesPolicy(ListTimeline.RepliesPolicy.LIST);
|
||||
}
|
||||
|
||||
public void applyList(String title, boolean exclusive, @Nullable ListTimeline.RepliesPolicy policy) {
|
||||
input.getEditText().setText(title);
|
||||
exclusiveSwitch.setChecked(exclusive);
|
||||
if (policy != null) setRepliesPolicy(policy);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return input.getEditText().getText().toString();
|
||||
}
|
||||
|
||||
public ListTimeline.RepliesPolicy getRepliesPolicy() {
|
||||
return policy;
|
||||
}
|
||||
|
||||
public boolean isExclusive() {
|
||||
return exclusiveSwitch.isChecked();
|
||||
}
|
||||
|
||||
public void setRepliesPolicy(@NonNull ListTimeline.RepliesPolicy policy) {
|
||||
this.policy = policy;
|
||||
switch (policy) {
|
||||
case FOLLOWED -> button.setText(R.string.sk_list_replies_policy_followed);
|
||||
case LIST -> button.setText(R.string.sk_list_replies_policy_list);
|
||||
case NONE -> button.setText(R.string.sk_list_replies_policy_none);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean onMenuItemClick(MenuItem i) {
|
||||
if (i.getItemId() == R.id.reply_policy_none) {
|
||||
setRepliesPolicy(ListTimeline.RepliesPolicy.NONE);
|
||||
} else if (i.getItemId() == R.id.reply_policy_followed) {
|
||||
setRepliesPolicy(ListTimeline.RepliesPolicy.FOLLOWED);
|
||||
} else if (i.getItemId() == R.id.reply_policy_list) {
|
||||
setRepliesPolicy(ListTimeline.RepliesPolicy.LIST);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public ListEditor(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public ListEditor(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ListEditor(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,13 @@ package org.joinmastodon.android.ui.views;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
public class MaxWidthFrameLayout extends FrameLayout{
|
||||
private int maxWidth;
|
||||
private int maxWidth, defaultWidth;
|
||||
|
||||
public MaxWidthFrameLayout(Context context){
|
||||
this(context, null);
|
||||
@@ -22,6 +23,7 @@ public class MaxWidthFrameLayout extends FrameLayout{
|
||||
super(context, attrs, defStyle);
|
||||
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.MaxWidthFrameLayout);
|
||||
maxWidth=ta.getDimensionPixelSize(R.styleable.MaxWidthFrameLayout_android_maxWidth, Integer.MAX_VALUE);
|
||||
defaultWidth=ta.getDimensionPixelSize(R.styleable.MaxWidthFrameLayout_defaultWidth, -1);
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
@@ -33,10 +35,19 @@ public class MaxWidthFrameLayout extends FrameLayout{
|
||||
this.maxWidth=maxWidth;
|
||||
}
|
||||
|
||||
public int getDefaultWidth() {
|
||||
return defaultWidth;
|
||||
}
|
||||
|
||||
public void setDefaultWidth(int defaultWidth) {
|
||||
this.defaultWidth = defaultWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
if(MeasureSpec.getSize(widthMeasureSpec)>maxWidth){
|
||||
widthMeasureSpec=maxWidth | MeasureSpec.getMode(widthMeasureSpec);
|
||||
int width = defaultWidth >= 0 ? defaultWidth : maxWidth;
|
||||
widthMeasureSpec=width | MeasureSpec.getMode(widthMeasureSpec);
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class MediaGridLayout extends ViewGroup{
|
||||
private static final String TAG="MediaGridLayout";
|
||||
|
||||
public static final int MAX_WIDTH=400; // dp
|
||||
private static final int GAP=2; // dp
|
||||
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
|
||||
private int[] columnStarts, columnEnds, rowStarts, rowEnds;
|
||||
@@ -27,7 +27,6 @@ public class MediaGridLayout extends ViewGroup{
|
||||
|
||||
public MediaGridLayout(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -36,7 +35,7 @@ public class MediaGridLayout extends ViewGroup{
|
||||
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 0);
|
||||
return;
|
||||
}
|
||||
int width=Math.min(V.dp(MAX_WIDTH), MeasureSpec.getSize(widthMeasureSpec));
|
||||
int width=Math.min(UiUtils.MAX_WIDTH, MeasureSpec.getSize(widthMeasureSpec));
|
||||
int height=Math.round(width*(tiledLayout.height/(float)PhotoLayoutHelper.MAX_WIDTH));
|
||||
if(tiledLayout.width<PhotoLayoutHelper.MAX_WIDTH){
|
||||
width=Math.round(width*(tiledLayout.width/(float)PhotoLayoutHelper.MAX_WIDTH));
|
||||
@@ -85,7 +84,7 @@ public class MediaGridLayout extends ViewGroup{
|
||||
if(tiledLayout==null || rowStarts==null)
|
||||
return;
|
||||
|
||||
int maxWidth=V.dp(MAX_WIDTH);
|
||||
int maxWidth=UiUtils.MAX_WIDTH;
|
||||
if(tiledLayout.width<PhotoLayoutHelper.MAX_WIDTH){
|
||||
maxWidth=Math.round((r-l)*(tiledLayout.width/(float)PhotoLayoutHelper.MAX_WIDTH));
|
||||
}
|
||||
|
||||
@@ -1,42 +1,23 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
public class ProgressBarButton extends Button{
|
||||
private boolean textVisible=true;
|
||||
private ProgressBar progressBar;
|
||||
private int progressBarID;
|
||||
|
||||
public ProgressBarButton(Context context){
|
||||
this(context, null);
|
||||
super(context);
|
||||
}
|
||||
|
||||
public ProgressBarButton(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public ProgressBarButton(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.ProgressBarButton);
|
||||
progressBarID=ta.getResourceId(R.styleable.ProgressBarButton_progressBar, 0);
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow(){
|
||||
super.onAttachedToWindow();
|
||||
if(progressBarID!=0){
|
||||
progressBar=((ViewGroup)getParent()).findViewById(progressBarID);
|
||||
}
|
||||
public ProgressBarButton(Context context, AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public void setTextVisible(boolean textVisible){
|
||||
@@ -48,19 +29,6 @@ public class ProgressBarButton extends Button{
|
||||
return textVisible;
|
||||
}
|
||||
|
||||
public void setProgressBarVisible(boolean visible){
|
||||
if(progressBar==null)
|
||||
throw new IllegalStateException("progressBar is not set");
|
||||
if(visible){
|
||||
setTextVisible(false);
|
||||
progressBar.setIndeterminateTintList(getTextColors());
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
}else{
|
||||
setTextVisible(true);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas){
|
||||
if(textVisible){
|
||||
|
||||
@@ -2,15 +2,12 @@ package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
@@ -284,7 +281,7 @@ public class ReorderableLinearLayout extends LinearLayout implements CustomViewH
|
||||
|
||||
private int getMaxDragScroll(){
|
||||
if(cachedMaxScrollSpeed==-1){
|
||||
cachedMaxScrollSpeed=getResources().getDimensionPixelSize(R.dimen.item_touch_helper_max_drag_scroll_per_frame);
|
||||
cachedMaxScrollSpeed=getResources().getDimensionPixelSize(androidx.recyclerview.R.dimen.item_touch_helper_max_drag_scroll_per_frame);
|
||||
}
|
||||
return cachedMaxScrollSpeed;
|
||||
}
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.text.Layout;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.dynamicanimation.animation.FloatValueHolder;
|
||||
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
|
||||
public class RippleAnimationTextView extends TextView implements CustomViewHelper{
|
||||
private final Paint animationPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private CharacterAnimationState[] charStates;
|
||||
private final ArgbEvaluator colorEvaluator=new ArgbEvaluator();
|
||||
private int runningAnimCount=0;
|
||||
private Runnable[] delayedAnimations1, delayedAnimations2;
|
||||
|
||||
public RippleAnimationTextView(Context context){
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public RippleAnimationTextView(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public RippleAnimationTextView(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter){
|
||||
super.onTextChanged(text, start, lengthBefore, lengthAfter);
|
||||
if(charStates!=null){
|
||||
for(CharacterAnimationState state:charStates){
|
||||
state.colorAnimation.cancel();
|
||||
state.shadowAnimation.cancel();
|
||||
state.scaleAnimation.cancel();
|
||||
}
|
||||
for(Runnable r:delayedAnimations1){
|
||||
if(r!=null)
|
||||
removeCallbacks(r);
|
||||
}
|
||||
for(Runnable r:delayedAnimations2){
|
||||
if(r!=null)
|
||||
removeCallbacks(r);
|
||||
}
|
||||
}
|
||||
charStates=new CharacterAnimationState[lengthAfter];
|
||||
delayedAnimations1=new Runnable[lengthAfter];
|
||||
delayedAnimations2=new Runnable[lengthAfter];
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas){
|
||||
if(runningAnimCount==0 && !areThereDelayedAnimations()){
|
||||
super.onDraw(canvas);
|
||||
return;
|
||||
}
|
||||
Layout layout=getLayout();
|
||||
animationPaint.set(getPaint());
|
||||
CharSequence text=layout.getText();
|
||||
for(int i=0;i<layout.getLineCount();i++){
|
||||
int baseline=layout.getLineBaseline(i);
|
||||
for(int offset=layout.getLineStart(i); offset<layout.getLineEnd(i); offset++){
|
||||
float x=layout.getPrimaryHorizontal(offset);
|
||||
CharacterAnimationState state=charStates[offset];
|
||||
if(state==null || state.scaleAnimation==null){
|
||||
animationPaint.setColor(getCurrentTextColor());
|
||||
animationPaint.clearShadowLayer();
|
||||
canvas.drawText(text, offset, offset+1, x, baseline, animationPaint);
|
||||
}else{
|
||||
animationPaint.setColor((int)colorEvaluator.evaluate(Math.max(0, Math.min(1, state.color.getValue())), getCurrentTextColor(), getLinkTextColors().getDefaultColor()));
|
||||
float scale=state.scale.getValue();
|
||||
int shadowAlpha=Math.round(255*Math.max(0, Math.min(1, state.shadowAlpha.getValue())));
|
||||
animationPaint.setShadowLayer(dp(4), 0, dp(3), (getPaint().linkColor & 0xFFFFFF) | (shadowAlpha << 24));
|
||||
canvas.save();
|
||||
canvas.scale(scale, scale, x, baseline);
|
||||
canvas.drawText(text, offset, offset+1, x, baseline, animationPaint);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void animate(int startIndex, int endIndex){
|
||||
for(int i=startIndex;i<endIndex;i++){
|
||||
CharacterAnimationState _state=charStates[i];
|
||||
if(_state==null){
|
||||
_state=charStates[i]=new CharacterAnimationState();
|
||||
}
|
||||
CharacterAnimationState state=_state;
|
||||
int finalI=i;
|
||||
postOnAnimationDelayed(()->{
|
||||
if(!state.colorAnimation.isRunning())
|
||||
runningAnimCount++;
|
||||
state.colorAnimation.animateToFinalPosition(1f);
|
||||
if(!state.shadowAnimation.isRunning())
|
||||
runningAnimCount++;
|
||||
state.shadowAnimation.animateToFinalPosition(0.3f);
|
||||
if(!state.scaleAnimation.isRunning())
|
||||
runningAnimCount++;
|
||||
state.scaleAnimation.animateToFinalPosition(1.2f);
|
||||
invalidate();
|
||||
|
||||
if(delayedAnimations1[finalI]!=null)
|
||||
removeCallbacks(delayedAnimations1[finalI]);
|
||||
if(delayedAnimations2[finalI]!=null)
|
||||
removeCallbacks(delayedAnimations2[finalI]);
|
||||
Runnable delay1=()->{
|
||||
if(!state.colorAnimation.isRunning())
|
||||
runningAnimCount++;
|
||||
state.colorAnimation.animateToFinalPosition(0f);
|
||||
if(!state.shadowAnimation.isRunning())
|
||||
runningAnimCount++;
|
||||
state.shadowAnimation.animateToFinalPosition(0f);
|
||||
invalidate();
|
||||
delayedAnimations1[finalI]=null;
|
||||
};
|
||||
Runnable delay2=()->{
|
||||
if(!state.scaleAnimation.isRunning())
|
||||
runningAnimCount++;
|
||||
state.scaleAnimation.animateToFinalPosition(1f);
|
||||
delayedAnimations2[finalI]=null;
|
||||
};
|
||||
delayedAnimations1[finalI]=delay1;
|
||||
delayedAnimations2[finalI]=delay2;
|
||||
postOnAnimationDelayed(delay1, 2000);
|
||||
postOnAnimationDelayed(delay2, 100);
|
||||
}, 20L*(i-startIndex));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean areThereDelayedAnimations(){
|
||||
for(Runnable r:delayedAnimations1){
|
||||
if(r!=null)
|
||||
return true;
|
||||
}
|
||||
for(Runnable r:delayedAnimations2){
|
||||
if(r!=null)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private class CharacterAnimationState extends FloatValueHolder{
|
||||
private final SpringAnimation scaleAnimation, colorAnimation, shadowAnimation;
|
||||
private final FloatValueHolder scale=new FloatValueHolder(1), color=new FloatValueHolder(), shadowAlpha=new FloatValueHolder();
|
||||
|
||||
public CharacterAnimationState(){
|
||||
scaleAnimation=new SpringAnimation(scale);
|
||||
colorAnimation=new SpringAnimation(color);
|
||||
shadowAnimation=new SpringAnimation(shadowAlpha);
|
||||
setupSpring(scaleAnimation);
|
||||
setupSpring(colorAnimation);
|
||||
setupSpring(shadowAnimation);
|
||||
}
|
||||
|
||||
private void setupSpring(SpringAnimation anim){
|
||||
anim.setMinimumVisibleChange(0.01f);
|
||||
anim.setSpring(new SpringForce().setStiffness(500f).setDampingRatio(0.175f));
|
||||
anim.addEndListener((animation, canceled, value, velocity)->runningAnimCount--);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.function.IntConsumer;
|
||||
import java.util.function.IntPredicate;
|
||||
@@ -45,9 +47,7 @@ public class TabBar extends LinearLayout{
|
||||
listener.accept(v.getId());
|
||||
if(v.getId()==selectedTabID)
|
||||
return;
|
||||
findViewById(selectedTabID).setSelected(false);
|
||||
v.setSelected(true);
|
||||
selectedTabID=v.getId();
|
||||
selectTab(v.getId());
|
||||
}
|
||||
|
||||
private boolean onChildLongClick(View v){
|
||||
@@ -60,8 +60,17 @@ public class TabBar extends LinearLayout{
|
||||
}
|
||||
|
||||
public void selectTab(int id){
|
||||
findViewById(selectedTabID).setSelected(false);
|
||||
toggleSelected(selectedTabID, false);
|
||||
selectedTabID=id;
|
||||
findViewById(selectedTabID).setSelected(true);
|
||||
toggleSelected(id, true);
|
||||
}
|
||||
|
||||
private void toggleSelected(int selectedTabID, boolean selected){
|
||||
LinearLayout tab=findViewById(selectedTabID);
|
||||
tab.setSelected(selected);
|
||||
View v=tab.findViewWithTag("label");
|
||||
if(v instanceof TextView text){
|
||||
text.setTypeface(Typeface.create(text.getTypeface(), selected ? Typeface.BOLD : Typeface.NORMAL));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class TextInputFrameLayout extends FrameLayout {
|
||||
private final EditText editText;
|
||||
|
||||
public TextInputFrameLayout(@NonNull Context context, CharSequence hint, CharSequence text) {
|
||||
this(context, null, 0, 0, hint, text);
|
||||
}
|
||||
|
||||
public TextInputFrameLayout(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public TextInputFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public TextInputFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public TextInputFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
this(context, attrs, defStyleAttr, defStyleRes, null, null);
|
||||
}
|
||||
|
||||
public TextInputFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes, CharSequence hint, CharSequence text) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
editText = new EditText(context);
|
||||
editText.setHint(hint);
|
||||
editText.setText(text);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.setMargins(V.dp(24), V.dp(4), V.dp(24), 0);
|
||||
editText.setLayoutParams(params);
|
||||
addView(editText);
|
||||
}
|
||||
|
||||
public EditText getEditText() {
|
||||
return editText;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
public class UntouchableScrollView extends ScrollView {
|
||||
public UntouchableScrollView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public UntouchableScrollView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public UntouchableScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public UntouchableScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
super.onTouchEvent(event);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user