implement fetching listings from remote instances

This commit is contained in:
sk 2023-06-06 17:04:29 +02:00
parent 969f29e2e9
commit 4258c55b88
19 changed files with 422 additions and 53 deletions

View File

@ -48,6 +48,7 @@ public class GlobalUserPreferences{
public static boolean replyLineAboveHeader; public static boolean replyLineAboveHeader;
public static boolean compactReblogReplyLine; public static boolean compactReblogReplyLine;
public static boolean confirmBeforeReblog; public static boolean confirmBeforeReblog;
public static boolean allowRemoteLoading;
public static String publishButtonText; public static String publishButtonText;
public static ThemePreference theme; public static ThemePreference theme;
public static ColorPreference color; public static ColorPreference color;
@ -127,6 +128,7 @@ public class GlobalUserPreferences{
replyVisibility=prefs.getString("replyVisibility", null); replyVisibility=prefs.getString("replyVisibility", null);
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>()); accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>()); accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
try { try {
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name())); color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
@ -176,6 +178,7 @@ public class GlobalUserPreferences{
.putString("replyVisibility", replyVisibility) .putString("replyVisibility", replyVisibility)
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled) .putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes)) .putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
.putBoolean("allowRemoteLoading", allowRemoteLoading)
.apply(); .apply();
} }

View File

@ -20,9 +20,11 @@ import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import me.grishka.appkit.api.APIRequest; import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
@ -44,7 +46,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
TypeToken<T> respTypeToken; TypeToken<T> respTypeToken;
Call okhttpCall; Call okhttpCall;
Token token; Token token;
boolean canceled; boolean canceled, isRemote;
Map<String, String> headers; Map<String, String> headers;
private ProgressDialog progressDialog; private ProgressDialog progressDialog;
protected boolean removeUnsupportedItems; protected boolean removeUnsupportedItems;
@ -101,6 +103,21 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
return this; return this;
} }
public MastodonAPIRequest<T> execRemote(String domain) {
return execRemote(domain, null);
}
public MastodonAPIRequest<T> 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<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){ public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
return wrapProgress(activity, message, cancelable, null); return wrapProgress(activity, message, cancelable, null);
} }
@ -167,6 +184,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
@CallSuper @CallSuper
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{ public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
if(respObj instanceof BaseModel){ if(respObj instanceof BaseModel){
((BaseModel) respObj).isRemote = isRemote;
((BaseModel) respObj).postprocess(); ((BaseModel) respObj).postprocess();
}else if(respObj instanceof List){ }else if(respObj instanceof List){
if(removeUnsupportedItems){ if(removeUnsupportedItems){
@ -175,6 +193,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
Object item=itr.next(); Object item=itr.next();
if(item instanceof BaseModel){ if(item instanceof BaseModel){
try{ try{
((BaseModel) item).isRemote = isRemote;
((BaseModel) item).postprocess(); ((BaseModel) item).postprocess();
}catch(ObjectValidationException x){ }catch(ObjectValidationException x){
Log.w(TAG, "Removing invalid object from list", x); Log.w(TAG, "Removing invalid object from list", x);
@ -182,15 +201,20 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
} }
} }
} }
// 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)){ for(Object item:((List<?>) respObj)){
if(item instanceof BaseModel){ if(item instanceof BaseModel){
((BaseModel) item).isRemote = isRemote;
((BaseModel) item).postprocess(); ((BaseModel) item).postprocess();
} }
} }
}else{ }else{
for(Object item:((List<?>) respObj)){ for(Object item:((List<?>) respObj)){
if(item instanceof BaseModel) if(item instanceof BaseModel) {
((BaseModel) item).isRemote = isRemote;
((BaseModel) item).postprocess(); ((BaseModel) item).postprocess();
}
} }
} }
} }

View File

@ -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<Account>{
/**
* 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);
}
}

View File

@ -160,6 +160,11 @@ public class AccountSessionManager{
return sessions.get(id); return sessions.get(id);
} }
@Nullable
public AccountSession tryGetAccount(Account account) {
return sessions.get(account.getDomainFromURL() + "_" + account.id);
}
@Nullable @Nullable
public AccountSession getLastActiveAccount(){ public AccountSession getLastActiveAccount(){
if(sessions.isEmpty() || lastActiveAccountID==null) if(sessions.isEmpty() || lastActiveAccountID==null)

View File

@ -45,6 +45,7 @@ import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; 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.GetAccountByID;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses; import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
@ -137,7 +138,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private TextView followsYouView; private TextView followsYouView;
private ViewGroup rolesView; private ViewGroup rolesView;
private Account account; private Account account, remoteAccount;
private String accountID; private String accountID;
private String domain; private String domain;
private Relationship relationship; private Relationship relationship;
@ -176,7 +177,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
domain=AccountSessionManager.getInstance().getAccount(accountID).domain; 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")); account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
profileAccountID=account.id; profileAccountID=account.id;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account); isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
@ -347,36 +352,55 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return sizeWrapper; 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 @Override
protected void doLoadData(){ 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) currentRequest=new GetAccountByID(profileAccountID)
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(Account result){ public void onSuccess(Account result){
if (getActivity() == null) return; if (getActivity() == null) return;
account=result; onAccountLoaded(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);
} }
}) })
.exec(accountID); .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(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)); ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName); SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
HtmlParser.parseCustomEmoji(ssb, account.emojis); HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb); name.setText(ssb);
setTitle(ssb); setTitle(ssb);
if (account.roles != null && !account.roles.isEmpty()) { if (account.roles != null && !account.roles.isEmpty()) {
rolesView.setVisibility(View.VISIBLE); rolesView.setVisibility(View.VISIBLE);
@ -511,13 +535,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account); boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
String acct = ((isSelf || account.isRemote)
? account.getFullyQualifiedName()
: account.acct);
if(account.locked){ if(account.locked){
ssb=new SpannableStringBuilder("@"); ssb=new SpannableStringBuilder("@");
ssb.append(account.acct); ssb.append(acct);
if(isSelf){
ssb.append('@');
ssb.append(domain);
}
ssb.append(" "); ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate(); Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight()); lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
@ -526,7 +549,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username.setText(ssb); username.setText(ssb);
}else{ }else{
// noinspection SetTextI18n // noinspection SetTextI18n
username.setText('@'+account.acct+(isSelf ? ('@'+domain) : "")); username.setText('@'+acct);
} }
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID); CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(TextUtils.isEmpty(parsedBio)){ if(TextUtils.isEmpty(parsedBio)){

View File

@ -219,6 +219,11 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.confirmBeforeReblog=i.checked; GlobalUserPreferences.confirmBeforeReblog=i.checked;
GlobalUserPreferences.save(); 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 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->{ items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{

View File

@ -3,16 +3,27 @@ package org.joinmastodon.android.fragments.account_list;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; 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.joinmastodon.android.model.Account;
import org.parceler.Parcels; import org.parceler.Parcels;
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment{ import java.util.Optional;
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment<Account> {
protected Account account; protected Account account;
protected String initialSubtitle = "";
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount")); account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
if (getArguments().containsKey("remoteAccount")) {
remoteInfo = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
}
setTitle("@"+account.acct); setTitle("@"+account.acct);
} }
@ -22,4 +33,36 @@ public abstract class AccountRelatedAccountListFragment extends PaginatedAccount
? "/users/" + account.id ? "/users/" + account.id
: '@' + account.acct).build(); : '@' + account.acct).build();
} }
@Override
public String getRemoteDomain() {
return account.getDomainFromURL();
}
@Override
public Account getCurrentInfo() {
return doneWithHomeInstance && remoteInfo != null ? remoteInfo : account;
}
@Override
protected MastodonAPIRequest<Account> 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);
}
} }

View File

@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list; package org.joinmastodon.android.fragments.account_list;
import android.annotation.SuppressLint;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.app.assist.AssistContent; import android.app.assist.AssistContent;
import android.content.Intent; import android.content.Intent;
@ -47,6 +48,7 @@ import java.util.stream.Collectors;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest; import me.grishka.appkit.api.APIRequest;
@ -243,10 +245,13 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu); UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
} }
@SuppressLint("SetTextI18n")
@Override @Override
public void onBind(AccountItem item){ public void onBind(AccountItem item){
name.setText(item.parsedName); name.setText(item.parsedName);
username.setText("@"+item.account.acct); username.setText("@"+ (item.account.isRemote
? item.account.getFullyQualifiedName()
: item.account.acct));
bindRelationship(); bindRelationship();
} }
@ -282,7 +287,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
public void onClick(){ public void onClick(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); 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(getActivity(), ProfileFragment.class, args); Nav.go(getActivity(), ProfileFragment.class, args);
} }
@ -423,5 +429,10 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
emojiHelper=new CustomEmojiHelper(); emojiHelper=new CustomEmojiHelper();
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis)); emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
} }
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof AccountItem i && i.account.url.equals(account.url);
}
} }
} }

View File

@ -13,12 +13,12 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount)); setSubtitle(initialSubtitle = getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
} }
@Override @Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){ public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountFollowers(account.id, maxID, count); return new GetAccountFollowers(getCurrentInfo().id, maxID, count);
} }
@Override @Override

View File

@ -13,12 +13,12 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(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 @Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){ public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountFollowing(account.id, maxID, count); return new GetAccountFollowing(getCurrentInfo().id, maxID, count);
} }
@Override @Override

View File

@ -1,33 +1,173 @@
package org.joinmastodon.android.fragments.account_list; 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.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.HeaderPaginationList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
public abstract class PaginatedAccountListFragment extends BaseAccountListFragment{ public abstract class PaginatedAccountListFragment<T> extends BaseAccountListFragment{
private String nextMaxID; private String nextMaxID;
private MastodonAPIRequest<T> remoteInfoRequest;
protected boolean doneWithHomeInstance, remoteRequestFailed, startedRemoteLoading, remoteDisabled;
protected int localOffset;
protected T remoteInfo;
public abstract HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count); public abstract HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count);
protected abstract MastodonAPIRequest<T> 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 @Override
protected void doLoadData(int offset, int count){ 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){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(HeaderPaginationList<Account> result){ public void onSuccess(HeaderPaginationList<Account> result){
boolean justRefreshed = !doneWithHomeInstance && offset == 0;
Collection<AccountItem> d = justRefreshed ? List.of() : data;
if(result.nextPageUri!=null) if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id"); nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else else
nextMaxID=null; nextMaxID=null;
if (getActivity() == null) return; if (getActivity() == null) return;
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null); List<AccountItem> 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 @Override

View File

@ -7,17 +7,23 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest; import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusFavorites; import org.joinmastodon.android.api.requests.statuses.GetStatusFavorites;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
public class StatusFavoritesListFragment extends StatusRelatedAccountListFragment{ public class StatusFavoritesListFragment extends StatusRelatedAccountListFragment{
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(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)); setTitle(getResources().getQuantityString(R.plurals.x_favorites, (int)(status.favouritesCount%1000), status.favouritesCount));
} }
@Override @Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){ public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetStatusFavorites(status.id, maxID, count); return new GetStatusFavorites(getCurrentInfo().id, maxID, count);
} }
@Override @Override

View File

@ -7,17 +7,23 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest; import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusReblogs; import org.joinmastodon.android.api.requests.statuses.GetStatusReblogs;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{ public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(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)); setTitle(getResources().getQuantityString(R.plurals.x_reblogs, (int)(status.reblogsCount%1000), status.reblogsCount));
} }
@Override @Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){ public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetStatusReblogs(status.id, maxID, count); return new GetStatusReblogs(getCurrentInfo().id, maxID, count);
} }
@Override @Override

View File

@ -3,12 +3,27 @@ package org.joinmastodon.android.fragments.account_list;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; 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.joinmastodon.android.model.Status;
import org.parceler.Parcels; import org.parceler.Parcels;
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment{ import java.util.Optional;
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment<Status> {
protected Status status; protected Status status;
protected abstract void updateTitle(Status status);
protected MastodonAPIRequest<Status> loadRemoteInfo() {
String[] parts = status.url.split("/");
if (parts.length == 0) return null;
return new GetStatusByID(parts[parts.length - 1]);
}
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -17,7 +32,7 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
@Override @Override
protected boolean hasSubtitle(){ protected boolean hasSubtitle(){
return false; return remoteRequestFailed;
} }
@Override @Override
@ -28,4 +43,35 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
: '@' + status.account.acct + '/' + status.id) : '@' + status.account.acct + '/' + status.id)
.build(); .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();
}
} }

View File

@ -1,7 +1,10 @@
package org.joinmastodon.android.model; package org.joinmastodon.android.model;
import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.Nullable;
import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField; import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel; import org.parceler.Parcel;
@ -135,6 +138,8 @@ public class Account extends BaseModel implements Searchable{
public List<Role> roles; public List<Role> roles;
public @Nullable String fqn; // akkoma has this, mastodon't
@Override @Override
public String getQuery() { public String getQuery() {
return url; return url;
@ -162,6 +167,7 @@ public class Account extends BaseModel implements Searchable{
moved.postprocess(); moved.postprocess();
if(TextUtils.isEmpty(displayName)) if(TextUtils.isEmpty(displayName))
displayName=username; displayName=username;
if(fqn == null) fqn = getFullyQualifiedName();
} }
public boolean isLocal(){ public boolean isLocal(){
@ -173,6 +179,10 @@ public class Account extends BaseModel implements Searchable{
return parts.length==1 ? null : parts[1]; return parts.length==1 ? null : parts[1];
} }
public String getDomainFromURL() {
return Uri.parse(url).getHost();
}
public String getDisplayUsername(){ public String getDisplayUsername(){
return '@'+acct; return '@'+acct;
} }
@ -181,6 +191,10 @@ public class Account extends BaseModel implements Searchable{
return '@'+acct.split("@")[0]; return '@'+acct.split("@")[0];
} }
public String getFullyQualifiedName() {
return fqn != null ? fqn : acct.split("@")[0] + "@" + getDomainFromURL();
}
@Override @Override
public String toString(){ public String toString(){
return "Account{"+ return "Account{"+

View File

@ -11,6 +11,14 @@ import androidx.annotation.CallSuper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
public abstract class BaseModel implements Cloneable{ public abstract class BaseModel implements Cloneable{
/**
* indicates the profile has been fetched from a foreign instance.
*
* @see MastodonAPIRequest#execRemote
*/
public transient boolean isRemote;
@CallSuper @CallSuper
public void postprocess() throws ObjectValidationException{ public void postprocess() throws ObjectValidationException{
try{ try{

View File

@ -56,6 +56,7 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.StatusInteractionController; import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked; import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; 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<Class<? extends Fragment>, Bundle> go) {
parseFediverseHandle(query).ifPresentOrElse(
handle -> lookupAccountHandle(context, accountID, handle, go),
() -> go.accept(null, null)
);
}
public static void lookupAccountHandle(Context context, String accountID, Pair<String, Optional<String>> queryHandle, BiConsumer<Class<? extends Fragment>, Bundle> go) { public static void lookupAccountHandle(Context context, String accountID, Pair<String, Optional<String>> queryHandle, BiConsumer<Class<? extends Fragment>, Bundle> go) {
String fullHandle = ("@" + queryHandle.first) + (queryHandle.second.map(domain -> "@" + domain).orElse("")); String fullHandle = ("@" + queryHandle.first) + (queryHandle.second.map(domain -> "@" + domain).orElse(""));
new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true) new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true)
@ -1153,12 +1160,18 @@ public class UiUtils {
return; return;
} }
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show(); 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); go.accept(null, null);
} }
@Override @Override
public void onError(ErrorResponse error) { 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); }).exec(accountID);
} }

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="M12 4.5c-4.694 0-8.5 3.806-8.5 8.5 0 2.345 0.948 4.466 2.484 6.005 0.293 0.293 0.293 0.768 0 1.06-0.294 0.293-0.769 0.293-1.061 0C3.118 18.256 2 15.758 2 13 2 7.477 6.477 3 12 3s10 4.477 10 10c0 2.758-1.118 5.256-2.923 7.065-0.292 0.293-0.767 0.293-1.06 0-0.293-0.292-0.294-0.767-0.001-1.06C19.552 17.467 20.5 15.345 20.5 13c0-4.694-3.806-8.5-8.5-8.5zM12 8c-2.761 0-5 2.239-5 5 0 1.382 0.56 2.632 1.466 3.537 0.293 0.293 0.293 0.768 0 1.06-0.292 0.294-0.767 0.294-1.06 0.001C6.229 16.423 5.5 14.796 5.5 13c0-3.59 2.91-6.5 6.5-6.5s6.5 2.91 6.5 6.5c0 1.796-0.73 3.423-1.906 4.598-0.293 0.293-0.768 0.293-1.06 0-0.293-0.293-0.293-0.768 0-1.06C16.44 15.631 17 14.381 17 13c0-2.761-2.239-5-5-5zm0 2.5c-1.38 0-2.5 1.12-2.5 2.5s1.12 2.5 2.5 2.5 2.5-1.12 2.5-2.5-1.12-2.5-2.5-2.5zM11 13c0-0.552 0.448-1 1-1s1 0.448 1 1-0.448 1-1 1-1-0.448-1-1z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -293,4 +293,8 @@
<string name="sk_open_in_app_failed">Could not open in app</string> <string name="sk_open_in_app_failed">Could not open in app</string>
<string name="sk_external_share_title">Share with account</string> <string name="sk_external_share_title">Share with account</string>
<string name="sk_external_share_or_open_title">Share or open with account</string> <string name="sk_external_share_or_open_title">Share or open with account</string>
<string name="sk_no_remote_info_hint">remote info unavailable</string>
<string name="sk_error_loading_profile">Failed loading the profile on your home instance.</string>
<string name="sk_settings_allow_remote_loading">Load info from remote instances</string>
<string name="sk_settings_allow_remote_loading_explanation">Try fetching more accurate listings for followers, likes and boosts by loading the information from the instance of origin.</string>
</resources> </resources>