diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MutedStatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/MutedStatusViewHolder.java new file mode 100644 index 00000000..94e3efd0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutedStatusViewHolder.java @@ -0,0 +1,203 @@ +package com.keylesspalace.tusky.adapter; + +import android.content.Context; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.view.View; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.google.android.material.button.MaterialButton; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Attachment; +import com.keylesspalace.tusky.entity.Attachment.Focus; +import com.keylesspalace.tusky.entity.Attachment.MetaData; +import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CustomEmojiHelper; +import com.keylesspalace.tusky.util.HtmlUtils; +import com.keylesspalace.tusky.util.ImageLoadingHelper; +import com.keylesspalace.tusky.util.LinkHelper; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.util.ThemeUtils; +import com.keylesspalace.tusky.util.TimestampUtils; +import com.keylesspalace.tusky.view.MediaPreviewImageView; +import com.keylesspalace.tusky.viewdata.PollOptionViewData; +import com.keylesspalace.tusky.viewdata.PollViewData; +import com.keylesspalace.tusky.viewdata.PollViewDataKt; +import com.keylesspalace.tusky.viewdata.StatusViewData; +import com.mikepenz.iconics.utils.Utils; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +import at.connyduck.sparkbutton.SparkButton; +import kotlin.collections.CollectionsKt; + +import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; + +public class MutedStatusViewHolder extends RecyclerView.ViewHolder { + public static class Key { + public static final String KEY_CREATED = "created"; + } + + private TextView displayName; + private TextView username; + private TextView message; + private ImageButton unmuteButton; + public TextView timestampInfo; + + private SimpleDateFormat shortSdf; + private SimpleDateFormat longSdf; + + protected MutedStatusViewHolder(View itemView) { + super(itemView); + displayName = itemView.findViewById(R.id.status_display_name); + username = itemView.findViewById(R.id.status_username); + timestampInfo = itemView.findViewById(R.id.status_timestamp_info); + unmuteButton = itemView.findViewById(R.id.status_unmute); + + this.shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); + this.longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); + } + + protected void setDisplayName(String name, List customEmojis) { + CharSequence emojifiedName = CustomEmojiHelper.emojifyString(name, customEmojis, displayName); + displayName.setText(emojifiedName); + } + + protected void setUsername(String name) { + Context context = username.getContext(); + String usernameText = context.getString(R.string.status_username_format, name); + username.setText(usernameText); + } + + protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) { + if (statusDisplayOptions.useAbsoluteTime()) { + timestampInfo.setText(getAbsoluteTime(createdAt)); + } else { + if (createdAt == null) { + timestampInfo.setText("?m"); + } else { + long then = createdAt.getTime(); + long now = System.currentTimeMillis(); + String readout = TimestampUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now); + timestampInfo.setText(readout); + } + } + } + + private String getAbsoluteTime(Date createdAt) { + if (createdAt == null) { + return "??:??:??"; + } + if (DateUtils.isToday(createdAt.getTime())) { + return shortSdf.format(createdAt); + } else { + return longSdf.format(createdAt); + } + } + + private CharSequence getCreatedAtDescription(Date createdAt, + StatusDisplayOptions statusDisplayOptions) { + if (statusDisplayOptions.useAbsoluteTime()) { + return getAbsoluteTime(createdAt); + } else { + /* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m" + * as 17 meters instead of minutes. */ + + if (createdAt == null) { + return "? minutes"; + } else { + long then = createdAt.getTime(); + long now = System.currentTimeMillis(); + return DateUtils.getRelativeTimeSpanString(then, now, + DateUtils.SECOND_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE); + } + } + } + + private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status, + StatusDisplayOptions statusDisplayOptions) { + Context context = itemView.getContext(); + + String description = context.getString(R.string.description_muted_status, + status.getUserFullName(), + getCreatedAtDescription(status.getCreatedAt(), statusDisplayOptions), + status.getNickname() + ); + itemView.setContentDescription(description); + } + + + protected void setupButtons(final StatusActionListener listener, final String accountId) { + + unmuteButton.setOnClickListener(v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onMute(position, false); + } + }); + + itemView.setOnClickListener( v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onViewThread(position); + } + }); + } + + public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions) { + this.setupWithStatus(status, listener, statusDisplayOptions, null); + } + + protected void setupWithStatus(StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { + if (payloads == null) { + setDisplayName(status.getUserFullName(), status.getAccountEmojis()); + setUsername(status.getNickname()); + setCreatedAt(status.getCreatedAt(), statusDisplayOptions); + + setupButtons(listener, status.getSenderId()); + setDescriptionForStatus(status, statusDisplayOptions); + + // Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0 + // RecyclerView tries to set AccessibilityDelegateCompat to null + // but ViewCompat code replaces is with the default one. RecyclerView never + // fetches another one from its delegate because it checks that it's set so we remove it + // and let RecyclerView ask for a new delegate. + itemView.setAccessibilityDelegate(null); + } else { + if (payloads instanceof List) + for (Object item : (List) payloads) { + if (Key.KEY_CREATED.equals(item)) { + setCreatedAt(status.getCreatedAt(), statusDisplayOptions); + } + } + + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 6a5bc073..dd9503b1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -72,7 +72,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_STATUS_NOTIFICATION = 1; private static final int VIEW_TYPE_FOLLOW = 2; private static final int VIEW_TYPE_PLACEHOLDER = 3; - private static final int VIEW_TYPE_UNKNOWN = 4; + private static final int VIEW_TYPE_MUTED_STATUS = 4; + private static final int VIEW_TYPE_UNKNOWN = 6; private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE}; private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; @@ -108,6 +109,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter { .inflate(R.layout.item_status, parent, false); return new StatusViewHolder(view); } + case VIEW_TYPE_MUTED_STATUS: { + View view = inflater + .inflate(R.layout.item_status_muted, parent, false); + return new MutedStatusViewHolder(view); + } case VIEW_TYPE_STATUS_NOTIFICATION: { View view = inflater .inflate(R.layout.item_status_notification, parent, false); @@ -175,6 +181,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } break; } + case VIEW_TYPE_MUTED_STATUS: { + MutedStatusViewHolder holder = (MutedStatusViewHolder) viewHolder; + StatusViewData.Concrete status = concreteNotificaton.getStatusViewData(); + holder.setupWithStatus(status, + statusListener, statusDisplayOptions, payloadForHolder); + break; + } case VIEW_TYPE_STATUS_NOTIFICATION: { StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder; StatusViewData.Concrete statusViewData = concreteNotificaton.getStatusViewData(); @@ -246,6 +259,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { switch (concrete.getType()) { case MENTION: case POLL: { + if(concrete.getStatusViewData() != null && concrete.getStatusViewData().isMuted()) + return VIEW_TYPE_MUTED_STATUS; return VIEW_TYPE_STATUS; } case FAVOURITE: @@ -329,7 +344,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { avatar.setOnClickListener(v -> listener.onViewAccount(accountId)); } } - + + private static class StatusNotificationViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { private final TextView message; @@ -344,14 +360,14 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private final Button contentWarningButton; private final Button contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder private StatusDisplayOptions statusDisplayOptions; - + private String accountId; private String notificationId; private NotificationActionListener notificationActionListener; private StatusViewData.Concrete statusViewData; private SimpleDateFormat shortSdf; private SimpleDateFormat longSdf; - + StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) { super(itemView); message = itemView.findViewById(R.id.notification_top_text); @@ -511,7 +527,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { .getDimensionPixelSize(R.dimen.avatar_radius_24dp); ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar, - notificationAvatarRadius, statusDisplayOptions.animateAvatars()); + notificationAvatarRadius, statusDisplayOptions.animateAvatars()); } @Override @@ -530,7 +546,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } private void setupContentAndSpoiler(NotificationViewData.Concrete notificationViewData, final LinkListener listener) { - + boolean shouldShowContentIfSpoiler = notificationViewData.isExpanded(); boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText()); if (!shouldShowContentIfSpoiler && hasSpoiler) { @@ -538,10 +554,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } else { statusContent.setVisibility(View.VISIBLE); } - + Spanned content = statusViewData.getContent(); List emojis = statusViewData.getStatusEmojis(); - + if (statusViewData.isCollapsible() && (notificationViewData.isExpanded() || !hasSpoiler)) { contentCollapseButton.setOnClickListener(view -> { int position = getAdapterPosition(); @@ -549,7 +565,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { notificationActionListener.onNotificationContentCollapsedChange(statusViewData.isCollapsed(), position); } }); - + contentCollapseButton.setVisibility(View.VISIBLE); if (statusViewData.isCollapsed()) { contentCollapseButton.setText(R.string.status_content_warning_show_more); @@ -570,6 +586,5 @@ public class NotificationsAdapter extends RecyclerView.Adapter { CustomEmojiHelper.emojifyString(statusViewData.getSpoilerText(), statusViewData.getStatusEmojis(), contentWarningDescriptionTextView); contentWarningDescriptionTextView.setText(emojifiedContentWarning); } - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 40692c6b..52e2ee2b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -593,6 +593,21 @@ public class NotificationsFragment extends SFragment implements notifications.setPairedItem(position, notificationViewData); updateAdapter(); } + + @Override + public void onMute(int position, boolean isMuted) { + NotificationViewData.Concrete old = + (NotificationViewData.Concrete) notifications.getPairedItem(position); + StatusViewData.Concrete statusViewData = + new StatusViewData.Builder(old.getStatusViewData()) + .setMuted(isMuted) + .createStatusViewData(); + Log.d("ASDASDASD", "position = " + position + " isMuted = " + isMuted); + NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(), + old.getId(), old.getAccount(), statusViewData, old.isExpanded()); + notifications.setPairedItem(position, notificationViewData); + updateAdapter(); + } @Override public void onLoadMore(int position) { diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java index ec37680c..c04b03dc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -62,5 +62,7 @@ public interface StatusActionListener extends LinkListener { default void onShowFavs(int position) {} void onVoteInPoll(int position, @NonNull List choices); + + default void onMute(int position, boolean isMuted) {} } diff --git a/app/src/main/res/layout/item_status_muted.xml b/app/src/main/res/layout/item_status_muted.xml new file mode 100644 index 00000000..231372a1 --- /dev/null +++ b/app/src/main/res/layout/item_status_muted.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + +