From d0352b86e0f3bb753b1f46c2875e7ddcfa3ff26b Mon Sep 17 00:00:00 2001 From: Grishka Date: Sat, 15 Jun 2024 15:25:19 +0300 Subject: [PATCH] Generalize link card view holder --- .../fragments/BaseStatusListFragment.java | 10 +- .../discover/DiscoverNewsFragment.java | 4 +- .../model/viewmodel/CardViewModel.java | 35 ++- .../LinkCardStatusDisplayItem.java | 215 ++---------------- .../ui/displayitems/StatusDisplayItem.java | 14 +- .../ui/utils/InsetStatusItemDecoration.java | 8 +- .../ui/viewholders/LinkCardHolder.java | 190 ++++++++++++++++ 7 files changed, 263 insertions(+), 213 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/LinkCardHolder.java diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 7ccac52ba..aa30c27a0 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -287,8 +287,8 @@ public abstract class BaseStatusListFragment exten outRect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); } RecyclerView.ViewHolder holder=list.getChildViewHolder(view); - if(holder instanceof StatusDisplayItem.Holder){ - if(((StatusDisplayItem.Holder) holder).getItem().getType()==StatusDisplayItem.Type.GAP){ + if(holder instanceof StatusDisplayItem.Holder sih){ + if(sih.getItem() instanceof StatusDisplayItem sdi && sdi.getType()==StatusDisplayItem.Type.GAP){ outRect.setEmpty(); return; } @@ -296,8 +296,8 @@ public abstract class BaseStatusListFragment exten for(int i=0;i) holder).getItemID(); + if(holder instanceof StatusDisplayItem.Holder sih2){ + String otherID=sih2.getItemID(); if(otherID.equals(id)){ list.getDecoratedBoundsWithMargins(child, tmpRect); outRect.left=Math.min(outRect.left, tmpRect.left); @@ -760,7 +760,7 @@ public abstract class BaseStatusListFragment exten // Do not draw dividers between hashtag and/or account rows if((ih instanceof HashtagStatusDisplayItem.Holder || ih instanceof AccountStatusDisplayItem.Holder) && (sh instanceof HashtagStatusDisplayItem.Holder || sh instanceof AccountStatusDisplayItem.Holder)) return false; - return !ih.getItemID().equals(sh.getItemID()) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP; + return !ih.getItemID().equals(sh.getItemID()) && ih.getItem() instanceof StatusDisplayItem sdi && sdi.getType()!=StatusDisplayItem.Type.GAP; } return false; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverNewsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverNewsFragment.java index 46462ebdc..6a9e13d10 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverNewsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverNewsFragment.java @@ -71,11 +71,11 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment im @Override public void onSuccess(List result){ top3.clear(); - top3.addAll(result.subList(0, Math.min(3, result.size())).stream().map(card->new CardViewModel(card, 280, 140)).collect(Collectors.toList())); + top3.addAll(result.subList(0, Math.min(3, result.size())).stream().map(card->new CardViewModel(card, 280, 140, card, accountID)).collect(Collectors.toList())); cardsAdapter.notifyDataSetChanged(); onDataLoaded(result.subList(top3.size(), result.size()).stream() - .map(card->new CardViewModel(card, 56, 56)) + .map(card->new CardViewModel(card, 56, 56, card, accountID)) .collect(Collectors.toList()), false); bannerHelper.onBannerBecameVisible(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/CardViewModel.java b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/CardViewModel.java index 18286683a..ba2db9e3e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/CardViewModel.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/CardViewModel.java @@ -1,19 +1,52 @@ package org.joinmastodon.android.model.viewmodel; +import android.text.SpannableStringBuilder; import android.text.TextUtils; +import org.joinmastodon.android.GlobalUserPreferences; +import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.Card; +import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; public class CardViewModel{ + public final Object parentObject; public final Card card; public final ImageLoaderRequest imageRequest; + public final UrlImageLoaderRequest authorAvaRequest; + public final SpannableStringBuilder parsedAuthorName; + public final CustomEmojiHelper authorNameEmojiHelper=new CustomEmojiHelper(); - public CardViewModel(Card card, int width, int height){ + public CardViewModel(Card card, int width, int height, Object parentObject, String accountID){ this.card=card; + this.parentObject=parentObject; this.imageRequest=TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(width), V.dp(height)); + + if(card.authorAccount!=null){ + parsedAuthorName=new SpannableStringBuilder(card.authorAccount.displayName); + if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames) + HtmlParser.parseCustomEmoji(parsedAuthorName, card.authorAccount.emojis); + authorNameEmojiHelper.setText(parsedAuthorName); + authorAvaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? card.authorAccount.avatar : card.authorAccount.avatarStatic, V.dp(50), V.dp(50)); + }else{ + parsedAuthorName=null; + authorAvaRequest=null; + } + } + + public int getImageCount(){ + return 1+(card.authorAccount!=null ? (1+authorNameEmojiHelper.getImageCount()) : 0); + } + + public ImageLoaderRequest getImageRequest(int index){ + return switch(index){ + case 0 -> imageRequest; + case 1 -> authorAvaRequest; + default -> authorNameEmojiHelper.getImageRequest(index-2); + }; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/LinkCardStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/LinkCardStatusDisplayItem.java index cde389824..0a67d40f7 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/LinkCardStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/LinkCardStatusDisplayItem.java @@ -1,230 +1,55 @@ package org.joinmastodon.android.ui.displayitems; -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.view.View; +import android.app.Activity; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import org.joinmastodon.android.GlobalUserPreferences; -import org.joinmastodon.android.R; -import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.model.Card; import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.OutlineProviders; -import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable; -import org.joinmastodon.android.ui.text.HtmlParser; -import org.joinmastodon.android.ui.utils.CustomEmojiHelper; -import org.joinmastodon.android.ui.utils.UiUtils; -import org.parceler.Parcels; +import org.joinmastodon.android.model.viewmodel.CardViewModel; +import org.joinmastodon.android.ui.viewholders.LinkCardHolder; -import java.util.Objects; - -import me.grishka.appkit.Nav; -import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; -import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; -import me.grishka.appkit.utils.V; -public class LinkCardStatusDisplayItem extends StatusDisplayItem{ +public class LinkCardStatusDisplayItem extends StatusDisplayItem implements LinkCardHolder.LinkCardProvider{ private final Status status; - private final UrlImageLoaderRequest imgRequest, authorAvaRequest; - private final SpannableStringBuilder parsedAuthorName; - private final CustomEmojiHelper authorNameEmojiHelper=new CustomEmojiHelper(); + private final CardViewModel cardViewModel; public LinkCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){ super(parentID, parentFragment); this.status=status; - if(status.card.image!=null) - imgRequest=new UrlImageLoaderRequest(status.card.image, 1000, 1000); - else - imgRequest=null; + int size=shouldUseLargeCard() ? 1000 : 192; + cardViewModel=new CardViewModel(status.card, size, size, status, parentFragment.getAccountID()); + } - if(status.card.authorAccount!=null){ - parsedAuthorName=new SpannableStringBuilder(status.card.authorAccount.displayName); - if(AccountSessionManager.get(parentFragment.getAccountID()).getLocalPreferences().customEmojiInNames) - HtmlParser.parseCustomEmoji(parsedAuthorName, status.card.authorAccount.emojis); - authorNameEmojiHelper.setText(parsedAuthorName); - authorAvaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? status.card.authorAccount.avatar : status.card.authorAccount.avatarStatic, V.dp(50), V.dp(50)); - }else{ - parsedAuthorName=null; - authorAvaRequest=null; - } + private boolean shouldUseLargeCard(){ + return status.card.type==Card.Type.VIDEO || (status.card.image!=null && status.card.width>status.card.height); } @Override public Type getType(){ - return status.card.type==Card.Type.VIDEO || (status.card.image!=null && status.card.width>status.card.height) ? Type.CARD_LARGE : Type.CARD_COMPACT; + return shouldUseLargeCard() ? Type.CARD_LARGE : Type.CARD_COMPACT; } @Override public int getImageCount(){ - return 1+(status.card.authorAccount!=null ? (1+authorNameEmojiHelper.getImageCount()) : 0); + return cardViewModel.getImageCount(); } @Override public ImageLoaderRequest getImageRequest(int index){ - return switch(index){ - case 0 -> imgRequest; - case 1 -> authorAvaRequest; - default -> authorNameEmojiHelper.getImageRequest(index-2); - }; + return cardViewModel.getImageRequest(index); } - public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ - private final TextView title, description, domain, timestamp, authorBefore, authorAfter, authorName; - private final ImageView photo, authorAva; - private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); - private boolean didClear; - private final View inner, authorFooter, authorChip; - private final boolean isLarge; - private final Drawable logoIcon; + @Override + public CardViewModel getCard(){ + return cardViewModel; + } - public Holder(Context context, ViewGroup parent, boolean isLarge){ - super(context, isLarge ? R.layout.display_item_link_card : R.layout.display_item_link_card_compact, parent); - this.isLarge=isLarge; - title=findViewById(R.id.title); - description=findViewById(R.id.description); - domain=findViewById(R.id.domain); - timestamp=findViewById(R.id.timestamp); - photo=findViewById(R.id.photo); - inner=findViewById(R.id.inner); - authorBefore=findViewById(R.id.author_before); - authorAfter=findViewById(R.id.author_after); - authorName=findViewById(R.id.author_name); - authorAva=findViewById(R.id.author_ava); - authorChip=findViewById(R.id.author_chip); - authorFooter=findViewById(R.id.author_footer); + public static class Holder extends LinkCardHolder{ - inner.setOnClickListener(this::onClick); - inner.setOutlineProvider(OutlineProviders.roundedRect(8)); - inner.setClipToOutline(true); - if(!isLarge){ - photo.setOutlineProvider(OutlineProviders.roundedRect(4)); - photo.setClipToOutline(true); - } - authorAva.setOutlineProvider(OutlineProviders.roundedRect(3)); - authorAva.setClipToOutline(true); - authorChip.setOnClickListener(this::onAuthorChipClick); - - logoIcon=context.getResources().getDrawable(R.drawable.ic_ntf_logo, context.getTheme()).mutate(); - logoIcon.setBounds(0, 0, V.dp(17), V.dp(17)); - } - - @SuppressLint("SetTextI18n") - @Override - public void onBind(LinkCardStatusDisplayItem item){ - Card card=item.status.card; - title.setText(card.title); - if(description!=null){ - description.setText(card.description); - description.setVisibility(TextUtils.isEmpty(card.description) ? View.GONE : View.VISIBLE); - } - String cardDomain=HtmlParser.normalizeDomain(Objects.requireNonNull(Uri.parse(card.url).getHost())); - domain.setText(TextUtils.isEmpty(card.providerName) ? cardDomain : card.providerName); - if(card.authorAccount!=null){ - authorFooter.setVisibility(View.VISIBLE); - authorChip.setVisibility(View.VISIBLE); - authorBefore.setVisibility(View.VISIBLE); - String[] authorParts=itemView.getContext().getString(R.string.article_by_author, "{author}").split("\\{author\\}"); - String before=authorParts[0].trim(); - String after=authorParts.length>1 ? authorParts[1].trim() : ""; - if(!TextUtils.isEmpty(before)){ - authorBefore.setText(before); - } - if(TextUtils.isEmpty(after)){ - authorAfter.setVisibility(View.GONE); - }else{ - authorAfter.setVisibility(View.VISIBLE); - authorAfter.setText(after); - } - authorName.setText(item.parsedAuthorName); - authorBefore.setCompoundDrawablesRelative(logoIcon, null, null, null); - }else if(!TextUtils.isEmpty(card.authorName)){ - authorFooter.setVisibility(View.VISIBLE); - authorBefore.setVisibility(View.VISIBLE); - authorBefore.setCompoundDrawables(null, null, null, null); - authorChip.setVisibility(View.GONE); - authorAfter.setVisibility(View.GONE); - authorBefore.setText(itemView.getContext().getString(R.string.article_by_author, card.authorName)); - }else{ - authorFooter.setVisibility(View.GONE); - } - - if(card.publishedAt!=null){ - timestamp.setVisibility(View.VISIBLE); - timestamp.setText(" · "+UiUtils.formatRelativeTimestamp(itemView.getContext(), card.publishedAt)); - }else{ - timestamp.setVisibility(View.GONE); - } - - photo.setImageDrawable(null); - if(item.imgRequest!=null){ - photo.setScaleType(ImageView.ScaleType.CENTER_CROP); - photo.setBackground(null); - photo.setImageTintList(null); - crossfadeDrawable.setSize(card.width, card.height); - crossfadeDrawable.setBlurhashDrawable(card.blurhashPlaceholder); - crossfadeDrawable.setCrossfadeAlpha(0f); - photo.setImageDrawable(null); - photo.setImageDrawable(crossfadeDrawable); - didClear=false; - }else{ - photo.setBackgroundColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3SurfaceVariant)); - photo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3Outline))); - photo.setScaleType(ImageView.ScaleType.CENTER); - photo.setImageResource(R.drawable.ic_feed_48px); - } - } - - @Override - public void setImage(int index, Drawable drawable){ - if(index==0){ - crossfadeDrawable.setImageDrawable(drawable); - if(didClear) - crossfadeDrawable.animateAlpha(0f); - Card card=item.status.card; - // Make sure the image is not stretched if the server returned wrong dimensions - if(drawable!=null && (drawable.getIntrinsicWidth()!=card.width || drawable.getIntrinsicHeight()!=card.height)){ - photo.setImageDrawable(null); - photo.setImageDrawable(crossfadeDrawable); - } - }else if(index==1){ - authorAva.setImageDrawable(drawable); - }else{ - item.authorNameEmojiHelper.setImageDrawable(index-2, drawable); - authorName.invalidate(); - } - } - - @Override - public void clearImage(int index){ - if(index==0){ - crossfadeDrawable.setCrossfadeAlpha(1f); - didClear=true; - }else{ - setImage(index, null); - } - } - - private void onClick(View v){ - UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), item.status.card.url, item.status); - } - - private void onAuthorChipClick(View v){ - Bundle args=new Bundle(); - args.putString("account", item.parentFragment.getAccountID()); - args.putParcelable("profileAccount", Parcels.wrap(item.status.card.authorAccount)); - Nav.go(item.parentFragment.getActivity(), ProfileFragment.class, args); + public Holder(Activity context, ViewGroup parent, boolean isLarge, String accountID){ + super(context, parent, isLarge, accountID); } } } 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 52246a628..745c6bc4b 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 @@ -11,6 +11,7 @@ import android.view.ViewGroup; import org.joinmastodon.android.R; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Attachment; @@ -68,8 +69,8 @@ public abstract class StatusDisplayItem{ case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent); case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent); case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent); - case CARD_LARGE -> new LinkCardStatusDisplayItem.Holder(activity, parent, true); - case CARD_COMPACT -> new LinkCardStatusDisplayItem.Holder(activity, parent, false); + case CARD_LARGE -> new LinkCardStatusDisplayItem.Holder(activity, parent, true, ((StatusListFragment)parentFragment).getAccountID()); + case CARD_COMPACT -> new LinkCardStatusDisplayItem.Holder(activity, parent, false, ((StatusListFragment)parentFragment).getAccountID()); case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent); case ACCOUNT -> new AccountStatusDisplayItem.Holder(new AccountViewHolder(parentFragment, parent, null)); case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent); @@ -226,7 +227,7 @@ public abstract class StatusDisplayItem{ FILTER_SPOILER } - public static abstract class Holder extends BindableViewHolder implements UsableRecyclerView.DisableableClickable{ + public static abstract class Holder extends BindableViewHolder implements UsableRecyclerView.DisableableClickable{ public Holder(View itemView){ super(itemView); } @@ -236,17 +237,18 @@ public abstract class StatusDisplayItem{ } public String getItemID(){ - return item.parentID; + return item instanceof StatusDisplayItem sdi ? sdi.parentID : null; } @Override public void onClick(){ - item.parentFragment.onItemClick(item.parentID); + if(item instanceof StatusDisplayItem sdi) + sdi.parentFragment.onItemClick(sdi.parentID); } @Override public boolean isEnabled(){ - return item.parentFragment.isItemEnabled(item.parentID); + return item instanceof StatusDisplayItem sdi && sdi.parentFragment.isItemEnabled(sdi.parentID); } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java index 5fd914139..2e0968974 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java @@ -38,7 +38,7 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{ View child=parent.getChildAt(i); RecyclerView.ViewHolder holder=parent.getChildViewHolder(child); pos=holder.getAbsoluteAdapterPosition()-listFragment.getMainAdapterOffset(); - boolean inset=(holder instanceof StatusDisplayItem.Holder sdi) && sdi.getItem().inset; + boolean inset=holder instanceof StatusDisplayItem.Holder sdi && sdi.getItem() instanceof StatusDisplayItem item && item.inset; if(inset){ if(rect.isEmpty()){ float childY=child.getY(); @@ -80,13 +80,13 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ List displayItems=listFragment.getDisplayItems(); RecyclerView.ViewHolder holder=parent.getChildViewHolder(view); - if(holder instanceof StatusDisplayItem.Holder sdi){ - boolean inset=sdi.getItem().inset; + if(holder instanceof StatusDisplayItem.Holder sdi && sdi.getItem() instanceof StatusDisplayItem item){ + boolean inset=item.inset; int pos=holder.getAbsoluteAdapterPosition()-listFragment.getMainAdapterOffset(); if(inset){ boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset; boolean bottomSiblingInset=pos extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ + private final TextView title, description, domain, timestamp, authorBefore, authorAfter, authorName; + private final ImageView photo, authorAva; + private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); + private boolean didClear; + private final View inner, authorFooter, authorChip; + private final boolean isLarge; + private final Drawable logoIcon; + private final String accountID; + private Activity activity; + + public LinkCardHolder(Activity context, ViewGroup parent, boolean isLarge, String accountID){ + super(context, isLarge ? R.layout.display_item_link_card : R.layout.display_item_link_card_compact, parent); + this.isLarge=isLarge; + this.accountID=accountID; + activity=context; + title=findViewById(R.id.title); + description=findViewById(R.id.description); + domain=findViewById(R.id.domain); + timestamp=findViewById(R.id.timestamp); + photo=findViewById(R.id.photo); + inner=findViewById(R.id.inner); + authorBefore=findViewById(R.id.author_before); + authorAfter=findViewById(R.id.author_after); + authorName=findViewById(R.id.author_name); + authorAva=findViewById(R.id.author_ava); + authorChip=findViewById(R.id.author_chip); + authorFooter=findViewById(R.id.author_footer); + + inner.setOnClickListener(this::onClick); + inner.setOutlineProvider(OutlineProviders.roundedRect(8)); + inner.setClipToOutline(true); + if(!isLarge){ + photo.setOutlineProvider(OutlineProviders.roundedRect(4)); + photo.setClipToOutline(true); + } + authorAva.setOutlineProvider(OutlineProviders.roundedRect(3)); + authorAva.setClipToOutline(true); + authorChip.setOnClickListener(this::onAuthorChipClick); + + logoIcon=context.getResources().getDrawable(R.drawable.ic_ntf_logo, context.getTheme()).mutate(); + logoIcon.setBounds(0, 0, V.dp(17), V.dp(17)); + } + + @SuppressLint("SetTextI18n") + @Override + public void onBind(T item){ + CardViewModel cardVM=item.getCard(); + Card card=cardVM.card; + title.setText(card.title); + if(description!=null){ + description.setText(card.description); + description.setVisibility(TextUtils.isEmpty(card.description) ? View.GONE : View.VISIBLE); + } + String cardDomain=HtmlParser.normalizeDomain(Objects.requireNonNull(Uri.parse(card.url).getHost())); + domain.setText(TextUtils.isEmpty(card.providerName) ? cardDomain : card.providerName); + if(card.authorAccount!=null){ + authorFooter.setVisibility(View.VISIBLE); + authorChip.setVisibility(View.VISIBLE); + authorBefore.setVisibility(View.VISIBLE); + String[] authorParts=itemView.getContext().getString(R.string.article_by_author, "{author}").split("\\{author\\}"); + String before=authorParts[0].trim(); + String after=authorParts.length>1 ? authorParts[1].trim() : ""; + if(!TextUtils.isEmpty(before)){ + authorBefore.setText(before); + } + if(TextUtils.isEmpty(after)){ + authorAfter.setVisibility(View.GONE); + }else{ + authorAfter.setVisibility(View.VISIBLE); + authorAfter.setText(after); + } + authorName.setText(cardVM.parsedAuthorName); + authorBefore.setCompoundDrawablesRelative(logoIcon, null, null, null); + }else if(!TextUtils.isEmpty(card.authorName)){ + authorFooter.setVisibility(View.VISIBLE); + authorBefore.setVisibility(View.VISIBLE); + authorBefore.setCompoundDrawables(null, null, null, null); + authorChip.setVisibility(View.GONE); + authorAfter.setVisibility(View.GONE); + authorBefore.setText(itemView.getContext().getString(R.string.article_by_author, card.authorName)); + }else{ + authorFooter.setVisibility(View.GONE); + } + + if(card.publishedAt!=null){ + timestamp.setVisibility(View.VISIBLE); + timestamp.setText(" · "+UiUtils.formatRelativeTimestamp(itemView.getContext(), card.publishedAt)); + }else{ + timestamp.setVisibility(View.GONE); + } + + photo.setImageDrawable(null); + if(cardVM.imageRequest!=null){ + photo.setScaleType(ImageView.ScaleType.CENTER_CROP); + photo.setBackground(null); + photo.setImageTintList(null); + crossfadeDrawable.setSize(card.width, card.height); + crossfadeDrawable.setBlurhashDrawable(card.blurhashPlaceholder); + crossfadeDrawable.setCrossfadeAlpha(0f); + photo.setImageDrawable(null); + photo.setImageDrawable(crossfadeDrawable); + didClear=false; + }else{ + photo.setBackgroundColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3SurfaceVariant)); + photo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3Outline))); + photo.setScaleType(ImageView.ScaleType.CENTER); + photo.setImageResource(R.drawable.ic_feed_48px); + } + } + + @Override + public void setImage(int index, Drawable drawable){ + if(index==0){ + crossfadeDrawable.setImageDrawable(drawable); + if(didClear) + crossfadeDrawable.animateAlpha(0f); + CardViewModel card=item.getCard(); + // Make sure the image is not stretched if the server returned wrong dimensions + if(drawable!=null && (drawable.getIntrinsicWidth()!=card.card.width || drawable.getIntrinsicHeight()!=card.card.height)){ + photo.setImageDrawable(null); + photo.setImageDrawable(crossfadeDrawable); + } + }else if(index==1){ + authorAva.setImageDrawable(drawable); + }else{ + item.getCard().authorNameEmojiHelper.setImageDrawable(index-2, drawable); + authorName.invalidate(); + } + } + + @Override + public void clearImage(int index){ + if(index==0){ + crossfadeDrawable.setCrossfadeAlpha(1f); + didClear=true; + }else{ + setImage(index, null); + } + } + + private void onClick(View v){ + CardViewModel card=item.getCard(); + UiUtils.openURL(itemView.getContext(), accountID, card.card.url, card.parentObject); + } + + private void onAuthorChipClick(View v){ + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putParcelable("profileAccount", Parcels.wrap(item.getCard().card.authorAccount)); + Nav.go(activity, ProfileFragment.class, args); + } + + public interface LinkCardProvider{ + CardViewModel getCard(); + } +}