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
This commit is contained in:
Levi Bard 2020-03-02 19:34:31 +01:00 committed by GitHub
parent dd8abad8ca
commit 3edc47aa4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 214 additions and 119 deletions

View file

@ -129,7 +129,7 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
} }
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
"useBlurhash" -> { "useBlurhash", "showCardsInTimelines" -> {
restartActivitiesOnExit = true restartActivitiesOnExit = true
} }
"language" -> { "language" -> {

View file

@ -44,6 +44,7 @@ import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.interfaces.LinkListener; import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
@ -230,7 +231,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
mediaPreviewEnabled, mediaPreviewEnabled,
statusDisplayOptions.useAbsoluteTime(), statusDisplayOptions.useAbsoluteTime(),
statusDisplayOptions.showBotOverlay(), statusDisplayOptions.showBotOverlay(),
statusDisplayOptions.useBlurhash() statusDisplayOptions.useBlurhash(),
CardViewMode.NONE
); );
} }

View file

@ -8,9 +8,11 @@ import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -22,14 +24,18 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide; 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.google.android.material.button.MaterialButton;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Attachment.Focus; import com.keylesspalace.tusky.entity.Attachment.Focus;
import com.keylesspalace.tusky.entity.Attachment.MetaData; import com.keylesspalace.tusky.entity.Attachment.MetaData;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.HtmlUtils; import com.keylesspalace.tusky.util.HtmlUtils;
import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper;
@ -86,6 +92,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private TextView pollDescription; private TextView pollDescription;
private Button pollButton; 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 PollAdapter pollAdapter;
private SimpleDateFormat shortSdf; private SimpleDateFormat shortSdf;
@ -143,6 +155,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
pollDescription = itemView.findViewById(R.id.status_poll_description); pollDescription = itemView.findViewById(R.id.status_poll_description);
pollButton = itemView.findViewById(R.id.status_poll_button); 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(); pollAdapter = new PollAdapter();
pollOptions.setAdapter(pollAdapter); pollOptions.setAdapter(pollAdapter);
pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext())); pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext()));
@ -683,6 +702,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
hideSensitiveMediaWarning(); hideSensitiveMediaWarning();
} }
if (cardView != null) {
setupCard(status, statusDisplayOptions.cardViewMode());
}
setupButtons(listener, status.getSenderId()); setupButtons(listener, status.getSenderId());
setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility()); 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); 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) { private static String formatDuration(double durationInSeconds) {
int seconds = (int) Math.round(durationInSeconds) % 60; int seconds = (int) Math.round(durationInSeconds) % 60;
int minutes = (int) durationInSeconds % 3600 / 60; int minutes = (int) durationInSeconds % 3600 / 60;

View file

@ -4,25 +4,18 @@ import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; 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.R;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
@ -33,27 +26,13 @@ import java.util.Date;
class StatusDetailedViewHolder extends StatusBaseViewHolder { class StatusDetailedViewHolder extends StatusBaseViewHolder {
private TextView reblogs; private TextView reblogs;
private TextView favourites; private TextView favourites;
private LinearLayout cardView;
private LinearLayout cardInfo;
private ImageView cardImage;
private TextView cardTitle;
private TextView cardDescription;
private TextView cardUrl;
private View infoDivider; private View infoDivider;
StatusDetailedViewHolder(View view) { StatusDetailedViewHolder(View view) {
super(view); super(view);
reblogs = view.findViewById(R.id.status_reblogs); reblogs = view.findViewById(R.id.status_reblogs);
favourites = view.findViewById(R.id.status_favourites); 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); infoDivider = view.findViewById(R.id.status_info_divider);
cardView.setClipToOutline(true);
} }
@Override @Override
@ -127,6 +106,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
StatusDisplayOptions statusDisplayOptions, StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) { @Nullable Object payloads) {
super.setupWithStatus(status, listener, statusDisplayOptions, payloads); super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
setupCard(status, CardViewMode.FULL_WIDTH); // Always show card for detailed status
if (payloads == null) { if (payloads == null) {
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
@ -145,82 +125,6 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
content.setOnLongClickListener(longClickListener); content.setOnLongClickListener(longClickListener);
contentWarningDescription.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()); setStatusVisibility(status.getVisibility());
} }
} }

View file

@ -63,7 +63,8 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
mediaPreviewEnabled, mediaPreviewEnabled,
statusDisplayOptions.useAbsoluteTime(), statusDisplayOptions.useAbsoluteTime(),
statusDisplayOptions.showBotOverlay(), statusDisplayOptions.showBotOverlay(),
statusDisplayOptions.useBlurhash() statusDisplayOptions.useBlurhash(),
statusDisplayOptions.cardViewMode()
); );
} }

View file

@ -36,10 +36,7 @@ import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import kotlinx.android.synthetic.main.fragment_timeline.* import kotlinx.android.synthetic.main.fragment_timeline.*
import javax.inject.Inject import javax.inject.Inject
@ -68,7 +65,8 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true), showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true) useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE
) )

View file

@ -43,10 +43,7 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.android.synthetic.main.fragment_report_statuses.* import kotlinx.android.synthetic.main.fragment_report_statuses.*
import javax.inject.Inject import javax.inject.Inject
@ -119,7 +116,8 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = false, showBotOverlay = false,
useBlurhash = preferences.getBoolean("useBlurhash", true) useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE
) )
adapter = StatusesAdapter(statusDisplayOptions, adapter = StatusesAdapter(statusDisplayOptions,

View file

@ -51,6 +51,7 @@ import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
@ -79,7 +80,8 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
mediaPreviewEnabled = viewModel.mediaPreviewEnabled, mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true), showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true) useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE
) )
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL))

View file

@ -69,6 +69,7 @@ import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.ReselectableFragment; import com.keylesspalace.tusky.interfaces.ReselectableFragment;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink; import com.keylesspalace.tusky.util.HttpHeaderLink;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
@ -244,7 +245,8 @@ public class NotificationsFragment extends SFragment implements
accountManager.getActiveAccount().getMediaPreviewEnabled(), accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false), preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true), preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true) preferences.getBoolean("useBlurhash", true),
CardViewMode.NONE
); );
adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(), adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(),

View file

@ -72,6 +72,7 @@ import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.repository.Placeholder; import com.keylesspalace.tusky.repository.Placeholder;
import com.keylesspalace.tusky.repository.TimelineRepository; import com.keylesspalace.tusky.repository.TimelineRepository;
import com.keylesspalace.tusky.repository.TimelineRequestMode; import com.keylesspalace.tusky.repository.TimelineRequestMode;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
@ -226,7 +227,10 @@ public class TimelineFragment extends SFragment implements
accountManager.getActiveAccount().getMediaPreviewEnabled(), accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false), preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true), preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true) preferences.getBoolean("useBlurhash", true),
preferences.getBoolean("showCardsInTimelines", false) ?
CardViewMode.INDENTED :
CardViewMode.NONE
); );
adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this); adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this);

View file

@ -58,6 +58,7 @@ import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StatusContext; import com.keylesspalace.tusky.entity.StatusContext;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
@ -131,7 +132,10 @@ public final class ViewThreadFragment extends SFragment implements
accountManager.getActiveAccount().getMediaPreviewEnabled(), accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false), preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true), preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true) preferences.getBoolean("useBlurhash", true),
preferences.getBoolean("showCardsInTimelines", false) ?
CardViewMode.INDENTED :
CardViewMode.NONE
); );
adapter = new ThreadAdapter(statusDisplayOptions, this); adapter = new ThreadAdapter(statusDisplayOptions, this);
} }

View file

@ -0,0 +1,7 @@
package com.keylesspalace.tusky.util
enum class CardViewMode {
NONE,
FULL_WIDTH,
INDENTED
}

View file

@ -10,5 +10,7 @@ data class StatusDisplayOptions(
@get:JvmName("showBotOverlay") @get:JvmName("showBotOverlay")
val showBotOverlay: Boolean, val showBotOverlay: Boolean,
@get:JvmName("useBlurhash") @get:JvmName("useBlurhash")
val useBlurhash: Boolean val useBlurhash: Boolean,
@get:JvmName("cardViewMode")
val cardViewMode: CardViewMode
) )

View file

@ -157,6 +157,73 @@
app:layout_constraintTop_toBottomOf="@id/status_content_warning_button" app:layout_constraintTop_toBottomOf="@id/status_content_warning_button"
tools:text="This is a status" /> tools:text="This is a status" />
<LinearLayout
android:id="@+id/status_card_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/card_frame"
android:clipChildren="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="80dp"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="@id/status_display_name"
app:layout_constraintTop_toBottomOf="@+id/status_content"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="gone">
<ImageView
android:id="@+id/card_image"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_margin="1dp"
android:background="?attr/colorBackgroundAccent"
android:importantForAccessibility="no"
android:scaleType="center" />
<LinearLayout
android:id="@+id/card_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="6dp"
android:paddingTop="6dp"
android:paddingRight="6dp"
android:paddingBottom="6dp">
<androidx.emoji.widget.EmojiTextView
android:id="@+id/card_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:lines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/card_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:lineSpacingMultiplier="1.1"
android:maxLines="2"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium" />
<TextView
android:id="@+id/card_link"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium" />
</LinearLayout>
</LinearLayout>
<Button <Button
android:id="@+id/button_toggle_content" android:id="@+id/button_toggle_content"
style="@style/TuskyButton.Outlined" style="@style/TuskyButton.Outlined"
@ -175,7 +242,7 @@
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
android:visibility="gone" android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/status_display_name" app:layout_constraintStart_toStartOf="@id/status_display_name"
app:layout_constraintTop_toBottomOf="@id/status_content" app:layout_constraintTop_toBottomOf="@id/status_card_view"
tools:text="@string/status_content_show_less" tools:text="@string/status_content_show_less"
tools:visibility="visible" /> tools:visibility="visible" />

View file

@ -131,7 +131,7 @@
tools:text="Status content. Can be pretty long. " /> tools:text="Status content. Can be pretty long. " />
<LinearLayout <LinearLayout
android:id="@+id/card_view" android:id="@+id/status_card_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
@ -204,7 +204,7 @@
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:background="@drawable/media_preview_outline" android:background="@drawable/media_preview_outline"
android:importantForAccessibility="noHideDescendants" android:importantForAccessibility="noHideDescendants"
app:layout_constraintTop_toBottomOf="@id/card_view"> app:layout_constraintTop_toBottomOf="@id/status_card_view">
<com.keylesspalace.tusky.view.MediaPreviewImageView <com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_0" android:id="@+id/status_media_preview_0"

View file

@ -548,5 +548,6 @@
<string name="no_saved_status">You don\'t have any drafts.</string> <string name="no_saved_status">You don\'t have any drafts.</string>
<string name="no_scheduled_status">You don\'t have any scheduled statuses.</string> <string name="no_scheduled_status">You don\'t have any scheduled statuses.</string>
<string name="warning_scheduling_interval">Mastodon has a minimum scheduling interval of 5 minutes.</string> <string name="warning_scheduling_interval">Mastodon has a minimum scheduling interval of 5 minutes.</string>
<string name="pref_title_show_cards_in_timelines">Show link previews in timelines</string>
</resources> </resources>

View file

@ -72,6 +72,12 @@
android:title="@string/pref_title_show_notifications_filter" android:title="@string/pref_title_show_notifications_filter"
app:singleLineTitle="false" /> app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="showCardsInTimelines"
android:title="@string/pref_title_show_cards_in_timelines"
app:singleLineTitle="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/pref_title_browser_settings"> <PreferenceCategory android:title="@string/pref_title_browser_settings">