From 4258c55b88d195ac50223c5baf14091340129c9c Mon Sep 17 00:00:00 2001 From: sk Date: Tue, 6 Jun 2023 17:04:29 +0200 Subject: [PATCH] implement fetching listings from remote instances --- .../android/GlobalUserPreferences.java | 3 + .../android/api/MastodonAPIRequest.java | 28 +++- .../requests/accounts/GetAccountByHandle.java | 15 ++ .../api/session/AccountSessionManager.java | 5 + .../android/fragments/ProfileFragment.java | 91 +++++++---- .../android/fragments/SettingsFragment.java | 5 + .../AccountRelatedAccountListFragment.java | 45 +++++- .../account_list/BaseAccountListFragment.java | 15 +- .../account_list/FollowerListFragment.java | 4 +- .../account_list/FollowingListFragment.java | 4 +- .../PaginatedAccountListFragment.java | 150 +++++++++++++++++- .../StatusFavoritesListFragment.java | 8 +- .../StatusReblogsListFragment.java | 8 +- .../StatusRelatedAccountListFragment.java | 50 +++++- .../joinmastodon/android/model/Account.java | 14 ++ .../joinmastodon/android/model/BaseModel.java | 8 + .../android/ui/utils/UiUtils.java | 15 +- .../ic_fluent_communication_24_regular.xml | 3 + mastodon/src/main/res/values/strings_sk.xml | 4 + 19 files changed, 422 insertions(+), 53 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountByHandle.java create mode 100644 mastodon/src/main/res/drawable/ic_fluent_communication_24_regular.xml diff --git a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java index d789d321d..3366f213f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java +++ b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java @@ -48,6 +48,7 @@ public class GlobalUserPreferences{ public static boolean replyLineAboveHeader; public static boolean compactReblogReplyLine; public static boolean confirmBeforeReblog; + public static boolean allowRemoteLoading; public static String publishButtonText; public static ThemePreference theme; public static ColorPreference color; @@ -127,6 +128,7 @@ public class GlobalUserPreferences{ replyVisibility=prefs.getString("replyVisibility", null); accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>()); accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>()); + allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true); try { color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name())); @@ -176,6 +178,7 @@ public class GlobalUserPreferences{ .putString("replyVisibility", replyVisibility) .putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled) .putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes)) + .putBoolean("allowRemoteLoading", allowRemoteLoading) .apply(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java index 44a740401..f9dd1ee72 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java @@ -20,9 +20,11 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Consumer; import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import me.grishka.appkit.api.APIRequest; import me.grishka.appkit.api.Callback; @@ -44,7 +46,7 @@ public abstract class MastodonAPIRequest extends APIRequest{ TypeToken respTypeToken; Call okhttpCall; Token token; - boolean canceled; + boolean canceled, isRemote; Map headers; private ProgressDialog progressDialog; protected boolean removeUnsupportedItems; @@ -101,6 +103,21 @@ public abstract class MastodonAPIRequest extends APIRequest{ return this; } + public MastodonAPIRequest execRemote(String domain) { + return execRemote(domain, null); + } + + public MastodonAPIRequest execRemote(String domain, @Nullable AccountSession remoteSession) { + this.isRemote = true; + return Optional.ofNullable(remoteSession) + .or(() -> AccountSessionManager.getInstance().getLoggedInAccounts().stream() + .filter(acc -> acc.domain.equals(domain)) + .findAny()) + .map(AccountSession::getID) + .map(this::exec) + .orElse(this.execNoAuth(domain)); + } + public MastodonAPIRequest wrapProgress(Activity activity, @StringRes int message, boolean cancelable){ return wrapProgress(activity, message, cancelable, null); } @@ -167,6 +184,7 @@ public abstract class MastodonAPIRequest extends APIRequest{ @CallSuper public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{ if(respObj instanceof BaseModel){ + ((BaseModel) respObj).isRemote = isRemote; ((BaseModel) respObj).postprocess(); }else if(respObj instanceof List){ if(removeUnsupportedItems){ @@ -175,6 +193,7 @@ public abstract class MastodonAPIRequest extends APIRequest{ Object item=itr.next(); if(item instanceof BaseModel){ try{ + ((BaseModel) item).isRemote = isRemote; ((BaseModel) item).postprocess(); }catch(ObjectValidationException x){ Log.w(TAG, "Removing invalid object from list", x); @@ -182,15 +201,20 @@ public abstract class MastodonAPIRequest extends APIRequest{ } } } + // no idea why we're post-processing twice, but well, as long + // as upstream does it like this, i don't wanna break anything for(Object item:((List) respObj)){ if(item instanceof BaseModel){ + ((BaseModel) item).isRemote = isRemote; ((BaseModel) item).postprocess(); } } }else{ for(Object item:((List) respObj)){ - if(item instanceof BaseModel) + if(item instanceof BaseModel) { + ((BaseModel) item).isRemote = isRemote; ((BaseModel) item).postprocess(); + } } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountByHandle.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountByHandle.java new file mode 100644 index 000000000..011a8bf91 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountByHandle.java @@ -0,0 +1,15 @@ +package org.joinmastodon.android.api.requests.accounts; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Account; + +public class GetAccountByHandle extends MastodonAPIRequest{ + /** + * note that this method usually only returns a result if the instance already knows about an + * account - so it makes sense for looking up local users, search might be preferred otherwise + */ + public GetAccountByHandle(String acct){ + super(HttpMethod.GET, "/accounts/lookup", Account.class); + addQueryParameter("acct", acct); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java index 3b68ac36e..b2631b39c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java @@ -160,6 +160,11 @@ public class AccountSessionManager{ return sessions.get(id); } + @Nullable + public AccountSession tryGetAccount(Account account) { + return sessions.get(account.getDomainFromURL() + "_" + account.id); + } + @Nullable public AccountSession getLastActiveAccount(){ if(sessions.isEmpty() || lastActiveAccountID==null) diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java index d3c70d1d2..9f1c95296 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -45,6 +45,7 @@ import android.widget.Toolbar; import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; +import org.joinmastodon.android.api.MastodonErrorResponse; import org.joinmastodon.android.api.requests.accounts.GetAccountByID; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses; @@ -137,7 +138,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList private TextView followsYouView; private ViewGroup rolesView; - private Account account; + private Account account, remoteAccount; private String accountID; private String domain; private Relationship relationship; @@ -176,7 +177,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList accountID=getArguments().getString("account"); domain=AccountSessionManager.getInstance().getAccount(accountID).domain; - if(getArguments().containsKey("profileAccount")){ + if (getArguments().containsKey("remoteAccount")) { + remoteAccount = Parcels.unwrap(getArguments().getParcelable("remoteAccount")); + if(!getArguments().getBoolean("noAutoLoad", false)) + loadData(); + } else if(getArguments().containsKey("profileAccount")){ account=Parcels.unwrap(getArguments().getParcelable("profileAccount")); profileAccountID=account.id; isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account); @@ -347,36 +352,55 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList return sizeWrapper; } + private void onAccountLoaded(Account result) { + account=result; + isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account); + bindHeaderView(); + dataLoaded(); + if(!tabLayoutMediator.isAttached()) + tabLayoutMediator.attach(); + if(!isOwnProfile) + loadRelationship(); + else + AccountSessionManager.getInstance().updateAccountInfo(accountID, account); + if(refreshing){ + refreshing=false; + refreshLayout.setRefreshing(false); + if(postsFragment.loaded) + postsFragment.onRefresh(); + if(postsWithRepliesFragment.loaded) + postsWithRepliesFragment.onRefresh(); + if(pinnedPostsFragment.loaded) + pinnedPostsFragment.onRefresh(); + if(mediaFragment.loaded) + mediaFragment.onRefresh(); + } + V.setVisibilityAnimated(fab, View.VISIBLE); + } + @Override protected void doLoadData(){ + if (remoteAccount != null) { + UiUtils.lookupAccountHandle(getContext(), accountID, remoteAccount.getFullyQualifiedName(), (c, args) -> { + if (getContext() == null) return; + if (args == null || !args.containsKey("profileAccount")) { + onError(new MastodonErrorResponse( + getContext().getString(R.string.sk_error_loading_profile), + 0, null + )); + return; + } + onAccountLoaded(Parcels.unwrap(args.getParcelable("profileAccount"))); + }); + return; + } + currentRequest=new GetAccountByID(profileAccountID) .setCallback(new SimpleCallback<>(this){ @Override public void onSuccess(Account result){ if (getActivity() == null) return; - account=result; - isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account); - bindHeaderView(); - dataLoaded(); - if(!tabLayoutMediator.isAttached()) - tabLayoutMediator.attach(); - if(!isOwnProfile) - loadRelationship(); - else - AccountSessionManager.getInstance().updateAccountInfo(accountID, account); - if(refreshing){ - refreshing=false; - refreshLayout.setRefreshing(false); - if(postsFragment.loaded) - postsFragment.onRefresh(); - if(postsWithRepliesFragment.loaded) - postsWithRepliesFragment.onRefresh(); - if(pinnedPostsFragment.loaded) - pinnedPostsFragment.onRefresh(); - if(mediaFragment.loaded) - mediaFragment.onRefresh(); - } - V.setVisibilityAnimated(fab, View.VISIBLE); + onAccountLoaded(result); } }) .exec(accountID); @@ -490,9 +514,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100))); ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000)); SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName); - HtmlParser.parseCustomEmoji(ssb, account.emojis); - name.setText(ssb); - setTitle(ssb); + HtmlParser.parseCustomEmoji(ssb, account.emojis); + name.setText(ssb); + setTitle(ssb); if (account.roles != null && !account.roles.isEmpty()) { rolesView.setVisibility(View.VISIBLE); @@ -511,13 +535,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account); + String acct = ((isSelf || account.isRemote) + ? account.getFullyQualifiedName() + : account.acct); if(account.locked){ ssb=new SpannableStringBuilder("@"); - ssb.append(account.acct); - if(isSelf){ - ssb.append('@'); - ssb.append(domain); - } + ssb.append(acct); ssb.append(" "); Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate(); lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight()); @@ -526,7 +549,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList username.setText(ssb); }else{ // noinspection SetTextI18n - username.setText('@'+account.acct+(isSelf ? ('@'+domain) : "")); + username.setText('@'+acct); } CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID); if(TextUtils.isEmpty(parsedBio)){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java index b495b1316..fe1d8edea 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java @@ -219,6 +219,11 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide GlobalUserPreferences.confirmBeforeReblog=i.checked; GlobalUserPreferences.save(); })); + items.add(new SwitchItem(R.string.sk_settings_allow_remote_loading, R.drawable.ic_fluent_communication_24_regular, GlobalUserPreferences.allowRemoteLoading, i->{ + GlobalUserPreferences.allowRemoteLoading=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SmallTextItem(R.string.sk_settings_allow_remote_loading_explanation)); items.add(new HeaderItem(R.string.sk_timelines)); items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/AccountRelatedAccountListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/AccountRelatedAccountListFragment.java index 3a27bca56..43ad6ffa4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/AccountRelatedAccountListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/AccountRelatedAccountListFragment.java @@ -3,16 +3,27 @@ package org.joinmastodon.android.fragments.account_list; import android.net.Uri; import android.os.Bundle; +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.api.requests.accounts.GetAccountByHandle; +import org.joinmastodon.android.api.session.AccountSession; +import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.Account; import org.parceler.Parcels; -public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment{ +import java.util.Optional; + +public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment { protected Account account; + protected String initialSubtitle = ""; @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); account=Parcels.unwrap(getArguments().getParcelable("targetAccount")); + if (getArguments().containsKey("remoteAccount")) { + remoteInfo = Parcels.unwrap(getArguments().getParcelable("remoteAccount")); + } setTitle("@"+account.acct); } @@ -22,4 +33,36 @@ public abstract class AccountRelatedAccountListFragment extends PaginatedAccount ? "/users/" + account.id : '@' + account.acct).build(); } + + @Override + public String getRemoteDomain() { + return account.getDomainFromURL(); + } + + @Override + public Account getCurrentInfo() { + return doneWithHomeInstance && remoteInfo != null ? remoteInfo : account; + } + + @Override + protected MastodonAPIRequest loadRemoteInfo() { + return new GetAccountByHandle(account.acct); + } + + @Override + protected AccountSession getRemoteSession() { + return Optional.ofNullable(remoteInfo) + .map(AccountSessionManager.getInstance()::tryGetAccount) + .orElse(null); + } + + @Override + protected void onRemoteLoadingFailed() { + super.onRemoteLoadingFailed(); + String prefix = initialSubtitle == null ? "" : + initialSubtitle + " " + getContext().getString(R.string.sk_separator) + " "; + String str = prefix + + getContext().getString(R.string.sk_no_remote_info_hint, getSession().domain); + setSubtitle(str); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java index 312976597..bcf70060f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java @@ -1,5 +1,6 @@ package org.joinmastodon.android.fragments.account_list; +import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.app.assist.AssistContent; import android.content.Intent; @@ -47,6 +48,7 @@ import java.util.stream.Collectors; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.Nav; import me.grishka.appkit.api.APIRequest; @@ -243,10 +245,13 @@ public abstract class BaseAccountListFragment extends RecyclerFragment onCreateRequest(String maxID, int count){ - return new GetAccountFollowers(account.id, maxID, count); + return new GetAccountFollowers(getCurrentInfo().id, maxID, count); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FollowingListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FollowingListFragment.java index a9b73e921..0e1c2bacb 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FollowingListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FollowingListFragment.java @@ -13,12 +13,12 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{ @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); - setSubtitle(getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount)); + setSubtitle(initialSubtitle = getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount)); } @Override public HeaderPaginationRequest onCreateRequest(String maxID, int count){ - return new GetAccountFollowing(account.id, maxID, count); + return new GetAccountFollowing(getCurrentInfo().id, maxID, count); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/PaginatedAccountListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/PaginatedAccountListFragment.java index 5b34019f1..f6e77efe8 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/PaginatedAccountListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/PaginatedAccountListFragment.java @@ -1,33 +1,173 @@ package org.joinmastodon.android.fragments.account_list; +import android.os.Bundle; +import android.view.View; + +import org.joinmastodon.android.GlobalUserPreferences; +import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.requests.HeaderPaginationRequest; +import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.HeaderPaginationList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.stream.Collectors; +import me.grishka.appkit.api.Callback; +import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.SimpleCallback; -public abstract class PaginatedAccountListFragment extends BaseAccountListFragment{ +public abstract class PaginatedAccountListFragment extends BaseAccountListFragment{ private String nextMaxID; + private MastodonAPIRequest remoteInfoRequest; + protected boolean doneWithHomeInstance, remoteRequestFailed, startedRemoteLoading, remoteDisabled; + protected int localOffset; + protected T remoteInfo; public abstract HeaderPaginationRequest onCreateRequest(String maxID, int count); + protected abstract MastodonAPIRequest loadRemoteInfo(); + public abstract T getCurrentInfo(); + public abstract String getRemoteDomain(); + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + // already have remote info (e.g. from arguments), so no need to fetch it again + if (remoteInfo != null) { + onRemoteInfoLoaded(remoteInfo); + return; + } + + remoteDisabled = !GlobalUserPreferences.allowRemoteLoading + || getSession().domain.equals(getRemoteDomain()); + if (!remoteDisabled) { + remoteInfoRequest = loadRemoteInfo().setCallback(new Callback<>() { + @Override + public void onSuccess(T result) { + if (getContext() == null) return; + onRemoteInfoLoaded(result); + } + + @Override + public void onError(ErrorResponse error) { + if (getContext() == null) return; + onRemoteLoadingFailed(); + } + }); + remoteInfoRequest.execRemote(getRemoteDomain(), getRemoteSession()); + } + } + + /** + * override to provide an ideal account session (e.g. if you're logged into the author's remote + * account) to make the remote request from. if null is provided, will try to get any session + * on the remote domain, or tries the request without authentication. + */ + protected AccountSession getRemoteSession() { + return null; + } + + protected void onRemoteInfoLoaded(T info) { + this.remoteInfo = info; + this.remoteInfoRequest = null; + maybeStartLoadingRemote(); + } + + protected void onRemoteLoadingFailed() { + this.remoteRequestFailed = true; + this.remoteInfo = null; + this.remoteInfoRequest = null; + if (doneWithHomeInstance) dataLoaded(); + } + + @Override + public void dataLoaded() { + super.dataLoaded(); + footerProgress.setVisibility(View.GONE); + } + + private void maybeStartLoadingRemote() { + if (startedRemoteLoading || remoteDisabled) return; + if (!remoteRequestFailed) { + if (data.size() == 0) showProgress(); + else footerProgress.setVisibility(View.VISIBLE); + } + if (doneWithHomeInstance && remoteInfo != null) { + startedRemoteLoading = true; + loadData(localOffset, itemsPerPage * 2); + } + } + + @Override + public void onRefresh() { + localOffset = 0; + doneWithHomeInstance = false; + startedRemoteLoading = false; + super.onRefresh(); + } + + @Override + public void loadData(int offset, int count) { + // always subtract the amount loaded through the home instance once loading from remote + // since loadData gets called with data.size() (data includes both local and remote) + if (doneWithHomeInstance) offset -= localOffset; + super.loadData(offset, count); + } + @Override protected void doLoadData(int offset, int count){ - currentRequest=onCreateRequest(offset==0 ? null : nextMaxID, count) + MastodonAPIRequest request = onCreateRequest(offset==0 ? null : nextMaxID, count) .setCallback(new SimpleCallback<>(this){ @Override public void onSuccess(HeaderPaginationList result){ + boolean justRefreshed = !doneWithHomeInstance && offset == 0; + Collection d = justRefreshed ? List.of() : data; + if(result.nextPageUri!=null) nextMaxID=result.nextPageUri.getQueryParameter("max_id"); else nextMaxID=null; if (getActivity() == null) return; - onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null); + List items = result.stream() + .filter(a -> d.size() > 1000 || d.stream() + .noneMatch(i -> i.account.url.equals(a.url))) + .map(AccountItem::new) + .collect(Collectors.toList()); + + boolean hasMore = nextMaxID != null; + + if (!hasMore && !doneWithHomeInstance) { + // only runs last time data was fetched from the home instance + localOffset = d.size() + items.size(); + doneWithHomeInstance = true; + } + + onDataLoaded(items, hasMore); + if (doneWithHomeInstance) maybeStartLoadingRemote(); } - }) - .exec(accountID); + + @Override + public void onError(ErrorResponse error) { + if (doneWithHomeInstance) { + onRemoteLoadingFailed(); + onDataLoaded(Collections.emptyList(), false); + return; + } + super.onError(error); + } + }); + + if (doneWithHomeInstance && remoteInfo == null) return; // we are waiting + if (doneWithHomeInstance && remoteInfo != null) { + request.execRemote(getRemoteDomain(), getRemoteSession()); + } else { + request.exec(accountID); + } + currentRequest = request; } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusFavoritesListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusFavoritesListFragment.java index 915a8766e..20d0c2acf 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusFavoritesListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusFavoritesListFragment.java @@ -7,17 +7,23 @@ import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.HeaderPaginationRequest; import org.joinmastodon.android.api.requests.statuses.GetStatusFavorites; import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.Status; public class StatusFavoritesListFragment extends StatusRelatedAccountListFragment{ @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); + updateTitle(status); + } + + @Override + protected void updateTitle(Status status) { setTitle(getResources().getQuantityString(R.plurals.x_favorites, (int)(status.favouritesCount%1000), status.favouritesCount)); } @Override public HeaderPaginationRequest onCreateRequest(String maxID, int count){ - return new GetStatusFavorites(status.id, maxID, count); + return new GetStatusFavorites(getCurrentInfo().id, maxID, count); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusReblogsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusReblogsListFragment.java index 1308a14a9..3c5a5e228 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusReblogsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusReblogsListFragment.java @@ -7,17 +7,23 @@ import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.HeaderPaginationRequest; import org.joinmastodon.android.api.requests.statuses.GetStatusReblogs; import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.Status; public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{ @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); + updateTitle(status); + } + + @Override + protected void updateTitle(Status status) { setTitle(getResources().getQuantityString(R.plurals.x_reblogs, (int)(status.reblogsCount%1000), status.reblogsCount)); } @Override public HeaderPaginationRequest onCreateRequest(String maxID, int count){ - return new GetStatusReblogs(status.id, maxID, count); + return new GetStatusReblogs(getCurrentInfo().id, maxID, count); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusRelatedAccountListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusRelatedAccountListFragment.java index d0499afa6..aeee1fdc9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusRelatedAccountListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusRelatedAccountListFragment.java @@ -3,12 +3,27 @@ package org.joinmastodon.android.fragments.account_list; import android.net.Uri; import android.os.Bundle; +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.api.requests.statuses.GetStatusByID; +import org.joinmastodon.android.api.session.AccountSession; +import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.Status; import org.parceler.Parcels; -public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment{ +import java.util.Optional; + +public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment { protected Status status; + protected abstract void updateTitle(Status status); + + protected MastodonAPIRequest loadRemoteInfo() { + String[] parts = status.url.split("/"); + if (parts.length == 0) return null; + return new GetStatusByID(parts[parts.length - 1]); + } + @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); @@ -17,7 +32,7 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL @Override protected boolean hasSubtitle(){ - return false; + return remoteRequestFailed; } @Override @@ -28,4 +43,35 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL : '@' + status.account.acct + '/' + status.id) .build(); } + + @Override + public String getRemoteDomain() { + return Uri.parse(status.url).getHost(); + } + + @Override + public Status getCurrentInfo() { + return doneWithHomeInstance && remoteInfo != null ? remoteInfo : status; + } + + @Override + protected AccountSession getRemoteSession() { + return Optional.ofNullable(remoteInfo) + .map(s -> s.account) + .map(AccountSessionManager.getInstance()::tryGetAccount) + .orElse(null); + } + + @Override + protected void onRemoteInfoLoaded(Status info) { + super.onRemoteInfoLoaded(info); + updateTitle(remoteInfo); + } + + @Override + protected void onRemoteLoadingFailed() { + super.onRemoteLoadingFailed(); + setSubtitle(getContext().getString(R.string.sk_no_remote_info_hint, getSession().domain)); + updateToolbar(); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Account.java b/mastodon/src/main/java/org/joinmastodon/android/model/Account.java index b4424e0a6..1fdfe08cc 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Account.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Account.java @@ -1,7 +1,10 @@ package org.joinmastodon.android.model; +import android.net.Uri; import android.text.TextUtils; +import androidx.annotation.Nullable; + import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.RequiredField; import org.parceler.Parcel; @@ -135,6 +138,8 @@ public class Account extends BaseModel implements Searchable{ public List roles; + public @Nullable String fqn; // akkoma has this, mastodon't + @Override public String getQuery() { return url; @@ -162,6 +167,7 @@ public class Account extends BaseModel implements Searchable{ moved.postprocess(); if(TextUtils.isEmpty(displayName)) displayName=username; + if(fqn == null) fqn = getFullyQualifiedName(); } public boolean isLocal(){ @@ -173,6 +179,10 @@ public class Account extends BaseModel implements Searchable{ return parts.length==1 ? null : parts[1]; } + public String getDomainFromURL() { + return Uri.parse(url).getHost(); + } + public String getDisplayUsername(){ return '@'+acct; } @@ -181,6 +191,10 @@ public class Account extends BaseModel implements Searchable{ return '@'+acct.split("@")[0]; } + public String getFullyQualifiedName() { + return fqn != null ? fqn : acct.split("@")[0] + "@" + getDomainFromURL(); + } + @Override public String toString(){ return "Account{"+ diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/BaseModel.java b/mastodon/src/main/java/org/joinmastodon/android/model/BaseModel.java index b2ec8898f..051be6240 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/BaseModel.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/BaseModel.java @@ -11,6 +11,14 @@ import androidx.annotation.CallSuper; import androidx.annotation.NonNull; public abstract class BaseModel implements Cloneable{ + + /** + * indicates the profile has been fetched from a foreign instance. + * + * @see MastodonAPIRequest#execRemote + */ + public transient boolean isRemote; + @CallSuper public void postprocess() throws ObjectValidationException{ try{ 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 0c69a5a7d..82913f383 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 @@ -56,6 +56,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.MastodonErrorResponse; import org.joinmastodon.android.api.StatusInteractionController; import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked; import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; @@ -1136,6 +1137,12 @@ public class UiUtils { } } + public static void lookupAccountHandle(Context context, String accountID, String query, BiConsumer, Bundle> go) { + parseFediverseHandle(query).ifPresentOrElse( + handle -> lookupAccountHandle(context, accountID, handle, go), + () -> go.accept(null, null) + ); + } public static void lookupAccountHandle(Context context, String accountID, Pair> queryHandle, BiConsumer, Bundle> go) { String fullHandle = ("@" + queryHandle.first) + (queryHandle.second.map(domain -> "@" + domain).orElse("")); new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true) @@ -1153,12 +1160,18 @@ public class UiUtils { return; } Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show(); + args.putString("error", context.getString(R.string.sk_resource_not_found)); go.accept(null, null); } @Override public void onError(ErrorResponse error) { - + Bundle args = new Bundle(); + if (error instanceof MastodonErrorResponse e) { + args.putString("error", e.error); + args.putInt("httpStatus", e.httpStatus); + } + go.accept(null, args); } }).exec(accountID); } diff --git a/mastodon/src/main/res/drawable/ic_fluent_communication_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_communication_24_regular.xml new file mode 100644 index 000000000..a9f610543 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_communication_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/values/strings_sk.xml b/mastodon/src/main/res/values/strings_sk.xml index 9dc3bbbd3..221baaac9 100644 --- a/mastodon/src/main/res/values/strings_sk.xml +++ b/mastodon/src/main/res/values/strings_sk.xml @@ -293,4 +293,8 @@ Could not open in app Share with account Share or open with account + remote info unavailable + Failed loading the profile on your home instance. + Load info from remote instances + Try fetching more accurate listings for followers, likes and boosts by loading the information from the instance of origin. \ No newline at end of file