diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 0328eaaee..7d4f50c66 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -13,7 +13,7 @@ android { applicationId "org.joinmastodon.android" minSdk 23 targetSdk 33 - versionCode 100 + versionCode 101 versionName "2.5.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } 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..7bc98864a 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 @@ -2,56 +2,35 @@ package org.joinmastodon.android.fragments.discover; import android.annotation.SuppressLint; import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.trends.GetTrendingLinks; import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.model.Card; import org.joinmastodon.android.model.viewmodel.CardViewModel; -import org.joinmastodon.android.ui.DividerItemDecoration; -import org.joinmastodon.android.ui.OutlineProviders; -import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; -import org.joinmastodon.android.ui.utils.HorizontalScrollingTouchListener; -import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.ui.viewholders.LinkCardHolder; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import androidx.annotation.NonNull; -import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; -import me.grishka.appkit.imageloader.ImageLoaderViewHolder; -import me.grishka.appkit.imageloader.ListImageLoaderAdapter; import me.grishka.appkit.imageloader.ListImageLoaderWrapper; -import me.grishka.appkit.imageloader.RecyclerViewDelegate; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; -import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; -import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.MergeRecyclerAdapter; -import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; -public class DiscoverNewsFragment extends BaseRecyclerFragment implements ScrollableToTop{ +public class DiscoverNewsFragment extends BaseRecyclerFragment implements ScrollableToTop{ private String accountID; private DiscoverInfoBannerHelper bannerHelper; private MergeRecyclerAdapter mergeAdapter; - private UsableRecyclerView cardsList; - private ArrayList top3=new ArrayList<>(); - private CardLinksAdapter cardsAdapter; public DiscoverNewsFragment(){ super(10); @@ -70,12 +49,14 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment im .setCallback(new SimpleCallback<>(this){ @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())); - cardsAdapter.notifyDataSetChanged(); - - onDataLoaded(result.subList(top3.size(), result.size()).stream() - .map(card->new CardViewModel(card, 56, 56)) + int[] index={0}; + onDataLoaded(result.stream() + .map(card->{ + int actualIndex=index[0]+(refreshing ? 0 : (data.size()+preloadedData.size())); + index[0]++; + int size=actualIndex==0 ? 1000 : 192; + return new CardItem(new CardViewModel(card, size, size, card, accountID)); + }) .collect(Collectors.toList()), false); bannerHelper.onBannerBecameVisible(); } @@ -86,27 +67,9 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment im @SuppressLint("ClickableViewAccessibility") @Override protected RecyclerView.Adapter getAdapter(){ - cardsList=new UsableRecyclerView(getActivity()); - cardsList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false)); - ListImageLoaderWrapper cardsImageLoader=new ListImageLoaderWrapper(getActivity(), cardsList, new RecyclerViewDelegate(cardsList), this); - cardsList.setAdapter(cardsAdapter=new CardLinksAdapter(cardsImageLoader, top3)); - cardsList.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(256))); - cardsList.setPadding(V.dp(16), V.dp(8), 0, 0); - cardsList.setClipToPadding(false); - cardsList.addItemDecoration(new RecyclerView.ItemDecoration(){ - @Override - public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ - outRect.right=V.dp(16); - } - }); - cardsList.setSelector(R.drawable.bg_rect_12dp_ripple); - cardsList.setDrawSelectorOnTop(true); - cardsList.setOnTouchListener(new HorizontalScrollingTouchListener(getActivity())); - mergeAdapter=new MergeRecyclerAdapter(); bannerHelper.maybeAddBanner(list, mergeAdapter); - mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(cardsList)); - mergeAdapter.addAdapter(new LinksAdapter(imgLoader, data)); + mergeAdapter.addAdapter(new LinksAdapter(imgLoader)); return mergeAdapter; } @@ -115,18 +78,46 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment im smoothScrollRecyclerViewToTop(list); } - private class LinksAdapter extends UsableRecyclerView.Adapter implements ImageLoaderRecyclerAdapter{ - private final List data; + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + list.addItemDecoration(new RecyclerView.ItemDecoration(){ + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ + if(parent.getChildAdapterPosition(view)==0 && !bannerHelper.isBannerShown()){ + outRect.top=V.dp(16); + } + if(parent.getChildViewHolder(view) instanceof LinkCardHolder){ + outRect.bottom=V.dp(8); + } + } + }); + } - public LinksAdapter(ListImageLoaderWrapper imgLoader, List data){ + public static class CardItem implements LinkCardHolder.LinkCardProvider{ + public final CardViewModel card; + + private CardItem(CardViewModel card){ + this.card=card; + } + + @Override + public CardViewModel getCard(){ + return card; + } + } + + private class LinksAdapter extends UsableRecyclerView.Adapter> implements ImageLoaderRecyclerAdapter{ + public LinksAdapter(ListImageLoaderWrapper imgLoader){ super(imgLoader); - this.data=data; } @NonNull @Override - public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ - return new LinkViewHolder(); + public LinkCardHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + LinkCardHolder vh=new LinkCardHolder<>(getActivity(), list, viewType==1, accountID); + vh.setTryResolving(false); + return vh; } @Override @@ -135,91 +126,24 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment im } @Override - public void onBindViewHolder(BaseLinkViewHolder holder, int position){ - holder.bind(data.get(position).card); + public void onBindViewHolder(LinkCardHolder holder, int position){ + holder.bind(data.get(position)); super.onBindViewHolder(holder, position); } @Override public int getImageCountForItem(int position){ - return data.get(position).imageRequest==null ? 0 : 1; + return data.get(position).card.getImageCount(); } @Override public ImageLoaderRequest getImageRequest(int position, int image){ - return data.get(position).imageRequest; - } - } - - private class CardLinksAdapter extends LinksAdapter{ - public CardLinksAdapter(ListImageLoaderWrapper imgLoader, List data){ - super(imgLoader, data); - } - - @NonNull - @Override - public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ - return new LinkCardViewHolder(); - } - } - - private class BaseLinkViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{ - protected final TextView name, title; - protected final ImageView photo; - private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); - private boolean didClear; - - public BaseLinkViewHolder(int layout){ - super(getActivity(), layout, list); - name=findViewById(R.id.name); - title=findViewById(R.id.title); - photo=findViewById(R.id.photo); + return data.get(position).card.getImageRequest(image); } @Override - public void onBind(Card item){ - name.setText(item.providerName); - title.setText(item.title); - crossfadeDrawable.setSize(item.width, item.height); - crossfadeDrawable.setBlurhashDrawable(item.blurhashPlaceholder); - crossfadeDrawable.setCrossfadeAlpha(0f); - photo.setImageDrawable(null); - photo.setImageDrawable(crossfadeDrawable); - didClear=false; - } - - @Override - public void setImage(int index, Drawable drawable){ - crossfadeDrawable.setImageDrawable(drawable); - if(didClear) - crossfadeDrawable.animateAlpha(0f); - } - - @Override - public void clearImage(int index){ - crossfadeDrawable.setCrossfadeAlpha(1f); - didClear=true; - } - - @Override - public void onClick(){ - UiUtils.launchWebBrowser(getActivity(), item.url); - } - } - - private class LinkViewHolder extends BaseLinkViewHolder{ - public LinkViewHolder(){ - super(R.layout.item_trending_link); - photo.setOutlineProvider(OutlineProviders.roundedRect(12)); - photo.setClipToOutline(true); - } - } - - private class LinkCardViewHolder extends BaseLinkViewHolder{ - public LinkCardViewHolder(){ - super(R.layout.item_trending_link_card); - itemView.setOutlineProvider(OutlineProviders.roundedRect(12)); - itemView.setClipToOutline(true); + public int getItemViewType(int position){ + return position==0 ? 1 : 2; } } } 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/DiscoverInfoBannerHelper.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/DiscoverInfoBannerHelper.java index b0247b0d7..261275708 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/DiscoverInfoBannerHelper.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/DiscoverInfoBannerHelper.java @@ -85,6 +85,10 @@ public class DiscoverInfoBannerHelper{ bannerTypesToShow=EnumSet.allOf(BannerType.class); } + public boolean isBannerShown(){ + return added; + } + public enum BannerType{ TRENDING_POSTS, TRENDING_LINKS, 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; + private boolean tryResolving=true; + + 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); + } + } + + public void setTryResolving(boolean tryResolving){ + this.tryResolving=tryResolving; + } + + private void onClick(View v){ + CardViewModel card=item.getCard(); + if(tryResolving) + UiUtils.openURL(activity, accountID, card.card.url, card.parentObject); + else + UiUtils.launchWebBrowser(activity, card.card.url); + } + + 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(); + } +} diff --git a/mastodon/src/main/res/drawable/fg_link_card.xml b/mastodon/src/main/res/drawable/fg_link_card.xml index 1d837b3ec..4c7dfb7d3 100644 --- a/mastodon/src/main/res/drawable/fg_link_card.xml +++ b/mastodon/src/main/res/drawable/fg_link_card.xml @@ -6,10 +6,10 @@ - + - - + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/item_trending_link.xml b/mastodon/src/main/res/layout/item_trending_link.xml deleted file mode 100644 index c426aa736..000000000 --- a/mastodon/src/main/res/layout/item_trending_link.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/mastodon/src/main/res/layout/item_trending_link_card.xml b/mastodon/src/main/res/layout/item_trending_link_card.xml deleted file mode 100644 index c829b19c5..000000000 --- a/mastodon/src/main/res/layout/item_trending_link_card.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - \ No newline at end of file