Use blurhash as image preview and as sensitive media cover, close #1571 (#1581)

* Use blurhash as image preview and as sensitive media cover, close #1571

* Fix focal point for blurhashes

* Fix video indicator overlapping sensitive media indicator

* Add a preference for blurhash

* Add blurhash to report UI.

* Introduce StatusDisplayOptions
This commit is contained in:
Ivan Kupalov 2019-12-30 21:37:20 +01:00 committed by Konrad Pozniak
parent 2994af7091
commit 7623962a0d
32 changed files with 560 additions and 368 deletions

View file

@ -56,7 +56,7 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
} }
val fragment: Fragment = when(intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) { val fragment: Fragment = when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
GENERAL_PREFERENCES -> { GENERAL_PREFERENCES -> {
setTitle(R.string.action_view_preferences) setTitle(R.string.action_view_preferences)
PreferencesFragment.newInstance() PreferencesFragment.newInstance()
@ -128,7 +128,8 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
this.restartCurrentActivity() this.restartCurrentActivity()
} }
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars" -> { "statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
"useBlurhash" -> {
restartActivitiesOnExit = true restartActivitiesOnExit = true
} }
"language" -> { "language" -> {

View file

@ -33,6 +33,12 @@ import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.ToggleButton; import android.widget.ToggleButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.text.BidiFormatter;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Emoji;
@ -40,10 +46,11 @@ 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.CustomEmojiHelper; import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.SmartLengthInputFilter; import com.keylesspalace.tusky.util.SmartLengthInputFilter;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.viewdata.NotificationViewData; import com.keylesspalace.tusky.viewdata.NotificationViewData;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
import com.mikepenz.iconics.utils.Utils; import com.mikepenz.iconics.utils.Utils;
@ -53,12 +60,6 @@ import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.text.BidiFormatter;
import androidx.recyclerview.widget.RecyclerView;
public class NotificationsAdapter extends RecyclerView.Adapter { public class NotificationsAdapter extends RecyclerView.Adapter {
public interface AdapterDataSource<T> { public interface AdapterDataSource<T> {
@ -78,28 +79,23 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
private String accountId; private String accountId;
private StatusDisplayOptions statusDisplayOptions;
private StatusActionListener statusListener; private StatusActionListener statusListener;
private NotificationActionListener notificationActionListener; private NotificationActionListener notificationActionListener;
private boolean mediaPreviewEnabled;
private boolean useAbsoluteTime;
private boolean showBotOverlay;
private boolean animateAvatar;
private BidiFormatter bidiFormatter; private BidiFormatter bidiFormatter;
private AdapterDataSource<NotificationViewData> dataSource; private AdapterDataSource<NotificationViewData> dataSource;
public NotificationsAdapter(String accountId, public NotificationsAdapter(String accountId,
AdapterDataSource<NotificationViewData> dataSource, AdapterDataSource<NotificationViewData> dataSource,
StatusDisplayOptions statusDisplayOptions,
StatusActionListener statusListener, StatusActionListener statusListener,
NotificationActionListener notificationActionListener) { NotificationActionListener notificationActionListener) {
this.accountId = accountId; this.accountId = accountId;
this.dataSource = dataSource; this.dataSource = dataSource;
this.statusDisplayOptions = statusDisplayOptions;
this.statusListener = statusListener; this.statusListener = statusListener;
this.notificationActionListener = notificationActionListener; this.notificationActionListener = notificationActionListener;
mediaPreviewEnabled = true;
useAbsoluteTime = false;
showBotOverlay = true;
animateAvatar = false;
bidiFormatter = BidiFormatter.getInstance(); bidiFormatter = BidiFormatter.getInstance();
} }
@ -108,20 +104,20 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext()); LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType) { switch (viewType) {
case VIEW_TYPE_STATUS: { case VIEW_TYPE_STATUS: {
View view = inflater View view = inflater
.inflate(R.layout.item_status, parent, false); .inflate(R.layout.item_status, parent, false);
return new StatusViewHolder(view, useAbsoluteTime); return new StatusViewHolder(view);
} }
case VIEW_TYPE_STATUS_NOTIFICATION: { case VIEW_TYPE_STATUS_NOTIFICATION: {
View view = inflater View view = inflater
.inflate(R.layout.item_status_notification, parent, false); .inflate(R.layout.item_status_notification, parent, false);
return new StatusNotificationViewHolder(view, useAbsoluteTime, animateAvatar); return new StatusNotificationViewHolder(view, statusDisplayOptions);
} }
case VIEW_TYPE_FOLLOW: { case VIEW_TYPE_FOLLOW: {
View view = inflater View view = inflater
.inflate(R.layout.item_follow, parent, false); .inflate(R.layout.item_follow, parent, false);
return new FollowViewHolder(view, animateAvatar); return new FollowViewHolder(view, statusDisplayOptions);
} }
case VIEW_TYPE_PLACEHOLDER: { case VIEW_TYPE_PLACEHOLDER: {
View view = inflater View view = inflater
@ -137,7 +133,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
Utils.convertDpToPx(parent.getContext(), 24) Utils.convertDpToPx(parent.getContext(), 24)
) )
); );
return new RecyclerView.ViewHolder(view) {}; return new RecyclerView.ViewHolder(view) {
};
} }
} }
} }
@ -171,8 +168,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
StatusViewHolder holder = (StatusViewHolder) viewHolder; StatusViewHolder holder = (StatusViewHolder) viewHolder;
StatusViewData.Concrete status = concreteNotificaton.getStatusViewData(); StatusViewData.Concrete status = concreteNotificaton.getStatusViewData();
holder.setupWithStatus(status, holder.setupWithStatus(status,
statusListener, mediaPreviewEnabled, showBotOverlay, animateAvatar, payloadForHolder); statusListener, statusDisplayOptions, payloadForHolder);
if(concreteNotificaton.getType() == Notification.Type.POLL) { if (concreteNotificaton.getType() == Notification.Type.POLL) {
holder.setPollInfo(accountId.equals(concreteNotificaton.getAccount().getId())); holder.setPollInfo(accountId.equals(concreteNotificaton.getAccount().getId()));
} else { } else {
holder.hideStatusInfo(); holder.hideStatusInfo();
@ -202,7 +199,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
concreteNotificaton.getId()); concreteNotificaton.getId());
} else { } else {
if (payloadForHolder instanceof List) if (payloadForHolder instanceof List)
for (Object item : (List)payloadForHolder) { for (Object item : (List) payloadForHolder) {
if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item)) { if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item)) {
holder.setCreatedAt(statusViewData.getCreatedAt()); holder.setCreatedAt(statusViewData.getCreatedAt());
} }
@ -221,7 +218,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
default: default:
} }
} }
} }
@Override @Override
@ -229,6 +225,20 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
return dataSource.getItemCount(); return dataSource.getItemCount();
} }
public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) {
this.statusDisplayOptions = statusDisplayOptions.copy(
statusDisplayOptions.animateAvatars(),
mediaPreviewEnabled,
statusDisplayOptions.useAbsoluteTime(),
statusDisplayOptions.showBotOverlay(),
statusDisplayOptions.useBlurhash()
);
}
public boolean isMediaPreviewEnabled() {
return this.statusDisplayOptions.mediaPreviewEnabled();
}
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
NotificationViewData notification = dataSource.getItemAt(position); NotificationViewData notification = dataSource.getItemAt(position);
@ -256,26 +266,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
throw new AssertionError("Unknown notification type"); throw new AssertionError("Unknown notification type");
} }
}
public void setMediaPreviewEnabled(boolean enabled) {
mediaPreviewEnabled = enabled;
}
public boolean isMediaPreviewEnabled() {
return mediaPreviewEnabled;
}
public void setUseAbsoluteTime(boolean useAbsoluteTime) {
this.useAbsoluteTime = useAbsoluteTime;
}
public void setShowBotOverlay(boolean showBotOverlay) {
this.showBotOverlay = showBotOverlay;
}
public void setAnimateAvatar(boolean animateAvatar) {
this.animateAvatar = animateAvatar;
} }
public interface NotificationActionListener { public interface NotificationActionListener {
@ -300,15 +291,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private TextView usernameView; private TextView usernameView;
private TextView displayNameView; private TextView displayNameView;
private ImageView avatar; private ImageView avatar;
private boolean animateAvatar; private StatusDisplayOptions statusDisplayOptions;
FollowViewHolder(View itemView, boolean animateAvatar) { FollowViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) {
super(itemView); super(itemView);
message = itemView.findViewById(R.id.notification_text); message = itemView.findViewById(R.id.notification_text);
usernameView = itemView.findViewById(R.id.notification_username); usernameView = itemView.findViewById(R.id.notification_username);
displayNameView = itemView.findViewById(R.id.notification_display_name); displayNameView = itemView.findViewById(R.id.notification_display_name);
avatar = itemView.findViewById(R.id.notification_avatar); avatar = itemView.findViewById(R.id.notification_avatar);
this.animateAvatar = animateAvatar; this.statusDisplayOptions = statusDisplayOptions;
} }
void setMessage(Account account, BidiFormatter bidiFormatter) { void setMessage(Account account, BidiFormatter bidiFormatter) {
@ -330,7 +321,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
int avatarRadius = avatar.getContext().getResources() int avatarRadius = avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_42dp); .getDimensionPixelSize(R.dimen.avatar_radius_42dp);
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar); ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius,
statusDisplayOptions.animateAvatars());
} }
@ -352,18 +344,16 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private final TextView contentWarningDescriptionTextView; private final TextView contentWarningDescriptionTextView;
private final ToggleButton contentWarningButton; private final ToggleButton contentWarningButton;
private final ToggleButton contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder private final ToggleButton contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder
private StatusDisplayOptions statusDisplayOptions;
private String accountId; private String accountId;
private String notificationId; private String notificationId;
private NotificationActionListener notificationActionListener; private NotificationActionListener notificationActionListener;
private StatusViewData.Concrete statusViewData; private StatusViewData.Concrete statusViewData;
private boolean useAbsoluteTime;
private boolean animateAvatar;
private SimpleDateFormat shortSdf; private SimpleDateFormat shortSdf;
private SimpleDateFormat longSdf; private SimpleDateFormat longSdf;
StatusNotificationViewHolder(View itemView, boolean useAbsoluteTime, boolean animateAvatar) { StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) {
super(itemView); super(itemView);
message = itemView.findViewById(R.id.notification_top_text); message = itemView.findViewById(R.id.notification_top_text);
statusNameBar = itemView.findViewById(R.id.status_name_bar); statusNameBar = itemView.findViewById(R.id.status_name_bar);
@ -376,6 +366,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
contentWarningDescriptionTextView = itemView.findViewById(R.id.notification_content_warning_description); contentWarningDescriptionTextView = itemView.findViewById(R.id.notification_content_warning_description);
contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button); contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button);
contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content); contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content);
this.statusDisplayOptions = statusDisplayOptions;
int darkerFilter = Color.rgb(123, 123, 123); int darkerFilter = Color.rgb(123, 123, 123);
statusAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY); statusAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY);
@ -385,9 +376,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
message.setOnClickListener(this); message.setOnClickListener(this);
statusContent.setOnClickListener(this); statusContent.setOnClickListener(this);
contentWarningButton.setOnCheckedChangeListener(this); contentWarningButton.setOnCheckedChangeListener(this);
this.useAbsoluteTime = useAbsoluteTime;
this.animateAvatar = animateAvatar;
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
} }
@ -414,7 +402,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
} }
protected void setCreatedAt(@Nullable Date createdAt) { protected void setCreatedAt(@Nullable Date createdAt) {
if (useAbsoluteTime) { if (statusDisplayOptions.useAbsoluteTime()) {
String time; String time;
if (createdAt != null) { if (createdAt != null) {
if (System.currentTimeMillis() - createdAt.getTime() > 86400000L) { if (System.currentTimeMillis() - createdAt.getTime() > 86400000L) {
@ -511,13 +499,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
.getDimensionPixelSize(R.dimen.avatar_radius_36dp); .getDimensionPixelSize(R.dimen.avatar_radius_36dp);
ImageLoadingHelper.loadAvatar(statusAvatarUrl, ImageLoadingHelper.loadAvatar(statusAvatarUrl,
statusAvatar, statusAvatarRadius, animateAvatar); statusAvatar, statusAvatarRadius, statusDisplayOptions.animateAvatars());
int notificationAvatarRadius = statusAvatar.getContext().getResources() int notificationAvatarRadius = statusAvatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_24dp); .getDimensionPixelSize(R.dimen.avatar_radius_24dp);
ImageLoadingHelper.loadAvatar(notificationAvatarUrl, ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar,
notificationAvatar, notificationAvatarRadius, animateAvatar); notificationAvatarRadius, statusDisplayOptions.animateAvatars());
} }
@Override @Override

View file

@ -1,6 +1,8 @@
package com.keylesspalace.tusky.adapter; package com.keylesspalace.tusky.adapter;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
@ -32,6 +34,7 @@ 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;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.TimestampUtils; import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.view.MediaPreviewImageView; import com.keylesspalace.tusky.view.MediaPreviewImageView;
@ -49,7 +52,6 @@ import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import at.connyduck.sparkbutton.SparkButton; import at.connyduck.sparkbutton.SparkButton;
import at.connyduck.sparkbutton.SparkEventListener;
import kotlin.collections.CollectionsKt; import kotlin.collections.CollectionsKt;
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
@ -95,10 +97,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private int avatarRadius36dp; private int avatarRadius36dp;
private int avatarRadius24dp; private int avatarRadius24dp;
private final int mediaPreviewUnloadedId; private final Drawable mediaPreviewUnloaded;
protected StatusBaseViewHolder(View itemView, protected StatusBaseViewHolder(View itemView) {
boolean useAbsoluteTime) {
super(itemView); super(itemView);
displayName = itemView.findViewById(R.id.status_display_name); displayName = itemView.findViewById(R.id.status_display_name);
username = itemView.findViewById(R.id.status_username); username = itemView.findViewById(R.id.status_username);
@ -152,8 +153,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp); this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp); this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
mediaPreviewUnloadedId = ThemeUtils.getDrawableId(itemView.getContext(), mediaPreviewUnloaded = itemView.getContext().getDrawable(
R.attr.media_preview_unloaded_drawable, android.R.color.black); ThemeUtils.getDrawableId(itemView.getContext(),
R.attr.media_preview_unloaded_drawable, android.R.color.black)
);
} }
protected abstract int getMediaPreviewHeight(Context context); protected abstract int getMediaPreviewHeight(Context context);
@ -234,14 +237,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private void setAvatar(String url, private void setAvatar(String url,
@Nullable String rebloggedUrl, @Nullable String rebloggedUrl,
boolean isBot, boolean isBot,
boolean showBotOverlay, StatusDisplayOptions statusDisplayOptions) {
boolean animateAvatar) {
int avatarRadius; int avatarRadius;
if (TextUtils.isEmpty(rebloggedUrl)) { if (TextUtils.isEmpty(rebloggedUrl)) {
avatar.setPaddingRelative(0, 0, 0, 0); avatar.setPaddingRelative(0, 0, 0, 0);
if (showBotOverlay && isBot) { if (statusDisplayOptions.showBotOverlay() && isBot) {
avatarInset.setVisibility(View.VISIBLE); avatarInset.setVisibility(View.VISIBLE);
avatarInset.setBackgroundColor(0x50ffffff); avatarInset.setBackgroundColor(0x50ffffff);
Glide.with(avatarInset) Glide.with(avatarInset)
@ -260,12 +262,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
avatarInset.setVisibility(View.VISIBLE); avatarInset.setVisibility(View.VISIBLE);
avatarInset.setBackground(null); avatarInset.setBackground(null);
ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp, animateAvatar); ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp,
statusDisplayOptions.animateAvatars());
avatarRadius = avatarRadius36dp; avatarRadius = avatarRadius36dp;
} }
ImageLoadingHelper.loadAvatar(url, avatar, avatarRadius, animateAvatar); ImageLoadingHelper.loadAvatar(url, avatar, avatarRadius,
statusDisplayOptions.animateAvatars());
} }
@ -273,7 +277,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (useAbsoluteTime) { if (useAbsoluteTime) {
timestampInfo.setText(getAbsoluteTime(createdAt)); timestampInfo.setText(getAbsoluteTime(createdAt));
} else { } else {
if(createdAt == null) { if (createdAt == null) {
timestampInfo.setText("?m"); timestampInfo.setText("?m");
} else { } else {
long then = createdAt.getTime(); long then = createdAt.getTime();
@ -285,7 +289,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} }
private String getAbsoluteTime(Date createdAt) { private String getAbsoluteTime(Date createdAt) {
if(createdAt == null) { if (createdAt == null) {
return "??:??:??"; return "??:??:??";
} }
if (DateUtils.isToday(createdAt.getTime())) { if (DateUtils.isToday(createdAt.getTime())) {
@ -302,7 +306,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m" /* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
* as 17 meters instead of minutes. */ * as 17 meters instead of minutes. */
if(createdAt == null) { if (createdAt == null) {
return "? minutes"; return "? minutes";
} else { } else {
long then = createdAt.getTime(); long then = createdAt.getTime();
@ -367,12 +371,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
bookmarkButton.setChecked(bookmarked); bookmarkButton.setChecked(bookmarked);
} }
private void loadImage(MediaPreviewImageView imageView, String previewUrl, MetaData meta) { private BitmapDrawable decodeBlurHash(String blurhash) {
return ImageLoadingHelper.decodeBlurHash(this.avatar.getContext(), blurhash);
}
private void loadImage(MediaPreviewImageView imageView, String previewUrl, MetaData meta,
@Nullable String blurhash) {
Drawable placeholder = blurhash != null ? decodeBlurHash(blurhash) : mediaPreviewUnloaded;
if (TextUtils.isEmpty(previewUrl)) { if (TextUtils.isEmpty(previewUrl)) {
Glide.with(imageView) if (blurhash != null) {
.load(mediaPreviewUnloadedId) imageView.setImageDrawable(decodeBlurHash(blurhash));
.centerInside() } else {
.into(imageView); Glide.with(imageView)
.load(placeholder)
.centerInside()
.into(imageView);
}
} else { } else {
Focus focus = meta != null ? meta.getFocus() : null; Focus focus = meta != null ? meta.getFocus() : null;
@ -381,7 +395,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Glide.with(imageView) Glide.with(imageView)
.load(previewUrl) .load(previewUrl)
.placeholder(mediaPreviewUnloadedId) .placeholder(placeholder)
.centerInside() .centerInside()
.addListener(imageView) .addListener(imageView)
.into(imageView); .into(imageView);
@ -390,7 +404,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Glide.with(imageView) Glide.with(imageView)
.load(previewUrl) .load(previewUrl)
.placeholder(mediaPreviewUnloadedId) .placeholder(placeholder)
.centerInside() .centerInside()
.into(imageView); .into(imageView);
} }
@ -398,39 +412,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} }
protected void setMediaPreviews(final List<Attachment> attachments, boolean sensitive, protected void setMediaPreviews(final List<Attachment> attachments, boolean sensitive,
final StatusActionListener listener, boolean showingContent) { final StatusActionListener listener, boolean showingContent,
boolean useBlurhash) {
Context context = itemView.getContext(); Context context = itemView.getContext();
final int n = Math.min(attachments.size(), Status.MAX_MEDIA_ATTACHMENTS); final int n = Math.min(attachments.size(), Status.MAX_MEDIA_ATTACHMENTS);
for (int i = 0; i < n; i++) {
String previewUrl = attachments.get(i).getPreviewUrl();
String description = attachments.get(i).getDescription();
MediaPreviewImageView imageView = mediaPreviews[i];
imageView.setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(description)) {
imageView.setContentDescription(imageView.getContext()
.getString(R.string.action_view_media));
} else {
imageView.setContentDescription(description);
}
if (!sensitive || showingContent) {
loadImage(imageView, previewUrl, attachments.get(i).getMeta());
} else {
imageView.setImageResource(mediaPreviewUnloadedId);
}
final Attachment.Type type = attachments.get(i).getType();
if (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV) {
mediaOverlays[i].setVisibility(View.VISIBLE);
} else {
mediaOverlays[i].setVisibility(View.GONE);
}
setAttachmentClickListener(imageView, listener, i, attachments.get(i), true);
}
final int mediaPreviewHeight = getMediaPreviewHeight(context); final int mediaPreviewHeight = getMediaPreviewHeight(context);
@ -444,15 +430,51 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
mediaPreviews[3].getLayoutParams().height = mediaPreviewHeight; mediaPreviews[3].getLayoutParams().height = mediaPreviewHeight;
} }
for (int i = 0; i < n; i++) {
Attachment attachment = attachments.get(i);
String previewUrl = attachment.getPreviewUrl();
String description = attachment.getDescription();
MediaPreviewImageView imageView = mediaPreviews[i];
imageView.setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(description)) {
imageView.setContentDescription(imageView.getContext()
.getString(R.string.action_view_media));
} else {
imageView.setContentDescription(description);
}
if (showingContent) {
loadImage(imageView, previewUrl, attachment.getMeta(), attachment.getBlurhash());
} else {
imageView.setFocalPoint(null);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
if (useBlurhash && attachment.getBlurhash() != null) {
BitmapDrawable blurhashBitmap = decodeBlurHash(attachment.getBlurhash());
imageView.setImageDrawable(blurhashBitmap);
} else {
imageView.setImageDrawable(new ColorDrawable(ThemeUtils.getColor(
context, R.attr.sensitive_media_warning_background_color)));
}
}
final Attachment.Type type = attachment.getType();
if (showingContent && (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV)) {
mediaOverlays[i].setVisibility(View.VISIBLE);
} else {
mediaOverlays[i].setVisibility(View.GONE);
}
setAttachmentClickListener(imageView, listener, i, attachment, true);
}
final String hiddenContentText; final String hiddenContentText;
if (sensitive) { if (sensitive) {
hiddenContentText = context.getString(R.string.status_sensitive_media_template, hiddenContentText = context.getString(R.string.status_sensitive_media_title);
context.getString(R.string.status_sensitive_media_title),
context.getString(R.string.status_sensitive_media_directions));
} else { } else {
hiddenContentText = context.getString(R.string.status_sensitive_media_template, hiddenContentText = context.getString(R.string.status_media_hidden_title);
context.getString(R.string.status_media_hidden_title),
context.getString(R.string.status_sensitive_media_directions));
} }
sensitiveMediaWarning.setText(HtmlUtils.fromHtml(hiddenContentText)); sensitiveMediaWarning.setText(HtmlUtils.fromHtml(hiddenContentText));
@ -528,7 +550,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
view.setOnClickListener(v -> { view.setOnClickListener(v -> {
int position = getAdapterPosition(); int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onViewMedia(position, index, animateTransition ? v : null); if (sensitiveMediaWarning.getVisibility() == View.VISIBLE) {
listener.onContentHiddenChange(true, getAdapterPosition());
} else {
listener.onViewMedia(position, index, animateTransition ? v : null);
}
} }
}); });
view.setOnLongClickListener(v -> { view.setOnLongClickListener(v -> {
@ -540,7 +566,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private static CharSequence getAttachmentDescription(Context context, Attachment attachment) { private static CharSequence getAttachmentDescription(Context context, Attachment attachment) {
String duration = ""; String duration = "";
if(attachment.getMeta() != null && attachment.getMeta().getDuration() != null && attachment.getMeta().getDuration() > 0) { if (attachment.getMeta() != null && attachment.getMeta().getDuration() != null && attachment.getMeta().getDuration() > 0) {
duration = formatDuration(attachment.getMeta().getDuration()) + " "; duration = formatDuration(attachment.getMeta().getDuration()) + " ";
} }
if (TextUtils.isEmpty(attachment.getDescription())) { if (TextUtils.isEmpty(attachment.getDescription())) {
@ -608,29 +634,27 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} }
public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
boolean mediaPreviewEnabled, boolean showBotOverlay, boolean animateAvatar) { StatusDisplayOptions statusDisplayOptions) {
this.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, null); this.setupWithStatus(status, listener, statusDisplayOptions, null);
} }
protected void setupWithStatus(StatusViewData.Concrete status, protected void setupWithStatus(StatusViewData.Concrete status,
final StatusActionListener listener, final StatusActionListener listener,
boolean mediaPreviewEnabled, StatusDisplayOptions statusDisplayOptions,
boolean showBotOverlay,
boolean animateAvatar,
@Nullable Object payloads) { @Nullable Object payloads) {
if (payloads == null) { if (payloads == null) {
setDisplayName(status.getUserFullName(), status.getAccountEmojis()); setDisplayName(status.getUserFullName(), status.getAccountEmojis());
setUsername(status.getNickname()); setUsername(status.getNickname());
setCreatedAt(status.getCreatedAt()); setCreatedAt(status.getCreatedAt());
setIsReply(status.getInReplyToId() != null); setIsReply(status.getInReplyToId() != null);
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), showBotOverlay, animateAvatar); setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), statusDisplayOptions);
setReblogged(status.isReblogged()); setReblogged(status.isReblogged());
setFavourited(status.isFavourited()); setFavourited(status.isFavourited());
setBookmarked(status.isBookmarked()); setBookmarked(status.isBookmarked());
List<Attachment> attachments = status.getAttachments(); List<Attachment> attachments = status.getAttachments();
boolean sensitive = status.isSensitive(); boolean sensitive = status.isSensitive();
if (mediaPreviewEnabled && !hasAudioAttachment(attachments)) { if (statusDisplayOptions.mediaPreviewEnabled() && !hasAudioAttachment(attachments)) {
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent()); setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash());
if (attachments.size() == 0) { if (attachments.size() == 0) {
hideSensitiveMediaWarning(); hideSensitiveMediaWarning();
@ -674,7 +698,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} }
protected static boolean hasAudioAttachment(List<Attachment> attachments) { protected static boolean hasAudioAttachment(List<Attachment> attachments) {
for(Attachment attachment: attachments) { for (Attachment attachment : attachments) {
if (attachment.getType() == Attachment.Type.AUDIO) { if (attachment.getType() == Attachment.Type.AUDIO) {
return true; return true;
} }
@ -747,7 +771,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private static CharSequence getVisibilityDescription(Context context, Status.Visibility visibility) { private static CharSequence getVisibilityDescription(Context context, Status.Visibility visibility) {
if(visibility == null) { if (visibility == null) {
return ""; return "";
} }

View file

@ -16,6 +16,9 @@ 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.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.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners; import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
@ -25,14 +28,12 @@ import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CustomURLSpan; import com.keylesspalace.tusky.util.CustomURLSpan;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.text.DateFormat; import java.text.DateFormat;
import java.util.Date; import java.util.Date;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
class StatusDetailedViewHolder extends StatusBaseViewHolder { class StatusDetailedViewHolder extends StatusBaseViewHolder {
private TextView reblogs; private TextView reblogs;
private TextView favourites; private TextView favourites;
@ -44,8 +45,8 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
private TextView cardUrl; private TextView cardUrl;
private View infoDivider; private View infoDivider;
StatusDetailedViewHolder(View view, boolean useAbsoluteTime) { StatusDetailedViewHolder(View view) {
super(view, useAbsoluteTime); 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); cardView = view.findViewById(R.id.card_view);
@ -125,10 +126,11 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
} }
@Override @Override
protected void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener, protected void setupWithStatus(final StatusViewData.Concrete status,
boolean mediaPreviewEnabled, boolean showBotOverlay, boolean animateAvatar, final StatusActionListener listener,
StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) { @Nullable Object payloads) {
super.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, payloads); super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
if (payloads == null) { if (payloads == null) {
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);

View file

@ -22,14 +22,15 @@ import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import android.widget.ToggleButton; import android.widget.ToggleButton;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.SmartLengthInputFilter; import com.keylesspalace.tusky.util.SmartLengthInputFilter;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import at.connyduck.sparkbutton.helpers.Utils; import at.connyduck.sparkbutton.helpers.Utils;
public class StatusViewHolder extends StatusBaseViewHolder { public class StatusViewHolder extends StatusBaseViewHolder {
@ -39,8 +40,8 @@ public class StatusViewHolder extends StatusBaseViewHolder {
private TextView statusInfo; private TextView statusInfo;
private ToggleButton contentCollapseButton; private ToggleButton contentCollapseButton;
public StatusViewHolder(View itemView, boolean useAbsoluteTime) { public StatusViewHolder(View itemView) {
super(itemView, useAbsoluteTime); super(itemView);
statusInfo = itemView.findViewById(R.id.status_info); statusInfo = itemView.findViewById(R.id.status_info);
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content); contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
} }
@ -51,8 +52,9 @@ public class StatusViewHolder extends StatusBaseViewHolder {
} }
@Override @Override
protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, protected void setupWithStatus(StatusViewData.Concrete status,
boolean mediaPreviewEnabled, boolean showBotOverlay, boolean animateAvatar, final StatusActionListener listener,
StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) { @Nullable Object payloads) {
if (payloads == null) { if (payloads == null) {
@ -67,7 +69,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
} }
} }
super.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, payloads); super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
} }

View file

@ -15,15 +15,17 @@
package com.keylesspalace.tusky.adapter; package com.keylesspalace.tusky.adapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.util.ArrayList; import java.util.ArrayList;
@ -34,20 +36,14 @@ public class ThreadAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_STATUS_DETAILED = 1; private static final int VIEW_TYPE_STATUS_DETAILED = 1;
private List<StatusViewData.Concrete> statuses; private List<StatusViewData.Concrete> statuses;
private StatusDisplayOptions statusDisplayOptions;
private StatusActionListener statusActionListener; private StatusActionListener statusActionListener;
private boolean mediaPreviewEnabled;
private boolean useAbsoluteTime;
private boolean showBotOverlay;
private boolean animateAvatar;
private int detailedStatusPosition; private int detailedStatusPosition;
public ThreadAdapter(StatusActionListener listener) { public ThreadAdapter(StatusDisplayOptions statusDisplayOptions, StatusActionListener listener) {
this.statusDisplayOptions = statusDisplayOptions;
this.statusActionListener = listener; this.statusActionListener = listener;
this.statuses = new ArrayList<>(); this.statuses = new ArrayList<>();
mediaPreviewEnabled = true;
useAbsoluteTime = false;
showBotOverlay = true;
animateAvatar = false;
detailedStatusPosition = RecyclerView.NO_POSITION; detailedStatusPosition = RecyclerView.NO_POSITION;
} }
@ -59,12 +55,12 @@ public class ThreadAdapter extends RecyclerView.Adapter {
case VIEW_TYPE_STATUS: { case VIEW_TYPE_STATUS: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_status, parent, false); .inflate(R.layout.item_status, parent, false);
return new StatusViewHolder(view, useAbsoluteTime); return new StatusViewHolder(view);
} }
case VIEW_TYPE_STATUS_DETAILED: { case VIEW_TYPE_STATUS_DETAILED: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_status_detailed, parent, false); .inflate(R.layout.item_status_detailed, parent, false);
return new StatusDetailedViewHolder(view, useAbsoluteTime); return new StatusDetailedViewHolder(view);
} }
} }
} }
@ -74,10 +70,10 @@ public class ThreadAdapter extends RecyclerView.Adapter {
StatusViewData.Concrete status = statuses.get(position); StatusViewData.Concrete status = statuses.get(position);
if (position == detailedStatusPosition) { if (position == detailedStatusPosition) {
StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder; StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder;
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled, showBotOverlay, animateAvatar); holder.setupWithStatus(status, statusActionListener, statusDisplayOptions);
} else { } else {
StatusViewHolder holder = (StatusViewHolder) viewHolder; StatusViewHolder holder = (StatusViewHolder) viewHolder;
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled, showBotOverlay, animateAvatar); holder.setupWithStatus(status, statusActionListener, statusDisplayOptions);
} }
} }
@ -151,22 +147,6 @@ public class ThreadAdapter extends RecyclerView.Adapter {
} }
} }
public void setMediaPreviewEnabled(boolean enabled) {
mediaPreviewEnabled = enabled;
}
public void setUseAbsoluteTime(boolean useAbsoluteTime) {
this.useAbsoluteTime = useAbsoluteTime;
}
public void setShowBotOverlay(boolean showBotOverlay) {
this.showBotOverlay = showBotOverlay;
}
public void setAnimateAvatar(boolean animateAvatar) {
this.animateAvatar = animateAvatar;
}
public void setDetailedStatusPosition(int position) { public void setDetailedStatusPosition(int position) {
if (position != detailedStatusPosition if (position != detailedStatusPosition
&& detailedStatusPosition != RecyclerView.NO_POSITION) { && detailedStatusPosition != RecyclerView.NO_POSITION) {

View file

@ -15,19 +15,21 @@
package com.keylesspalace.tusky.adapter; package com.keylesspalace.tusky.adapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import java.util.List; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.util.List;
public final class TimelineAdapter extends RecyclerView.Adapter { public final class TimelineAdapter extends RecyclerView.Adapter {
public interface AdapterDataSource<T> { public interface AdapterDataSource<T> {
@ -40,20 +42,29 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_PLACEHOLDER = 2; private static final int VIEW_TYPE_PLACEHOLDER = 2;
private final AdapterDataSource<StatusViewData> dataSource; private final AdapterDataSource<StatusViewData> dataSource;
private StatusDisplayOptions statusDisplayOptions;
private final StatusActionListener statusListener; private final StatusActionListener statusListener;
private boolean mediaPreviewEnabled;
private boolean useAbsoluteTime;
private boolean showBotOverlay;
private boolean animateAvatar;
public TimelineAdapter(AdapterDataSource<StatusViewData> dataSource, public TimelineAdapter(AdapterDataSource<StatusViewData> dataSource,
StatusDisplayOptions statusDisplayOptions,
StatusActionListener statusListener) { StatusActionListener statusListener) {
this.dataSource = dataSource; this.dataSource = dataSource;
this.statusDisplayOptions = statusDisplayOptions;
this.statusListener = statusListener; this.statusListener = statusListener;
mediaPreviewEnabled = true; }
useAbsoluteTime = false;
showBotOverlay = true; public boolean getMediaPreviewEnabled() {
animateAvatar = false; return statusDisplayOptions.mediaPreviewEnabled();
}
public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) {
this.statusDisplayOptions = statusDisplayOptions.copy(
statusDisplayOptions.animateAvatars(),
mediaPreviewEnabled,
statusDisplayOptions.useAbsoluteTime(),
statusDisplayOptions.showBotOverlay(),
statusDisplayOptions.useBlurhash()
);
} }
@NonNull @NonNull
@ -64,7 +75,7 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
case VIEW_TYPE_STATUS: { case VIEW_TYPE_STATUS: {
View view = LayoutInflater.from(viewGroup.getContext()) View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_status, viewGroup, false); .inflate(R.layout.item_status, viewGroup, false);
return new StatusViewHolder(view, useAbsoluteTime); return new StatusViewHolder(view);
} }
case VIEW_TYPE_PLACEHOLDER: { case VIEW_TYPE_PLACEHOLDER: {
View view = LayoutInflater.from(viewGroup.getContext()) View view = LayoutInflater.from(viewGroup.getContext())
@ -76,16 +87,16 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
@Override @Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
bindViewHolder(viewHolder,position,null); bindViewHolder(viewHolder, position, null);
} }
@Override @Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) {
bindViewHolder(viewHolder,position,payloads); bindViewHolder(viewHolder, position, payloads);
} }
private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads){ private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads) {
StatusViewData status = dataSource.getItemAt(position); StatusViewData status = dataSource.getItemAt(position);
if (status instanceof StatusViewData.Placeholder) { if (status instanceof StatusViewData.Placeholder) {
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder; PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
@ -94,12 +105,11 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
StatusViewHolder holder = (StatusViewHolder) viewHolder; StatusViewHolder holder = (StatusViewHolder) viewHolder;
holder.setupWithStatus((StatusViewData.Concrete) status, holder.setupWithStatus((StatusViewData.Concrete) status,
statusListener, statusListener,
mediaPreviewEnabled, statusDisplayOptions,
showBotOverlay,
animateAvatar,
payloads != null && !payloads.isEmpty() ? payloads.get(0) : null); payloads != null && !payloads.isEmpty() ? payloads.get(0) : null);
} }
} }
@Override @Override
public int getItemCount() { public int getItemCount() {
return dataSource.getItemCount(); return dataSource.getItemCount();
@ -114,26 +124,6 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
} }
} }
public void setMediaPreviewEnabled(boolean enabled) {
mediaPreviewEnabled = enabled;
}
public void setUseAbsoluteTime(boolean useAbsoluteTime){
this.useAbsoluteTime = useAbsoluteTime;
}
public boolean getMediaPreviewEnabled() {
return mediaPreviewEnabled;
}
public void setShowBotOverlay(boolean showBotOverlay) {
this.showBotOverlay = showBotOverlay;
}
public void setAnimateAvatar(boolean animateAvatar) {
this.animateAvatar = animateAvatar;
}
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
return dataSource.getItemAt(position).getViewDataId(); return dataSource.getItemAt(position).getViewDataId();

View file

@ -12,20 +12,21 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.NetworkStateViewHolder import com.keylesspalace.tusky.adapter.NetworkStateViewHolder
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions
class ConversationAdapter(private val useAbsoluteTime: Boolean, class ConversationAdapter(
private val mediaPreviewEnabled: Boolean, private val statusDisplayOptions: StatusDisplayOptions,
private val listener: StatusActionListener, private val listener: StatusActionListener,
private val topLoadedCallback: () -> Unit, private val topLoadedCallback: () -> Unit,
private val retryCallback: () -> Unit) private val retryCallback: () -> Unit
: RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var networkState: NetworkState? = null private var networkState: NetworkState? = null
private val differ: AsyncPagedListDiffer<ConversationEntity> = AsyncPagedListDiffer(object: ListUpdateCallback { private val differ: AsyncPagedListDiffer<ConversationEntity> = AsyncPagedListDiffer(object : ListUpdateCallback {
override fun onInserted(position: Int, count: Int) { override fun onInserted(position: Int, count: Int) {
notifyItemRangeInserted(position, count) notifyItemRangeInserted(position, count)
if(position == 0) { if (position == 0) {
topLoadedCallback() topLoadedCallback()
} }
} }
@ -51,7 +52,8 @@ class ConversationAdapter(private val useAbsoluteTime: Boolean,
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) { return when (viewType) {
R.layout.item_network_state -> NetworkStateViewHolder(view, retryCallback) R.layout.item_network_state -> NetworkStateViewHolder(view, retryCallback)
R.layout.item_conversation -> ConversationViewHolder(view, listener, useAbsoluteTime, mediaPreviewEnabled) R.layout.item_conversation -> ConversationViewHolder(view, statusDisplayOptions,
listener)
else -> throw IllegalArgumentException("unknown view type $viewType") else -> throw IllegalArgumentException("unknown view type $viewType")
} }
} }

View file

@ -23,7 +23,6 @@ import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.ToggleButton; import android.widget.ToggleButton;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
@ -32,6 +31,7 @@ import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.SmartLengthInputFilter; import com.keylesspalace.tusky.util.SmartLengthInputFilter;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.PollViewDataKt; import com.keylesspalace.tusky.viewdata.PollViewDataKt;
import java.util.List; import java.util.List;
@ -44,15 +44,13 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
private ToggleButton contentCollapseButton; private ToggleButton contentCollapseButton;
private ImageView[] avatars; private ImageView[] avatars;
private StatusDisplayOptions statusDisplayOptions;
private StatusActionListener listener; private StatusActionListener listener;
private boolean mediaPreviewEnabled;
private boolean animateAvatars;
ConversationViewHolder(View itemView, ConversationViewHolder(View itemView,
StatusActionListener listener, StatusDisplayOptions statusDisplayOptions,
boolean useAbsoluteTime, StatusActionListener listener) {
boolean mediaPreviewEnabled) { super(itemView);
super(itemView, useAbsoluteTime);
conversationNameTextView = itemView.findViewById(R.id.conversation_name); conversationNameTextView = itemView.findViewById(R.id.conversation_name);
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content); contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
avatars = new ImageView[]{ avatars = new ImageView[]{
@ -60,11 +58,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
itemView.findViewById(R.id.status_avatar_1), itemView.findViewById(R.id.status_avatar_1),
itemView.findViewById(R.id.status_avatar_2) itemView.findViewById(R.id.status_avatar_2)
}; };
this.statusDisplayOptions = statusDisplayOptions;
this.listener = listener; this.listener = listener;
this.mediaPreviewEnabled = mediaPreviewEnabled;
this.animateAvatars = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()).getBoolean("animateGifAvatars", false);
} }
@Override @Override
@ -86,8 +83,9 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
setBookmarked(status.getBookmarked()); setBookmarked(status.getBookmarked());
List<Attachment> attachments = status.getAttachments(); List<Attachment> attachments = status.getAttachments();
boolean sensitive = status.getSensitive(); boolean sensitive = status.getSensitive();
if(mediaPreviewEnabled && !hasAudioAttachment(attachments)) { if (statusDisplayOptions.mediaPreviewEnabled() && !hasAudioAttachment(attachments)) {
setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent()); setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(),
statusDisplayOptions.useBlurhash());
if (attachments.size() == 0) { if (attachments.size() == 0) {
hideSensitiveMediaWarning(); hideSensitiveMediaWarning();
@ -118,11 +116,11 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
private void setConversationName(List<ConversationAccountEntity> accounts) { private void setConversationName(List<ConversationAccountEntity> accounts) {
Context context = conversationNameTextView.getContext(); Context context = conversationNameTextView.getContext();
String conversationName = ""; String conversationName = "";
if(accounts.size() == 1) { if (accounts.size() == 1) {
conversationName = context.getString(R.string.conversation_1_recipients, accounts.get(0).getUsername()); conversationName = context.getString(R.string.conversation_1_recipients, accounts.get(0).getUsername());
} else if(accounts.size() == 2) { } else if (accounts.size() == 2) {
conversationName = context.getString(R.string.conversation_2_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername()); conversationName = context.getString(R.string.conversation_2_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername());
} else if (accounts.size() > 2){ } else if (accounts.size() > 2) {
conversationName = context.getString(R.string.conversation_more_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername(), accounts.size() - 2); conversationName = context.getString(R.string.conversation_more_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername(), accounts.size() - 2);
} }
@ -130,10 +128,11 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
} }
private void setAvatars(List<ConversationAccountEntity> accounts) { private void setAvatars(List<ConversationAccountEntity> accounts) {
for(int i=0; i < avatars.length; i++) { for (int i = 0; i < avatars.length; i++) {
ImageView avatarView = avatars[i]; ImageView avatarView = avatars[i];
if(i < accounts.size()) { if (i < accounts.size()) {
ImageLoadingHelper.loadAvatar(accounts.get(i).getAvatar(), avatarView, avatarRadius48dp, animateAvatars); ImageLoadingHelper.loadAvatar(accounts.get(i).getAvatar(), avatarView,
avatarRadius48dp, statusDisplayOptions.animateAvatars());
avatarView.setVisibility(View.VISIBLE); avatarView.setVisibility(View.VISIBLE);
} else { } else {
avatarView.setVisibility(View.GONE); avatarView.setVisibility(View.GONE);

View file

@ -37,6 +37,7 @@ 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.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import kotlinx.android.synthetic.main.fragment_timeline.* import kotlinx.android.synthetic.main.fragment_timeline.*
@ -62,15 +63,18 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context) val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
val account = accountManager.activeAccount val statusDisplayOptions = StatusDisplayOptions(
val mediaPreviewEnabled = account?.mediaPreviewEnabled ?: true animateAvatars = preferences.getBoolean("animateGifAvatars", false),
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true)
)
adapter = ConversationAdapter(useAbsoluteTime, mediaPreviewEnabled, this, ::onTopLoaded, viewModel::retry) adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry)
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(view.context) layoutManager = LinearLayoutManager(view.context)

View file

@ -31,12 +31,13 @@ import com.keylesspalace.tusky.viewdata.toViewData
import kotlinx.android.synthetic.main.item_report_status.view.* import kotlinx.android.synthetic.main.item_report_status.view.*
import java.util.* import java.util.*
class StatusViewHolder(itemView: View, class StatusViewHolder(
private val useAbsoluteTime: Boolean, itemView: View,
private val mediaPreviewEnabled: Boolean, private val statusDisplayOptions: StatusDisplayOptions,
private val viewState: StatusViewState, private val viewState: StatusViewState,
private val adapterHandler: AdapterHandler, private val adapterHandler: AdapterHandler,
private val getStatusForPosition: (Int) -> Status?) : RecyclerView.ViewHolder(itemView) { private val getStatusForPosition: (Int) -> Status?
) : RecyclerView.ViewHolder(itemView) {
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height) private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
private val statusViewHelper = StatusViewHelper(itemView) private val statusViewHelper = StatusViewHelper(itemView)
@ -69,11 +70,11 @@ class StatusViewHolder(itemView: View,
val sensitive = status.sensitive val sensitive = status.sensitive
statusViewHelper.setMediasPreview(mediaPreviewEnabled, status.attachments, sensitive, previewListener, statusViewHelper.setMediasPreview(statusDisplayOptions, status.attachments,
viewState.isMediaShow(status.id, status.sensitive), sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
mediaViewHeight) mediaViewHeight)
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, useAbsoluteTime) statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions.useAbsoluteTime)
setCreatedAt(status.createdAt) setCreatedAt(status.createdAt)
} }
@ -124,7 +125,7 @@ class StatusViewHolder(itemView: View,
} }
private fun setCreatedAt(createdAt: Date?) { private fun setCreatedAt(createdAt: Date?) {
if (useAbsoluteTime) { if (statusDisplayOptions.useAbsoluteTime) {
itemView.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt) itemView.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt)
} else { } else {
itemView.timestampInfo.text = if (createdAt != null) { itemView.timestampInfo.text = if (createdAt != null) {

View file

@ -23,12 +23,13 @@ import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.model.StatusViewState import com.keylesspalace.tusky.components.report.model.StatusViewState
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.StatusDisplayOptions
class StatusesAdapter(private val useAbsoluteTime: Boolean, class StatusesAdapter(
private val mediaPreviewEnabled: Boolean, private val statusDisplayOptions: StatusDisplayOptions,
private val statusViewState: StatusViewState, private val statusViewState: StatusViewState,
private val adapterHandler: AdapterHandler) private val adapterHandler: AdapterHandler
: PagedListAdapter<Status, RecyclerView.ViewHolder>(STATUS_COMPARATOR) { ) : PagedListAdapter<Status, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
private val statusForPosition: (Int) -> Status? = { position: Int -> private val statusForPosition: (Int) -> Status? = { position: Int ->
if (position != RecyclerView.NO_POSITION) getItem(position) else null if (position != RecyclerView.NO_POSITION) getItem(position) else null
@ -36,8 +37,10 @@ class StatusesAdapter(private val useAbsoluteTime: Boolean,
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return StatusViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_report_status, parent, false), val view = LayoutInflater.from(parent.context)
useAbsoluteTime, mediaPreviewEnabled, statusViewState, adapterHandler, statusForPosition) .inflate(R.layout.item_report_status, parent, false)
return StatusViewHolder(view, statusDisplayOptions, statusViewState, adapterHandler,
statusForPosition)
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {

View file

@ -43,6 +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.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
@ -119,14 +120,16 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
private fun initStatusesView() { private fun initStatusesView() {
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = false,
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = false,
useBlurhash = preferences.getBoolean("useBlurhash", true)
)
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false) adapter = StatusesAdapter(statusDisplayOptions,
viewModel.statusViewState, this)
val account = accountManager.activeAccount
val mediaPreviewEnabled = account?.mediaPreviewEnabled ?: true
adapter = StatusesAdapter(useAbsoluteTime, mediaPreviewEnabled, viewModel.statusViewState, this)
recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(requireContext()) layoutManager = LinearLayoutManager(requireContext())

View file

@ -24,28 +24,26 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.StatusViewHolder import com.keylesspalace.tusky.adapter.StatusViewHolder
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.StatusDisplayOptions
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
class SearchStatusesAdapter(private val useAbsoluteTime: Boolean, class SearchStatusesAdapter(
private val mediaPreviewEnabled: Boolean, private val statusDisplayOptions: StatusDisplayOptions,
private val showBotOverlay: Boolean, private val statusListener: StatusActionListener
private val animateAvatar: Boolean, ) : PagedListAdapter<Pair<Status, StatusViewData.Concrete>, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
private val statusListener: StatusActionListener)
: PagedListAdapter<Pair<Status, StatusViewData.Concrete>, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_status, parent, false) .inflate(R.layout.item_status, parent, false)
return StatusViewHolder(view, useAbsoluteTime) return StatusViewHolder(view)
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { item -> getItem(position)?.let { item ->
(holder as? StatusViewHolder)?.setupWithStatus(item.second, statusListener, (holder as? StatusViewHolder)?.setupWithStatus(item.second, statusListener,
mediaPreviewEnabled, showBotOverlay, animateAvatar) statusDisplayOptions)
} }
} }
public override fun getItem(position: Int): Pair<Status, StatusViewData.Concrete>? { public override fun getItem(position: Int): Pair<Status, StatusViewData.Concrete>? {

View file

@ -52,6 +52,7 @@ 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.NetworkState import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
@ -71,13 +72,17 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> { override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context)
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false) val statusDisplayOptions = StatusDisplayOptions(
val showBotOverlay = preferences.getBoolean("showBotOverlay", true) animateAvatars = preferences.getBoolean("animateGifAvatars", false),
val animateAvatar = preferences.getBoolean("animateGifAvatars", false) mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true)
)
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL))
searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context) searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context)
return SearchStatusesAdapter(useAbsoluteTime, viewModel.mediaPreviewEnabled, showBotOverlay, animateAvatar, this) return SearchStatusesAdapter(statusDisplayOptions, this)
} }

View file

@ -31,7 +31,8 @@ data class Attachment(
@SerializedName("preview_url") val previewUrl: String, @SerializedName("preview_url") val previewUrl: String,
val meta: MetaData?, val meta: MetaData?,
val type: Type, val type: Type,
val description: String? val description: String?,
val blurhash: String?
) : Parcelable { ) : Parcelable {
@JsonAdapter(MediaTypeDeserializer::class) @JsonAdapter(MediaTypeDeserializer::class)

View file

@ -75,6 +75,7 @@ import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.ListUtils; import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.NotificationTypeConverterKt; import com.keylesspalace.tusky.util.NotificationTypeConverterKt;
import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.util.ViewDataUtils;
import com.keylesspalace.tusky.view.BackgroundMessageView; import com.keylesspalace.tusky.view.BackgroundMessageView;
@ -237,18 +238,18 @@ public class NotificationsFragment extends SFragment implements
recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL)); recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL));
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
preferences.getBoolean("animateGifAvatars", false),
accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true)
);
adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(), adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(),
dataSource, this, this); dataSource, statusDisplayOptions, this, this);
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler(); alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
boolean mediaPreviewEnabled = accountManager.getActiveAccount().getMediaPreviewEnabled();
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
adapter.setUseAbsoluteTime(useAbsoluteTime);
boolean showBotOverlay = preferences.getBoolean("showBotOverlay", true);
adapter.setShowBotOverlay(showBotOverlay);
boolean animateAvatar = preferences.getBoolean("animateGifAvatars", false);
adapter.setAnimateAvatar(animateAvatar);
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
topLoading = false; topLoading = false;

View file

@ -77,6 +77,7 @@ import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.ListUtils; import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.util.ViewDataUtils;
@ -219,7 +220,15 @@ public class TimelineFragment extends SFragment implements
hashtagOrId = arguments.getString(HASHTAG_OR_ID_ARG); hashtagOrId = arguments.getString(HASHTAG_OR_ID_ARG);
} }
adapter = new TimelineAdapter(dataSource, this); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
preferences.getBoolean("animateGifAvatars", false),
accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true)
);
adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this);
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true); isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true);
@ -341,18 +350,10 @@ public class TimelineFragment extends SFragment implements
} }
private void setupTimelinePreferences() { private void setupTimelinePreferences() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler(); alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
boolean mediaPreviewEnabled = accountManager.getActiveAccount().getMediaPreviewEnabled();
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
adapter.setUseAbsoluteTime(useAbsoluteTime);
boolean showBotOverlay = preferences.getBoolean("showBotOverlay", true);
adapter.setShowBotOverlay(showBotOverlay);
boolean animateAvatar = preferences.getBoolean("animateGifAvatars", false);
adapter.setAnimateAvatar(animateAvatar);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean filter = preferences.getBoolean("tabFilterHomeReplies", true); boolean filter = preferences.getBoolean("tabFilterHomeReplies", true);
filterRemoveReplies = kind == Kind.HOME && !filter; filterRemoveReplies = kind == Kind.HOME && !filter;

View file

@ -60,6 +60,7 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
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.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.util.ViewDataUtils;
import com.keylesspalace.tusky.view.ConversationLineItemDecoration; import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
@ -123,8 +124,16 @@ public final class ViewThreadFragment extends SFragment implements
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
thisThreadsStatusId = getArguments().getString("id"); thisThreadsStatusId = getArguments().getString("id");
SharedPreferences preferences =
adapter = new ThreadAdapter(this); PreferenceManager.getDefaultSharedPreferences(getActivity());
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
preferences.getBoolean("animateGifAvatars", false),
accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true)
);
adapter = new ThreadAdapter(statusDisplayOptions, this);
} }
@Override @Override
@ -150,18 +159,8 @@ public final class ViewThreadFragment extends SFragment implements
recyclerView.addItemDecoration(divider); recyclerView.addItemDecoration(divider);
recyclerView.addItemDecoration(new ConversationLineItemDecoration(context)); recyclerView.addItemDecoration(new ConversationLineItemDecoration(context));
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
getActivity());
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler(); alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
boolean mediaPreviewEnabled = accountManager.getActiveAccount().getMediaPreviewEnabled();
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
adapter.setUseAbsoluteTime(useAbsoluteTime);
boolean animateAvatars = preferences.getBoolean("animateGifAvatars", false);
adapter.setAnimateAvatar(animateAvatars);
boolean showBotIndicator = preferences.getBoolean("showBotOverlay", true);
adapter.setShowBotOverlay(showBotIndicator);
reloadFilters(false); reloadFilters(false);
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);

View file

@ -0,0 +1,130 @@
/**
* Blurhash implementation from blurhash project:
* https://github.com/woltapp/blurhash
* Minor modifications by charlag
*/
package com.keylesspalace.tusky.util
import android.graphics.Bitmap
import android.graphics.Color
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.withSign
object BlurHashDecoder {
fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f): Bitmap? {
require(width > 0) { "Width must be greater than zero" }
require(height > 0) { "height must be greater than zero" }
if (blurHash == null || blurHash.length < 6) {
return null
}
val numCompEnc = decode83(blurHash, 0, 1)
val numCompX = (numCompEnc % 9) + 1
val numCompY = (numCompEnc / 9) + 1
if (blurHash.length != 4 + 2 * numCompX * numCompY) {
return null
}
val maxAcEnc = decode83(blurHash, 1, 2)
val maxAc = (maxAcEnc + 1) / 166f
val colors = Array(numCompX * numCompY) { i ->
if (i == 0) {
val colorEnc = decode83(blurHash, 2, 6)
decodeDc(colorEnc)
} else {
val from = 4 + i * 2
val colorEnc = decode83(blurHash, from, from + 2)
decodeAc(colorEnc, maxAc * punch)
}
}
return composeBitmap(width, height, numCompX, numCompY, colors)
}
private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int {
var result = 0
for (i in from until to) {
val index = charMap[str[i]] ?: -1
if (index != -1) {
result = result * 83 + index
}
}
return result
}
private fun decodeDc(colorEnc: Int): FloatArray {
val r = colorEnc shr 16
val g = (colorEnc shr 8) and 255
val b = colorEnc and 255
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))
}
private fun srgbToLinear(colorEnc: Int): Float {
val v = colorEnc / 255f
return if (v <= 0.04045f) {
(v / 12.92f)
} else {
((v + 0.055f) / 1.055f).pow(2.4f)
}
}
private fun decodeAc(value: Int, maxAc: Float): FloatArray {
val r = value / (19 * 19)
val g = (value / 19) % 19
val b = value % 19
return floatArrayOf(
signedPow2((r - 9) / 9.0f) * maxAc,
signedPow2((g - 9) / 9.0f) * maxAc,
signedPow2((b - 9) / 9.0f) * maxAc
)
}
private fun signedPow2(value: Float) = value.pow(2f).withSign(value)
private fun composeBitmap(
width: Int, height: Int,
numCompX: Int, numCompY: Int,
colors: Array<FloatArray>
): Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
for (y in 0 until height) {
for (x in 0 until width) {
var r = 0f
var g = 0f
var b = 0f
for (j in 0 until numCompY) {
for (i in 0 until numCompX) {
val basis = (cos(PI * x * i / width) * cos(PI * y * j / height)).toFloat()
val color = colors[j * numCompX + i]
r += color[0] * basis
g += color[1] * basis
b += color[2] * basis
}
}
bitmap.setPixel(x, y, Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)))
}
}
return bitmap
}
private fun linearToSrgb(value: Float): Int {
val v = value.coerceIn(0f, 1f)
return if (v <= 0.0031308f) {
(v * 12.92f * 255f + 0.5f).toInt()
} else {
((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()
}
}
private val charMap = listOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
)
.mapIndexed { i, c -> c to i }
.toMap()
}

View file

@ -2,6 +2,8 @@
package com.keylesspalace.tusky.util package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.Px import androidx.annotation.Px
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@ -14,7 +16,7 @@ private val centerCropTransformation = CenterCrop()
fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boolean) { fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boolean) {
if(url.isNullOrBlank()) { if (url.isNullOrBlank()) {
Glide.with(imageView) Glide.with(imageView)
.load(R.drawable.avatar_default) .load(R.drawable.avatar_default)
.into(imageView) .into(imageView)
@ -42,4 +44,8 @@ fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boo
} }
} }
}
fun decodeBlurHash(context: Context, blurhash: String): BitmapDrawable {
return BitmapDrawable(context.resources, BlurHashDecoder.decode(blurhash, 32, 32, 1f))
} }

View file

@ -0,0 +1,14 @@
package com.keylesspalace.tusky.util
data class StatusDisplayOptions(
@get:JvmName("animateAvatars")
val animateAvatars: Boolean,
@get:JvmName("mediaPreviewEnabled")
val mediaPreviewEnabled: Boolean,
@get:JvmName("useAbsoluteTime")
val useAbsoluteTime: Boolean,
@get:JvmName("showBotOverlay")
val showBotOverlay: Boolean,
@get:JvmName("useBlurhash")
val useBlurhash: Boolean
)

View file

@ -16,6 +16,7 @@
package com.keylesspalace.tusky.util package com.keylesspalace.tusky.util
import android.content.Context import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.text.InputFilter import android.text.InputFilter
import android.text.TextUtils import android.text.TextUtils
import android.view.View import android.view.View
@ -47,7 +48,7 @@ class StatusViewHelper(private val itemView: View) {
private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()) private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault())
fun setMediasPreview( fun setMediasPreview(
mediaPreviewEnabled: Boolean, statusDisplayOptions: StatusDisplayOptions,
attachments: List<Attachment>, attachments: List<Attachment>,
sensitive: Boolean, sensitive: Boolean,
previewListener: MediaPreviewListener, previewListener: MediaPreviewListener,
@ -70,7 +71,7 @@ class StatusViewHelper(private val itemView: View) {
val sensitiveMediaWarning = itemView.findViewById<TextView>(R.id.status_sensitive_media_warning) val sensitiveMediaWarning = itemView.findViewById<TextView>(R.id.status_sensitive_media_warning)
val sensitiveMediaShow = itemView.findViewById<View>(R.id.status_sensitive_media_button) val sensitiveMediaShow = itemView.findViewById<View>(R.id.status_sensitive_media_button)
val mediaLabel = itemView.findViewById<TextView>(R.id.status_media_label) val mediaLabel = itemView.findViewById<TextView>(R.id.status_media_label)
if (mediaPreviewEnabled) { if (statusDisplayOptions.mediaPreviewEnabled) {
// Hide the unused label. // Hide the unused label.
mediaLabel.visibility = View.GONE mediaLabel.visibility = View.GONE
} else { } else {
@ -86,13 +87,15 @@ class StatusViewHelper(private val itemView: View) {
} }
val mediaPreviewUnloadedId = ThemeUtils.getDrawableId(context, R.attr.media_preview_unloaded_drawable, android.R.color.black) val mediaPreviewUnloaded = ThemeUtils.getDrawable(context,
R.attr.media_preview_unloaded_drawable, android.R.color.black)
val n = min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS) val n = min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS)
for (i in 0 until n) { for (i in 0 until n) {
val previewUrl = attachments[i].previewUrl val attachment = attachments[i]
val description = attachments[i].description val previewUrl = attachment.previewUrl
val description = attachment.description
if (TextUtils.isEmpty(description)) { if (TextUtils.isEmpty(description)) {
mediaPreviews[i].contentDescription = context.getString(R.string.action_view_media) mediaPreviews[i].contentDescription = context.getString(R.string.action_view_media)
@ -104,35 +107,49 @@ class StatusViewHelper(private val itemView: View) {
if (TextUtils.isEmpty(previewUrl)) { if (TextUtils.isEmpty(previewUrl)) {
Glide.with(mediaPreviews[i]) Glide.with(mediaPreviews[i])
.load(mediaPreviewUnloadedId) .load(mediaPreviewUnloaded)
.centerInside() .centerInside()
.into(mediaPreviews[i]) .into(mediaPreviews[i])
} else { } else {
val meta = attachments[i].meta val placeholder = if (attachment.blurhash != null)
decodeBlurHash(context, attachment.blurhash)
else mediaPreviewUnloaded
val meta = attachment.meta
val focus = meta?.focus val focus = meta?.focus
if (showingContent) {
if (focus != null) { // If there is a focal point for this attachment:
mediaPreviews[i].setFocalPoint(focus)
if (focus != null) { // If there is a focal point for this attachment: Glide.with(mediaPreviews[i])
mediaPreviews[i].setFocalPoint(focus) .load(previewUrl)
.placeholder(placeholder)
.centerInside()
.addListener(mediaPreviews[i])
.into(mediaPreviews[i])
} else {
mediaPreviews[i].removeFocalPoint()
Glide.with(mediaPreviews[i]) Glide.with(mediaPreviews[i])
.load(previewUrl) .load(previewUrl)
.placeholder(mediaPreviewUnloadedId) .placeholder(placeholder)
.centerInside() .centerInside()
.addListener(mediaPreviews[i]) .into(mediaPreviews[i])
.into(mediaPreviews[i]) }
} else { } else {
mediaPreviews[i].removeFocalPoint() mediaPreviews[i].removeFocalPoint()
if (statusDisplayOptions.useBlurhash && attachment.blurhash != null) {
Glide.with(mediaPreviews[i]) val blurhashBitmap = decodeBlurHash(context, attachment.blurhash)
.load(previewUrl) mediaPreviews[i].setImageDrawable(blurhashBitmap)
.placeholder(mediaPreviewUnloadedId) } else {
.centerInside() mediaPreviews[i].setImageDrawable(ColorDrawable(ThemeUtils.getColor(
.into(mediaPreviews[i]) context, R.attr.sensitive_media_warning_background_color)))
}
} }
} }
val type = attachments[i].type val type = attachment.type
if ((type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV)) { if (showingContent
&& (type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV)) {
mediaOverlays[i].visibility = View.VISIBLE mediaOverlays[i].visibility = View.VISIBLE
} else { } else {
mediaOverlays[i].visibility = View.GONE mediaOverlays[i].visibility = View.GONE
@ -158,13 +175,9 @@ class StatusViewHelper(private val itemView: View) {
} else { } else {
val hiddenContentText: String = if (sensitive) { val hiddenContentText: String = if (sensitive) {
context.getString(R.string.status_sensitive_media_template, context.getString(R.string.status_sensitive_media_title)
context.getString(R.string.status_sensitive_media_title),
context.getString(R.string.status_sensitive_media_directions))
} else { } else {
context.getString(R.string.status_sensitive_media_template, context.getString(R.string.status_media_hidden_title)
context.getString(R.string.status_media_hidden_title),
context.getString(R.string.status_sensitive_media_directions))
} }
sensitiveMediaWarning.text = HtmlUtils.fromHtml(hiddenContentText) sensitiveMediaWarning.text = HtmlUtils.fromHtml(hiddenContentText)
@ -175,11 +188,15 @@ class StatusViewHelper(private val itemView: View) {
previewListener.onContentHiddenChange(false) previewListener.onContentHiddenChange(false)
v.visibility = View.GONE v.visibility = View.GONE
sensitiveMediaWarning.visibility = View.VISIBLE sensitiveMediaWarning.visibility = View.VISIBLE
setMediasPreview(statusDisplayOptions, attachments, sensitive, previewListener,
false, mediaPreviewHeight)
} }
sensitiveMediaWarning.setOnClickListener { v -> sensitiveMediaWarning.setOnClickListener { v ->
previewListener.onContentHiddenChange(true) previewListener.onContentHiddenChange(true)
v.visibility = View.GONE v.visibility = View.GONE
sensitiveMediaShow.visibility = View.VISIBLE sensitiveMediaShow.visibility = View.VISIBLE
setMediasPreview(statusDisplayOptions, attachments, sensitive, previewListener,
true, mediaPreviewHeight)
} }
} }

View file

@ -50,7 +50,7 @@ defStyleAttr: Int = 0
/** /**
* Set the focal point for this view. * Set the focal point for this view.
*/ */
fun setFocalPoint(focus: Attachment.Focus) { fun setFocalPoint(focus: Attachment.Focus?) {
this.focus = focus this.focus = focus
super.setScaleType(ScaleType.MATRIX) super.setScaleType(ScaleType.MATRIX)

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="8dp" />
<solid android:color="?attr/sensitive_media_warning_background_color" />
</shape>

View file

@ -93,7 +93,7 @@
app:layout_constraintEnd_toStartOf="@id/barrierEnd" app:layout_constraintEnd_toStartOf="@id/barrierEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin" app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/buttonToggleContent" app:layout_constraintTop_toBottomOf="@id/buttonToggleContent"
tools:visibility="gone"> tools:visibility="visible">
<com.keylesspalace.tusky.view.MediaPreviewImageView <com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_0" android:id="@+id/status_media_preview_0"
@ -199,17 +199,21 @@
android:visibility="gone" android:visibility="gone"
app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container" app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_container" app:layout_constraintTop_toTopOf="@+id/status_media_preview_container"
app:srcCompat="@drawable/ic_eye_24dp" /> app:srcCompat="@drawable/ic_eye_24dp"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/status_sensitive_media_warning" android:id="@+id/status_sensitive_media_warning"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
android:background="?attr/sensitive_media_warning_background_color" android:background="@drawable/media_warning_bg"
android:gravity="center" android:gravity="center"
android:lineSpacingMultiplier="1.2" android:lineSpacingMultiplier="1.2"
android:orientation="vertical" android:orientation="vertical"
android:padding="8dp" android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp"
android:textAlignment="center" android:textAlignment="center"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"

View file

@ -188,7 +188,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/status_display_name" app:layout_constraintStart_toStartOf="@id/status_display_name"
app:layout_constraintTop_toBottomOf="@id/button_toggle_content" app:layout_constraintTop_toBottomOf="@id/button_toggle_content"
tools:visibility="gone"> tools:visibility="visible">
<com.keylesspalace.tusky.view.MediaPreviewImageView <com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_0" android:id="@+id/status_media_preview_0"
@ -298,14 +298,17 @@
<TextView <TextView
android:id="@+id/status_sensitive_media_warning" android:id="@+id/status_sensitive_media_warning"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
android:background="?attr/sensitive_media_warning_background_color" android:background="@drawable/media_warning_bg"
android:gravity="center" android:gravity="center"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:lineSpacingMultiplier="1.2" android:lineSpacingMultiplier="1.2"
android:orientation="vertical" android:orientation="vertical"
android:padding="8dp" android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp"
android:textAlignment="center" android:textAlignment="center"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
@ -313,7 +316,9 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
tools:text="@string/status_sensitive_media_title"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/status_media_label_0" android:id="@+id/status_media_label_0"

View file

@ -313,9 +313,9 @@
<TextView <TextView
android:id="@+id/status_sensitive_media_warning" android:id="@+id/status_sensitive_media_warning"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
android:background="?attr/sensitive_media_warning_background_color" android:background="@drawable/media_warning_bg"
android:gravity="center" android:gravity="center"
android:lineSpacingMultiplier="1.2" android:lineSpacingMultiplier="1.2"
android:orientation="vertical" android:orientation="vertical"
@ -327,7 +327,9 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
tools:text="@string/status_sensitive_media_title"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/status_media_label_0" android:id="@+id/status_media_label_0"

View file

@ -36,7 +36,7 @@
<item name="status_favourite_active_drawable">@drawable/favourite_active_dark</item> <item name="status_favourite_active_drawable">@drawable/favourite_active_dark</item>
<item name="status_favourite_inactive_drawable">@drawable/favourite_inactive_dark</item> <item name="status_favourite_inactive_drawable">@drawable/favourite_inactive_dark</item>
<item name="content_warning_button">@drawable/toggle_small</item> <item name="content_warning_button">@drawable/toggle_small</item>
<item name="sensitive_media_warning_background_color">@color/color_background_dark</item> <item name="sensitive_media_warning_background_color">#80000000</item>
<item name="media_preview_unloaded_drawable">@drawable/media_preview_unloaded_dark</item> <item name="media_preview_unloaded_drawable">@drawable/media_preview_unloaded_dark</item>
<item name="android:listDivider">@drawable/status_divider_dark</item> <item name="android:listDivider">@drawable/status_divider_dark</item>
<item name="conversation_thread_line_drawable">@drawable/conversation_thread_line_dark</item> <item name="conversation_thread_line_drawable">@drawable/conversation_thread_line_dark</item>

View file

@ -227,6 +227,7 @@
<string name="pref_title_language">Language</string> <string name="pref_title_language">Language</string>
<string name="pref_title_bot_overlay">Show indicator for bots</string> <string name="pref_title_bot_overlay">Show indicator for bots</string>
<string name="pref_title_animate_gif_avatars">Animate GIF avatars</string> <string name="pref_title_animate_gif_avatars">Animate GIF avatars</string>
<string name="gradient_for_media">Show colorful gradients for hidden media</string>
<string name="pref_title_status_filter">Timeline filtering</string> <string name="pref_title_status_filter">Timeline filtering</string>
<string name="pref_title_status_tabs">Tabs</string> <string name="pref_title_status_tabs">Tabs</string>

View file

@ -88,9 +88,7 @@
<item name="status_favourite_active_drawable">@drawable/favourite_active_light</item> <item name="status_favourite_active_drawable">@drawable/favourite_active_light</item>
<item name="status_favourite_inactive_drawable">@drawable/favourite_inactive_light</item> <item name="status_favourite_inactive_drawable">@drawable/favourite_inactive_light</item>
<item name="content_warning_button">@drawable/toggle_small_light</item> <item name="content_warning_button">@drawable/toggle_small_light</item>
<item name="sensitive_media_warning_background_color"> <item name="sensitive_media_warning_background_color">#80B0B0B0</item>
@color/sensitive_media_warning_background_light
</item>
<item name="media_preview_unloaded_drawable">@drawable/media_preview_unloaded_light</item> <item name="media_preview_unloaded_drawable">@drawable/media_preview_unloaded_light</item>
<item name="android:listDivider">@drawable/status_divider_light</item> <item name="android:listDivider">@drawable/status_divider_light</item>
<item name="conversation_thread_line_drawable">@drawable/conversation_thread_line_light <item name="conversation_thread_line_drawable">@drawable/conversation_thread_line_light
@ -189,7 +187,7 @@
<item name="recents_background_color">@color/toolbar_background_black</item> <item name="recents_background_color">@color/toolbar_background_black</item>
<item name="window_background">@color/window_background_black</item> <item name="window_background">@color/window_background_black</item>
<item name="sensitive_media_warning_background_color">@color/color_background_black</item> <item name="sensitive_media_warning_background_color">#80000000</item>
<item name="account_header_background_color">@color/color_background_black</item> <item name="account_header_background_color">@color/color_background_black</item>
<item name="report_status_background_color">@color/color_background_black</item> <item name="report_status_background_color">@color/color_background_black</item>

View file

@ -60,6 +60,12 @@
android:title="@string/pref_title_animate_gif_avatars" android:title="@string/pref_title_animate_gif_avatars"
app:singleLineTitle="false" /> app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="useBlurhash"
android:title="@string/gradient_for_media"
app:singleLineTitle="false" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="showNotificationsFilter" android:key="showNotificationsFilter"