diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountByID.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountByID.java new file mode 100644 index 00000000..b178cc83 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountByID.java @@ -0,0 +1,10 @@ +package org.joinmastodon.android.api.requests.accounts; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Account; + +public class GetAccountByID extends MastodonAPIRequest{ + public GetAccountByID(String id){ + super(HttpMethod.GET, "/accounts/"+id, Account.class); + } +} 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 34e36531..645031cd 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -34,6 +34,7 @@ import android.widget.TextView; import android.widget.Toolbar; import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.accounts.GetAccountByID; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; @@ -71,6 +72,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.viewpager2.widget.ViewPager2; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; +import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.LoaderFragment; import me.grishka.appkit.fragments.OnBackPressedListener; @@ -100,6 +102,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList private EditText nameEdit, bioEdit; private ProgressBar actionProgress; private FrameLayout[] tabViews; + private TabLayoutMediator tabLayoutMediator; private Account account; private String accountID; @@ -110,6 +113,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList private boolean isInEditMode; private Uri editNewAvatar, editNewCover; + private String profileAccountID; public ProfileFragment(){ super(R.layout.loader_fragment_overlay_toolbar); @@ -119,16 +123,25 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setRetainInstance(true); - accountID=getArguments().getString("account"); + accountID=getArguments().getString("account"); + if(getArguments().containsKey("profileAccount")){ + account=Parcels.unwrap(getArguments().getParcelable("profileAccount")); + isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account); + loaded=true; + if(!isOwnProfile) + loadRelationship(); + }else{ + profileAccountID=getArguments().getString("profileAccountID"); + if(!getArguments().getBoolean("noAutoLoad", false)) + loadData(); + } } @Override public void onAttach(Activity activity){ super.onAttach(activity); setHasOptionsMenu(true); - if(!getArguments().getBoolean("noAutoLoad", false)) - loadData(); } @Override @@ -196,22 +209,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList pager.setAdapter(new ProfilePagerAdapter()); pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels; - if(getArguments().containsKey("profileAccount")){ - account=Parcels.unwrap(getArguments().getParcelable("profileAccount")); - isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account); - bindHeaderView(); - dataLoaded(); - if(!isOwnProfile) - loadRelationship(); - } - scrollView.setScrollableChildSupplier(this::getScrollableRecyclerView); sizeWrapper.addView(content); tabbar.setTabTextColors(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)); tabbar.setTabTextSize(V.dp(16)); - new TabLayoutMediator(tabbar, pager, new TabLayoutMediator.TabConfigurationStrategy(){ + tabLayoutMediator=new TabLayoutMediator(tabbar, pager, new TabLayoutMediator.TabConfigurationStrategy(){ @Override public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){ tab.setText(switch(position){ @@ -222,7 +226,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList default -> throw new IllegalStateException(); }); } - }).attach(); + }); cover.setForeground(coverGradient); cover.setOutlineProvider(new ViewOutlineProvider(){ @@ -236,11 +240,31 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList avatar.setOnClickListener(this::onAvatarClick); cover.setOnClickListener(this::onCoverClick); + if(loaded){ + bindHeaderView(); + dataLoaded(); + tabLayoutMediator.attach(); + } + return sizeWrapper; } @Override protected void doLoadData(){ + currentRequest=new GetAccountByID(profileAccountID) + .setCallback(new SimpleCallback<>(this){ + @Override + public void onSuccess(Account result){ + account=result; + isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account); + bindHeaderView(); + dataLoaded(); + tabLayoutMediator.attach(); + if(!isOwnProfile) + loadRelationship(); + } + }) + .exec(accountID); } @Override @@ -250,11 +274,15 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList @Override public void dataLoaded(){ - postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true); - postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false); - mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false); - aboutFragment=new ProfileAboutFragment(); - aboutFragment.setFields(fields); + if(getActivity()==null) + return; + if(postsFragment==null){ + postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true); + postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false); + mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false); + aboutFragment=new ProfileAboutFragment(); + aboutFragment.setFields(fields); + } pager.getAdapter().notifyDataSetChanged(); super.dataLoaded(); } @@ -322,7 +350,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList name.setText(ssb); setTitle(ssb); username.setText('@'+account.acct); - bio.setText(HtmlParser.parse(account.note, account.emojis)); + bio.setText(HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), accountID)); followersCount.setText(UiUtils.abbreviateNumber(account.followersCount)); followingCount.setText(UiUtils.abbreviateNumber(account.followingCount)); postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount)); @@ -347,7 +375,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList fields.add(joined); for(AccountField field:account.fields){ - field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis); + field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), accountID); field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class); ssb=new SpannableStringBuilder(field.name); HtmlParser.parseCustomEmoji(ssb, account.emojis); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index fe34fee6..a7c2b64e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -69,7 +69,7 @@ public abstract class StatusDisplayItem{ } items.add(new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID)); if(!TextUtils.isEmpty(statusForContent.content)) - items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis), fragment)); + items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, accountID), fragment)); int photoIndex=0; int totalPhotos=0; for(Attachment attachment:statusForContent.mediaAttachments){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java index d2cd4854..541cc66b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java @@ -5,6 +5,7 @@ import android.text.Spanned; import android.widget.TextView; import org.joinmastodon.android.model.Emoji; +import org.joinmastodon.android.model.Mention; import org.joinmastodon.android.ui.utils.UiUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Element; @@ -40,7 +41,7 @@ public class HtmlParser{ * @param emojis Custom emojis that are present in source as :code: * @return a spanned string */ - public static SpannableStringBuilder parse(String source, List emojis){ + public static SpannableStringBuilder parse(String source, List emojis, List mentions, String accountID){ class SpanInfo{ public Object span; public int start; @@ -53,6 +54,8 @@ public class HtmlParser{ } } + Map idsByUrl=mentions.stream().collect(Collectors.toMap(m->m.url, m->m.id)); + final SpannableStringBuilder ssb=new SpannableStringBuilder(); Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){ private final ArrayList openSpans=new ArrayList<>(); @@ -65,15 +68,22 @@ public class HtmlParser{ Element el=(Element)node; switch(el.nodeName()){ case "a" -> { + String href=el.attr("href"); LinkSpan.Type linkType; if(el.hasClass("hashtag")){ linkType=LinkSpan.Type.HASHTAG; }else if(el.hasClass("mention")){ - linkType=LinkSpan.Type.MENTION; + String id=idsByUrl.get(href); + if(id!=null){ + linkType=LinkSpan.Type.MENTION; + href=id; + }else{ + linkType=LinkSpan.Type.URL; + } }else{ linkType=LinkSpan.Type.URL; } - openSpans.add(new SpanInfo(new LinkSpan(el.attr("href"), null, linkType), ssb.length(), el)); + openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID), ssb.length(), el)); } case "br" -> ssb.append('\n'); case "span" -> { diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java index 326d3d05..e0ff34ff 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java @@ -13,11 +13,13 @@ public class LinkSpan extends CharacterStyle { private OnLinkClickListener listener; private String link; private Type type; + private String accountID; - public LinkSpan(String link, OnLinkClickListener listener, Type type) { + public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID){ this.listener=listener; this.link=link; this.type=type; + this.accountID=accountID; } public void setColor(int c){ @@ -36,7 +38,8 @@ public class LinkSpan extends CharacterStyle { public void onClick(Context context){ switch(getType()){ case URL -> UiUtils.launchWebBrowser(context, link); - case HASHTAG, MENTION -> Toast.makeText(context, "Not implemented yet", Toast.LENGTH_SHORT).show(); + case MENTION -> UiUtils.openProfileByID(context, accountID, link); + case HASHTAG -> Toast.makeText(context, "Not implemented yet", Toast.LENGTH_SHORT).show(); } } 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 81032972..6d7bc654 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 @@ -1,12 +1,14 @@ package org.joinmastodon.android.ui.utils; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.provider.OpenableColumns; @@ -17,6 +19,7 @@ import android.widget.TextView; import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; +import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.ui.text.CustomEmojiSpan; @@ -29,6 +32,7 @@ import java.util.stream.Collectors; import androidx.annotation.AttrRes; import androidx.annotation.ColorRes; import androidx.browser.customtabs.CustomTabsIntent; +import me.grishka.appkit.Nav; import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; @@ -161,4 +165,11 @@ public class UiUtils{ ta.recycle(); return color; } + + public static void openProfileByID(Context context, String selfID, String id){ + Bundle args=new Bundle(); + args.putString("account", selfID); + args.putString("profileAccountID", id); + Nav.go((Activity)context, ProfileFragment.class, args); + } }