diff --git a/mastodon/src/main/java/org/joinmastodon/android/ExternalShareActivity.java b/mastodon/src/main/java/org/joinmastodon/android/ExternalShareActivity.java index c289bde53..e68eaea6d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ExternalShareActivity.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ExternalShareActivity.java @@ -12,7 +12,6 @@ import android.widget.Toast; import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.ComposeFragment; -import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.utils.UiUtils; import java.util.ArrayList; @@ -36,13 +35,10 @@ public class ExternalShareActivity extends FragmentStackActivity{ openComposeFragment(sessions.get(0).getID()); }else{ getWindow().setBackgroundDrawable(new ColorDrawable(0xff000000)); - new M3AlertDialogBuilder(this) - .setItems(sessions.stream().map(as->"@"+as.self.username+"@"+as.domain).toArray(String[]::new), (dialog, which)->{ - openComposeFragment(sessions.get(which).getID()); - }) - .setTitle(R.string.choose_account) - .setOnCancelListener(dialog -> finish()) - .show(); + UiUtils.pickAccount(this, null, R.string.choose_account, 0, + session -> openComposeFragment(session.getID()), + b -> b.setOnCancelListener(d -> finish()) + ); } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/StatusInteractionController.java b/mastodon/src/main/java/org/joinmastodon/android/api/StatusInteractionController.java index 33df962bf..ec45b6d90 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/StatusInteractionController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/StatusInteractionController.java @@ -19,12 +19,18 @@ import me.grishka.appkit.api.ErrorResponse; public class StatusInteractionController{ private final String accountID; + private final boolean updateCounters; private final HashMap runningFavoriteRequests=new HashMap<>(); private final HashMap runningReblogRequests=new HashMap<>(); private final HashMap runningBookmarkRequests=new HashMap<>(); - public StatusInteractionController(String accountID){ + public StatusInteractionController(String accountID, boolean updateCounters) { this.accountID=accountID; + this.updateCounters=updateCounters; + } + + public StatusInteractionController(String accountID){ + this(accountID, true); } public void setFavorited(Status status, boolean favorited, Consumer cb){ @@ -42,7 +48,7 @@ public class StatusInteractionController{ runningFavoriteRequests.remove(status.id); result.favouritesCount = Math.max(0, status.favouritesCount) + (favorited ? 1 : -1); cb.accept(result); - E.post(new StatusCountersUpdatedEvent(result)); + if (updateCounters) E.post(new StatusCountersUpdatedEvent(result)); } @Override @@ -51,13 +57,13 @@ public class StatusInteractionController{ error.showToast(MastodonApp.context); status.favourited=!favorited; cb.accept(status); - E.post(new StatusCountersUpdatedEvent(status)); + if (updateCounters) E.post(new StatusCountersUpdatedEvent(status)); } }) .exec(accountID); runningFavoriteRequests.put(status.id, req); status.favourited=favorited; - E.post(new StatusCountersUpdatedEvent(status)); + if (updateCounters) E.post(new StatusCountersUpdatedEvent(status)); } public void setReblogged(Status status, boolean reblogged, StatusPrivacy visibility, Consumer cb){ @@ -76,7 +82,7 @@ public class StatusInteractionController{ runningReblogRequests.remove(status.id); result.reblogsCount = Math.max(0, status.reblogsCount) + (reblogged ? 1 : -1); cb.accept(result); - E.post(new StatusCountersUpdatedEvent(result)); + if (updateCounters) E.post(new StatusCountersUpdatedEvent(result)); } @Override @@ -85,13 +91,13 @@ public class StatusInteractionController{ error.showToast(MastodonApp.context); status.reblogged=!reblogged; cb.accept(status); - E.post(new StatusCountersUpdatedEvent(status)); + if (updateCounters) E.post(new StatusCountersUpdatedEvent(status)); } }) .exec(accountID); runningReblogRequests.put(status.id, req); status.reblogged=reblogged; - E.post(new StatusCountersUpdatedEvent(status)); + if (updateCounters) E.post(new StatusCountersUpdatedEvent(status)); } public void setBookmarked(Status status, boolean bookmarked){ @@ -112,7 +118,7 @@ public class StatusInteractionController{ public void onSuccess(Status result){ runningBookmarkRequests.remove(status.id); cb.accept(result); - E.post(new StatusCountersUpdatedEvent(result)); + if (updateCounters) E.post(new StatusCountersUpdatedEvent(result)); } @Override @@ -121,12 +127,12 @@ public class StatusInteractionController{ error.showToast(MastodonApp.context); status.bookmarked=!bookmarked; cb.accept(status); - E.post(new StatusCountersUpdatedEvent(status)); + if (updateCounters) E.post(new StatusCountersUpdatedEvent(status)); } }) .exec(accountID); runningBookmarkRequests.put(status.id, req); status.bookmarked=bookmarked; - E.post(new StatusCountersUpdatedEvent(status)); + if (updateCounters) E.post(new StatusCountersUpdatedEvent(status)); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java index 735b19aca..e7fa19d9e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java @@ -32,7 +32,7 @@ public class AccountSession{ public Preferences preferences; public AccountActivationInfo activationInfo; private transient MastodonAPIController apiController; - private transient StatusInteractionController statusInteractionController; + private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController; private transient CacheController cacheController; private transient PushSubscriptionManager pushSubscriptionManager; @@ -52,6 +52,10 @@ public class AccountSession{ return domain+"_"+self.id; } + public String getFullUsername() { + return "@"+self.username+"@"+domain; + } + public MastodonAPIController getApiController(){ if(apiController==null) apiController=new MastodonAPIController(this); @@ -64,6 +68,12 @@ public class AccountSession{ return statusInteractionController; } + public StatusInteractionController getRemoteStatusInteractionController(){ + if(remoteStatusInteractionController==null) + remoteStatusInteractionController=new StatusInteractionController(getID(), false); + return remoteStatusInteractionController; + } + public CacheController getCacheController(){ if(cacheController==null) cacheController=new CacheController(getID()); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/M3AlertDialogBuilder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/M3AlertDialogBuilder.java index 566bde320..f67a0c457 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/M3AlertDialogBuilder.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/M3AlertDialogBuilder.java @@ -4,6 +4,7 @@ import android.app.AlertDialog; import android.content.Context; import android.view.View; import android.widget.Button; +import android.widget.ImageView; import me.grishka.appkit.utils.V; @@ -31,8 +32,16 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{ if(titleID!=0){ View title=alert.findViewById(titleID); if(title!=null){ + int iconID=getContext().getResources().getIdentifier("icon", "id", "android"); + int alertTitleID=getContext().getResources().getIdentifier("alertTitle", "id", "android"); + if (alertTitleID != 0 && iconID != 0) { + ImageView icon = title.findViewById(iconID); + if (icon.getDrawable() != null) { + title.findViewById(alertTitleID).setPadding(V.dp(8), 0, 0, 0); + } + } int pad=V.dp(24); - title.setPadding(pad, pad, pad, V.dp(18)); + title.setPadding(pad, pad, pad, V.dp(12)); } } int titleDividerID=getContext().getResources().getIdentifier("titleDividerNoCustom", "id", "android"); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java index bc54579b9..3075b79f5 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java @@ -101,6 +101,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ View bookmark=findViewById(R.id.bookmark_btn); reply.setOnTouchListener(this::onButtonTouch); reply.setOnClickListener(this::onReplyClick); + reply.setOnLongClickListener(this::onReplyLongClick); reply.setAccessibilityDelegate(buttonAccessibilityDelegate); boost.setOnTouchListener(this::onButtonTouch); boost.setOnClickListener(this::onBoostClick); @@ -108,9 +109,11 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ 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); @@ -172,6 +175,19 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args); } + private boolean onReplyLongClick(View v) { + UiUtils.pickAccount(v.getContext(), item.accountID, R.string.sk_reply_as, R.drawable.ic_fluent_arrow_reply_24_regular, session -> { + Bundle args=new Bundle(); + String accountID = session.getID(); + args.putString("account", accountID); + UiUtils.lookupStatus(v.getContext(), item.status, accountID, item.accountID, status -> { + args.putParcelable("replyTo", Parcels.wrap(status)); + Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args); + }); + }, null); + return true; + } + private void onBoostClick(View v){ boost.setSelected(!item.status.reblogged); AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, null, r->boostConsumer(v, r)); @@ -198,6 +214,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ 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); @@ -205,6 +222,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ 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 || item.status.visibility.isLessVisibleThan(StatusPrivacy.PUBLIC) ? View.GONE : View.VISIBLE); itemUnlisted.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) ? View.GONE : View.VISIBLE); @@ -234,6 +252,18 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ 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, + R.drawable.ic_fluent_arrow_repeat_all_24_regular + ); + }); menu.findViewById(R.id.quote).setOnClickListener(c->{ dialog.dismiss(); @@ -257,6 +287,19 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ }); } + private boolean onFavoriteLongClick(View v) { + 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, + R.drawable.ic_fluent_star_24_regular + ); + return true; + } + private void onBookmarkClick(View v){ bookmark.setSelected(!item.status.bookmarked); AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{ @@ -264,6 +307,19 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ }); } + private boolean onBookmarkLongClick(View v) { + 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_24_regular + ); + return true; + } + private void onShareClick(View v){ v.startAnimation(opacityIn); Intent intent=new Intent(Intent.ACTION_SEND); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index e11c94f7d..91625f0b7 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -5,6 +5,7 @@ import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme; import android.annotation.SuppressLint; import android.app.Activity; +import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.ActivityNotFoundException; import android.content.ClipData; @@ -26,8 +27,6 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.os.VibrationEffect; -import android.os.Vibrator; import android.provider.OpenableColumns; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -46,6 +45,7 @@ import org.joinmastodon.android.E; import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; +import org.joinmastodon.android.api.StatusInteractionController; import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked; import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; import org.joinmastodon.android.api.requests.accounts.SetAccountMuted; @@ -96,6 +96,7 @@ import java.util.List; import java.util.Map; import java.util.function.BiPredicate; import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.stream.Collectors; import androidx.annotation.AttrRes; @@ -755,24 +756,87 @@ public class UiUtils{ return instance != null && !instance.title.isBlank() ? instance.title : session.domain; } + public static void pickAccount(Context context, String exceptFor, @StringRes int titleRes, @DrawableRes int iconRes, Consumer sessionConsumer, Consumer transformDialog) { + List sessions=AccountSessionManager.getInstance().getLoggedInAccounts() + .stream().filter(s->!s.getID().equals(exceptFor)).collect(Collectors.toList()); + + AlertDialog.Builder builder = new M3AlertDialogBuilder(context) + .setItems( + sessions.stream().map(AccountSession::getFullUsername).toArray(String[]::new), + (dialog, which) -> sessionConsumer.accept(sessions.get(which)) + ) + .setTitle(titleRes == 0 ? R.string.choose_account : titleRes) + .setIcon(iconRes); + if (transformDialog != null) transformDialog.accept(builder); + builder.show(); + } + + @FunctionalInterface + public interface InteractionPerformer { + void interact(StatusInteractionController ic, Status status, Consumer resultConsumer); + } + + public static void pickInteractAs(Context context, String accountID, Status sourceStatus, Predicate checkInteracted, InteractionPerformer interactionPerformer, @StringRes int interactAsRes, @StringRes int interactedAsAccountRes, @StringRes int alreadyInteractedRes, @DrawableRes int iconRes) { + pickAccount(context, accountID, interactAsRes, iconRes, session -> { + lookupStatus(context, sourceStatus, session.getID(), accountID, status -> { + if (checkInteracted.test(status)) { + Toast.makeText(context, alreadyInteractedRes, Toast.LENGTH_SHORT).show(); + return; + } + + StatusInteractionController ic = AccountSessionManager.getInstance().getAccount(session.getID()).getRemoteStatusInteractionController(); + interactionPerformer.interact(ic, status, s -> { + if (checkInteracted.test(s)) { + Toast.makeText(context, context.getString(interactedAsAccountRes, session.getFullUsername()), Toast.LENGTH_SHORT).show(); + } + }); + }); + }, null); + } + + public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer statusConsumer) { + if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) { + statusConsumer.accept(queryStatus); + return; + } + + new GetSearchResults(queryStatus.url, GetSearchResults.Type.STATUSES, true).setCallback(new Callback<>() { + @Override + public void onSuccess(SearchResults results) { + if (!results.statuses.isEmpty()) statusConsumer.accept(results.statuses.get(0)); + else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onError(ErrorResponse error) { + error.showToast(context); + } + }) + .wrapProgress((Activity)context, R.string.loading, true, + d -> transformDialogForLookup(context, targetAccountID, null, d)) + .exec(targetAccountID); + } + public static void openURL(Context context, String accountID, String url) { openURL(context, accountID, url, true); } - public static void openURL(Context context, String accountID, String url, boolean launchBrowser){ - Consumer transformDialogForLookup = dialog -> { - if (accountID != null) { - dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID))); - } else { - dialog.setTitle(R.string.sk_loading_fediverse_resource_title); - } - dialog.setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.cancel), (d, which) -> d.cancel()); + private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) { + if (accountID != null) { + dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID))); + } else { + dialog.setTitle(R.string.sk_loading_fediverse_resource_title); + } + dialog.setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.cancel), (d, which) -> d.cancel()); + if (url != null) { dialog.setButton(DialogInterface.BUTTON_POSITIVE, context.getString(R.string.open_in_browser), (d, which) -> { d.cancel(); launchWebBrowser(context, url); }); - }; + } + } + public static void openURL(Context context, String accountID, String url, boolean launchBrowser){ Uri uri=Uri.parse(url); List path=uri.getPathSegments(); if(accountID!=null && "https".equals(uri.getScheme())){ @@ -793,7 +857,8 @@ public class UiUtils{ if (launchBrowser) launchWebBrowser(context, url); } }) - .wrapProgress((Activity)context, R.string.loading, true, transformDialogForLookup) + .wrapProgress((Activity)context, R.string.loading, true, + d -> transformDialogForLookup(context, accountID, url, d)) .exec(accountID); return; } else if (looksLikeMastodonUrl(url)) { @@ -821,7 +886,8 @@ public class UiUtils{ if (launchBrowser) launchWebBrowser(context, url); } }) - .wrapProgress((Activity)context, R.string.loading, true, transformDialogForLookup) + .wrapProgress((Activity)context, R.string.loading, true, + d -> transformDialogForLookup(context, accountID, url, d)) .exec(accountID); return; } diff --git a/mastodon/src/main/res/drawable/ic_fluent_person_swap_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_person_swap_24_regular.xml new file mode 100644 index 000000000..8b7c650c8 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_person_swap_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/layout/item_boost_menu.xml b/mastodon/src/main/res/layout/item_boost_menu.xml index fa19a8f6c..91069afcb 100644 --- a/mastodon/src/main/res/layout/item_boost_menu.xml +++ b/mastodon/src/main/res/layout/item_boost_menu.xml @@ -78,6 +78,20 @@ android:layout_width="match_parent" android:layout_marginVertical="8dp" android:background="?colorPollVoted" /> + Copy link to post Open with other account Resource could not be found + Bookmark in other account + Bookmarked as %s + Already bookmarked + Favorite from other account + Favorited as %s + Already favorited + Reblog from other account + Reblogged as %s + Already reblogged + Reply with other account \ No newline at end of file diff --git a/mastodon/src/main/res/values/styles.xml b/mastodon/src/main/res/values/styles.xml index f36c34b05..5764471b4 100644 --- a/mastodon/src/main/res/values/styles.xml +++ b/mastodon/src/main/res/values/styles.xml @@ -366,7 +366,7 @@ ?android:textColorPrimary 24dp 38dp - bottom + center_vertical