From 3edc47aa4ac4008fdfaf88dd30f46692918bc2bd Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Mon, 2 Mar 2020 19:34:31 +0100 Subject: [PATCH] Add option to show link previews in timelines (#1681) * Add option to show link previews in timelines. Addresses #1075 * Indent cards in non-selected statuses when viewing threads * Indent cards in timelines * Fix clipping of right side of preview in timelines --- .../tusky/PreferencesActivity.kt | 2 +- .../tusky/adapter/NotificationsAdapter.java | 4 +- .../tusky/adapter/StatusBaseViewHolder.java | 97 +++++++++++++++++ .../adapter/StatusDetailedViewHolder.java | 100 +----------------- .../tusky/adapter/TimelineAdapter.java | 3 +- .../conversation/ConversationsFragment.kt | 8 +- .../fragments/ReportStatusesFragment.kt | 8 +- .../fragments/SearchStatusesFragment.kt | 4 +- .../tusky/fragment/NotificationsFragment.java | 4 +- .../tusky/fragment/TimelineFragment.java | 6 +- .../tusky/fragment/ViewThreadFragment.java | 6 +- .../keylesspalace/tusky/util/CardViewMode.kt | 7 ++ .../tusky/util/StatusDisplayOptions.kt | 4 +- app/src/main/res/layout/item_status.xml | 69 +++++++++++- .../main/res/layout/item_status_detailed.xml | 4 +- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/preferences.xml | 6 ++ 17 files changed, 214 insertions(+), 119 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/CardViewMode.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt index 4fd0b5c6..157916db 100644 --- a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt @@ -129,7 +129,7 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference } "statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", - "useBlurhash" -> { + "useBlurhash", "showCardsInTimelines" -> { restartActivitiesOnExit = true } "language" -> { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 5882b64d..61b6eecc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -44,6 +44,7 @@ import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.interfaces.LinkListener; import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.LinkHelper; @@ -230,7 +231,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { mediaPreviewEnabled, statusDisplayOptions.useAbsoluteTime(), statusDisplayOptions.showBotOverlay(), - statusDisplayOptions.useBlurhash() + statusDisplayOptions.useBlurhash(), + CardViewMode.NONE ); } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index d9acafe2..30baa503 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -8,9 +8,11 @@ import android.text.Spanned; import android.text.TextUtils; import android.text.format.DateUtils; import android.view.View; +import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; @@ -22,14 +24,18 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners; import com.google.android.material.button.MaterialButton; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment.Focus; import com.keylesspalace.tusky.entity.Attachment.MetaData; +import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.HtmlUtils; import com.keylesspalace.tusky.util.ImageLoadingHelper; @@ -86,6 +92,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private TextView pollDescription; private Button pollButton; + private LinearLayout cardView; + private LinearLayout cardInfo; + private ImageView cardImage; + private TextView cardTitle; + private TextView cardDescription; + private TextView cardUrl; private PollAdapter pollAdapter; private SimpleDateFormat shortSdf; @@ -143,6 +155,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { pollDescription = itemView.findViewById(R.id.status_poll_description); pollButton = itemView.findViewById(R.id.status_poll_button); + cardView = itemView.findViewById(R.id.status_card_view); + cardInfo = itemView.findViewById(R.id.card_info); + cardImage = itemView.findViewById(R.id.card_image); + cardTitle = itemView.findViewById(R.id.card_title); + cardDescription = itemView.findViewById(R.id.card_description); + cardUrl = itemView.findViewById(R.id.card_link); + pollAdapter = new PollAdapter(); pollOptions.setAdapter(pollAdapter); pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext())); @@ -683,6 +702,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { hideSensitiveMediaWarning(); } + if (cardView != null) { + setupCard(status, statusDisplayOptions.cardViewMode()); + } + setupButtons(listener, status.getSenderId()); setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility()); @@ -911,6 +934,80 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo); } + protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode) { + if (cardViewMode != CardViewMode.NONE && status.getAttachments().size() == 0 && status.getCard() != null && !TextUtils.isEmpty(status.getCard().getUrl())) { + final Card card = status.getCard(); + cardView.setVisibility(View.VISIBLE); + cardTitle.setText(card.getTitle()); + if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) { + cardDescription.setVisibility(View.GONE); + } else { + cardDescription.setVisibility(View.VISIBLE); + if (TextUtils.isEmpty(card.getDescription())) { + cardDescription.setText(card.getAuthorName()); + } else { + cardDescription.setText(card.getDescription()); + } + } + + cardUrl.setText(card.getUrl()); + + if (!TextUtils.isEmpty(card.getImage())) { + + int topLeftRadius = 0; + int topRightRadius = 0; + int bottomRightRadius = 0; + int bottomLeftRadius = 0; + + int radius = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_radius); + + if (card.getWidth() > card.getHeight()) { + cardView.setOrientation(LinearLayout.VERTICAL); + + cardImage.getLayoutParams().height = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_vertical_height); + cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; + topLeftRadius = radius; + topRightRadius = radius; + } else { + cardView.setOrientation(LinearLayout.HORIZONTAL); + cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.getLayoutParams().width = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + topLeftRadius = radius; + bottomLeftRadius = radius; + } + + + Glide.with(cardImage) + .load(card.getImage()) + .transform( + new CenterCrop(), + new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius) + ) + .into(cardImage); + } else { + cardView.setOrientation(LinearLayout.HORIZONTAL); + cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.getLayoutParams().width = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.setImageResource(R.drawable.card_image_placeholder); + } + + cardView.setOnClickListener(v -> LinkHelper.openLink(card.getUrl(), v.getContext())); + cardView.setClipToOutline(true); + } else { + cardView.setVisibility(View.GONE); + } + } + private static String formatDuration(double durationInSeconds) { int seconds = (int) Math.round(durationInSeconds) % 60; int minutes = (int) durationInSeconds % 3600 / 60; diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index 06b8aaf6..0d844b91 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -4,25 +4,18 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.graphics.drawable.Drawable; -import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.resource.bitmap.CenterCrop; -import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners; import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.viewdata.StatusViewData; @@ -33,27 +26,13 @@ import java.util.Date; class StatusDetailedViewHolder extends StatusBaseViewHolder { private TextView reblogs; private TextView favourites; - private LinearLayout cardView; - private LinearLayout cardInfo; - private ImageView cardImage; - private TextView cardTitle; - private TextView cardDescription; - private TextView cardUrl; private View infoDivider; StatusDetailedViewHolder(View view) { super(view); reblogs = view.findViewById(R.id.status_reblogs); favourites = view.findViewById(R.id.status_favourites); - cardView = view.findViewById(R.id.card_view); - cardInfo = view.findViewById(R.id.card_info); - cardImage = view.findViewById(R.id.card_image); - cardTitle = view.findViewById(R.id.card_title); - cardDescription = view.findViewById(R.id.card_description); - cardUrl = view.findViewById(R.id.card_link); infoDivider = view.findViewById(R.id.status_info_divider); - - cardView.setClipToOutline(true); } @Override @@ -127,6 +106,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { StatusDisplayOptions statusDisplayOptions, @Nullable Object payloads) { super.setupWithStatus(status, listener, statusDisplayOptions, payloads); + setupCard(status, CardViewMode.FULL_WIDTH); // Always show card for detailed status if (payloads == null) { setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); @@ -145,82 +125,6 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { content.setOnLongClickListener(longClickListener); contentWarningDescription.setOnLongClickListener(longClickListener); - - if (status.getAttachments().size() == 0 && status.getCard() != null && !TextUtils.isEmpty(status.getCard().getUrl())) { - final Card card = status.getCard(); - cardView.setVisibility(View.VISIBLE); - cardTitle.setText(card.getTitle()); - if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) { - cardDescription.setVisibility(View.GONE); - } else { - cardDescription.setVisibility(View.VISIBLE); - if (TextUtils.isEmpty(card.getDescription())) { - cardDescription.setText(card.getAuthorName()); - } else { - cardDescription.setText(card.getDescription()); - } - } - - cardUrl.setText(card.getUrl()); - - if (!TextUtils.isEmpty(card.getImage())) { - - int topLeftRadius = 0; - int topRightRadius = 0; - int bottomRightRadius = 0; - int bottomLeftRadius = 0; - - int radius = cardImage.getContext().getResources() - .getDimensionPixelSize(R.dimen.card_radius); - - if (card.getWidth() > card.getHeight()) { - cardView.setOrientation(LinearLayout.VERTICAL); - - cardImage.getLayoutParams().height = cardImage.getContext().getResources() - .getDimensionPixelSize(R.dimen.card_image_vertical_height); - cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; - cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; - cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; - topLeftRadius = radius; - topRightRadius = radius; - } else { - cardView.setOrientation(LinearLayout.HORIZONTAL); - cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; - cardImage.getLayoutParams().width = cardImage.getContext().getResources() - .getDimensionPixelSize(R.dimen.card_image_horizontal_width); - cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; - cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; - topLeftRadius = radius; - bottomLeftRadius = radius; - } - - - Glide.with(cardImage) - .load(card.getImage()) - .transform( - new CenterCrop(), - new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius) - ) - .into(cardImage); - - } else { - cardView.setOrientation(LinearLayout.HORIZONTAL); - cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; - cardImage.getLayoutParams().width = cardImage.getContext().getResources() - .getDimensionPixelSize(R.dimen.card_image_horizontal_width); - cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; - cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; - - cardImage.setImageResource(R.drawable.card_image_placeholder); - - } - - cardView.setOnClickListener(v -> LinkHelper.openLink(card.getUrl(), v.getContext())); - - } else { - cardView.setVisibility(View.GONE); - } - setStatusVisibility(status.getVisibility()); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java index 464d2bc6..307ddfc5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java @@ -63,7 +63,8 @@ public final class TimelineAdapter extends RecyclerView.Adapter { mediaPreviewEnabled, statusDisplayOptions.useAbsoluteTime(), statusDisplayOptions.showBotOverlay(), - statusDisplayOptions.useBlurhash() + statusDisplayOptions.useBlurhash(), + statusDisplayOptions.cardViewMode() ); } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index fbc34281..65816c18 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -36,10 +36,7 @@ import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.StatusActionListener -import com.keylesspalace.tusky.util.NetworkState -import com.keylesspalace.tusky.util.StatusDisplayOptions -import com.keylesspalace.tusky.util.ThemeUtils -import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.* import kotlinx.android.synthetic.main.fragment_timeline.* import javax.inject.Inject @@ -68,7 +65,8 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), showBotOverlay = preferences.getBoolean("showBotOverlay", true), - useBlurhash = preferences.getBoolean("useBlurhash", true) + useBlurhash = preferences.getBoolean("useBlurhash", true), + cardViewMode = CardViewMode.NONE ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt index 3e9bd622..f30406ce 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -43,10 +43,7 @@ import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.util.StatusDisplayOptions -import com.keylesspalace.tusky.util.ThemeUtils -import com.keylesspalace.tusky.util.hide -import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.viewdata.AttachmentViewData import kotlinx.android.synthetic.main.fragment_report_statuses.* import javax.inject.Inject @@ -119,7 +116,8 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler { mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), showBotOverlay = false, - useBlurhash = preferences.getBoolean("useBlurhash", true) + useBlurhash = preferences.getBoolean("useBlurhash", true), + cardViewMode = CardViewMode.NONE ) adapter = StatusesAdapter(statusDisplayOptions, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index c384d123..3ef3d524 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -51,6 +51,7 @@ import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.viewdata.AttachmentViewData @@ -79,7 +80,8 @@ class SearchStatusesFragment : SearchFragment + + + + + + + + + + + + + +