interact as other accounts

closes #199
This commit is contained in:
sk
2022-12-27 19:53:01 +01:00
parent 3af89a6175
commit 0f1456819b
10 changed files with 204 additions and 34 deletions

View File

@@ -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())
);
}
}
}

View File

@@ -19,12 +19,18 @@ import me.grishka.appkit.api.ErrorResponse;
public class StatusInteractionController{
private final String accountID;
private final boolean updateCounters;
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
private final HashMap<String, SetStatusBookmarked> 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<Status> 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<Status> 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));
}
}

View File

@@ -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());

View File

@@ -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");

View File

@@ -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);

View File

@@ -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<AccountSession> sessionConsumer, Consumer<AlertDialog.Builder> transformDialog) {
List<AccountSession> 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<Status> resultConsumer);
}
public static void pickInteractAs(Context context, String accountID, Status sourceStatus, Predicate<Status> 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<Status> 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<ProgressDialog> transformDialogForLookup = dialog -> {
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<String> 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;
}

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M15.75 13.997c0.981 0 1.815 0.628 2.123 1.503h-2.056l-0.066-0.003H4.249c-0.414 0-0.749 0.335-0.749 0.75v0.577c0 0.535 0.191 1.054 0.539 1.46 1.253 1.469 3.22 2.214 5.957 2.214h0.168c0.086 0.183 0.203 0.352 0.35 0.498l0.934 0.933c-0.464 0.046-0.948 0.07-1.452 0.07-3.145 0-5.531-0.906-7.098-2.74C2.318 18.58 2 17.717 2 16.824v-0.578c0-1.242 1.007-2.249 2.249-2.249H15.75zM9.997 2.002c2.762 0 5 2.239 5 5s-2.238 5-5 5c-2.761 0-5-2.239-5-5s2.239-5 5-5zm0 1.5c-1.933 0-3.5 1.567-3.5 3.5s1.567 3.5 3.5 3.5c1.934 0 3.5-1.567 3.5-3.5s-1.566-3.5-3.5-3.5zm4.784 14.282c0.293-0.293 0.293-0.768 0-1.061-0.293-0.293-0.768-0.293-1.06 0l-2.5 2.501c-0.141 0.14-0.22 0.332-0.22 0.53 0 0.2 0.079 0.39 0.22 0.53l2.5 2.5c0.292 0.293 0.767 0.293 1.06 0 0.293-0.293 0.293-0.768 0-1.061l-1.22-1.22h6.88l-1.22 1.22c-0.293 0.293-0.293 0.768 0 1.06 0.292 0.293 0.767 0.294 1.06 0l2.5-2.498c0.14-0.14 0.22-0.33 0.22-0.53 0-0.199-0.08-0.39-0.22-0.53l-2.5-2.502c-0.293-0.293-0.767-0.293-1.06 0-0.293 0.293-0.293 0.768 0 1.06l1.218 1.22h-6.877l1.22-1.22z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -78,6 +78,20 @@
android:layout_width="match_parent"
android:layout_marginVertical="8dp"
android:background="?colorPollVoted" />
<TextView
android:id="@+id/reblog_as"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="36dp"
android:paddingVertical="12dp"
android:paddingHorizontal="24dp"
android:gravity="center_vertical"
android:textSize="16sp"
android:textColor="?android:textColorPrimary"
android:drawablePadding="16dp"
android:background="?android:selectableItemBackground"
android:text="@string/sk_reblog_as"
android:drawableStart="@drawable/ic_fluent_person_swap_24_regular" />
<TextView
android:id="@+id/quote"
android:layout_width="match_parent"

View File

@@ -91,4 +91,14 @@
<string name="sk_copy_link_to_post">Copy link to post</string>
<string name="sk_open_with_account">Open with other account</string>
<string name="sk_resource_not_found">Resource could not be found</string>
<string name="sk_bookmark_as">Bookmark in other account</string>
<string name="sk_bookmarked_as">Bookmarked as %s</string>
<string name="sk_already_bookmarked">Already bookmarked</string>
<string name="sk_favorite_as">Favorite from other account</string>
<string name="sk_favorited_as">Favorited as %s</string>
<string name="sk_already_favorited">Already favorited</string>
<string name="sk_reblog_as">Reblog from other account</string>
<string name="sk_reblogged_as">Reblogged as %s</string>
<string name="sk_already_reblogged">Already reblogged</string>
<string name="sk_reply_as">Reply with other account</string>
</resources>

View File

@@ -366,7 +366,7 @@
<item name="android:textColor">?android:textColorPrimary</item>
<item name="android:textSize">24dp</item>
<item name="android:minHeight">38dp</item>
<item name="android:gravity">bottom</item>
<item name="android:gravity">center_vertical</item>
</style>
<style name="m3_body_large">