* 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:
parent
2994af7091
commit
7623962a0d
32 changed files with 560 additions and 368 deletions
|
@ -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" -> {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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>? {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
}
|
|
@ -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))
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
5
app/src/main/res/drawable/media_warning_bg.xml
Normal file
5
app/src/main/res/drawable/media_warning_bg.xml
Normal 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>
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue