From 326676a9c6cec3368eaab96fcc91fefe01fd4f6e Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 5 Jul 2024 10:13:37 +0200 Subject: [PATCH] split out FilteredStatusViewHolder from StatusBaseViewHolder (#4543) This is way more efficient than before as less views need to be inflated and bound for a filtered status to be rendered. It also should fix the bug where sometimes a `StatusViewHolder` that is set up for showing a status gets bound to a status that is filtered, leading to a crash. --- .../tusky/adapter/FilteredStatusViewHolder.kt | 57 +++++++++++++++++++ .../tusky/adapter/StatusBaseViewHolder.java | 48 ---------------- .../NotificationsPagingAdapter.kt | 14 ++--- .../notifications/NotificationsViewModel.kt | 2 +- .../timeline/TimelinePagingAdapter.kt | 34 +++++++---- .../components/viewthread/ThreadAdapter.kt | 30 ++++++---- .../main/res/layout/item_status_filtered.xml | 5 +- .../main/res/layout/item_status_wrapper.xml | 13 ----- 8 files changed, 107 insertions(+), 96 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/FilteredStatusViewHolder.kt delete mode 100644 app/src/main/res/layout/item_status_wrapper.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FilteredStatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FilteredStatusViewHolder.kt new file mode 100644 index 000000000..afc445ce2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FilteredStatusViewHolder.kt @@ -0,0 +1,57 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ +package com.keylesspalace.tusky.adapter + +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.notifications.NotificationsViewHolder +import com.keylesspalace.tusky.databinding.ItemStatusFilteredBinding +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.FilterResult +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.NotificationViewData +import com.keylesspalace.tusky.viewdata.StatusViewData + +class FilteredStatusViewHolder( + private val binding: ItemStatusFilteredBinding, + listener: StatusActionListener +) : NotificationsViewHolder, RecyclerView.ViewHolder(binding.root) { + + init { + binding.statusFilterShowAnyway.setOnClickListener { + listener.clearWarningAction(bindingAdapterPosition) + } + } + + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>, + statusDisplayOptions: StatusDisplayOptions + ) = bind(viewData.statusViewData!!) + + fun bind(viewData: StatusViewData.Concrete) { + val matchedFilterResult: FilterResult? = viewData.actionable.filtered.orEmpty().find { filterResult -> + filterResult.filter.action == Filter.Action.WARN + } + + val matchedFilterTitle = matchedFilterResult?.filter?.title.orEmpty() + + binding.statusFilterLabel.text = itemView.context.getString( + R.string.status_filter_placeholder_label_format, + matchedFilterTitle + ) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index 0cf9cb150..84fd53502 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -45,8 +45,6 @@ import com.keylesspalace.tusky.entity.Attachment.Focus; import com.keylesspalace.tusky.entity.Attachment.MetaData; import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Emoji; -import com.keylesspalace.tusky.entity.Filter; -import com.keylesspalace.tusky.entity.FilterResult; import com.keylesspalace.tusky.entity.HashTag; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Translation; @@ -120,9 +118,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private final TextView cardDescription; private final TextView cardUrl; private final PollAdapter pollAdapter; - protected final LinearLayout filteredPlaceholder; - protected final TextView filteredPlaceholderLabel; - protected final Button filteredPlaceholderShowButton; protected final ConstraintLayout statusContainer; private final TextView translationStatusView; private final Button untranslateButton; @@ -179,9 +174,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { cardDescription = itemView.findViewById(R.id.card_description); cardUrl = itemView.findViewById(R.id.card_link); - filteredPlaceholder = itemView.findViewById(R.id.status_filtered_placeholder); - filteredPlaceholderLabel = itemView.findViewById(R.id.status_filter_label); - filteredPlaceholderShowButton = itemView.findViewById(R.id.status_filter_show_anyway); statusContainer = itemView.findViewById(R.id.status_container); pollAdapter = new PollAdapter(); @@ -822,8 +814,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setSpoilerAndContent(status, statusDisplayOptions, listener); - setupFilterPlaceholder(status, listener, statusDisplayOptions); - setDescriptionForStatus(status, statusDisplayOptions); // Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0 @@ -866,35 +856,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } - private void setupFilterPlaceholder(StatusViewData.Concrete status, StatusActionListener listener, StatusDisplayOptions displayOptions) { - if (status.getFilterAction() != Filter.Action.WARN) { - showFilteredPlaceholder(false); - return; - } - - showFilteredPlaceholder(true); - - Filter matchedFilter = null; - - for (FilterResult result : status.getActionable().getFiltered()) { - Filter filter = result.getFilter(); - if (filter.getAction() == Filter.Action.WARN) { - matchedFilter = filter; - break; - } - } - - final String matchedFilterTitle; - if (matchedFilter == null) { - matchedFilterTitle = ""; - } else { - matchedFilterTitle = matchedFilter.getTitle(); - } - - filteredPlaceholderLabel.setText(itemView.getContext().getString(R.string.status_filter_placeholder_label_format, matchedFilterTitle)); - filteredPlaceholderShowButton.setOnClickListener(view -> listener.clearWarningAction(getBindingAdapterPosition())); - } - protected static boolean hasPreviewableAttachment(@NonNull List attachments) { for (Attachment attachment : attachments) { if (attachment.getType() == Attachment.Type.AUDIO || attachment.getType() == Attachment.Type.UNKNOWN) { @@ -1306,13 +1267,4 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { bookmarkButton.setVisibility(visibility); moreButton.setVisibility(visibility); } - - public void showFilteredPlaceholder(boolean show) { - if (statusContainer != null) { - statusContainer.setVisibility(show ? View.GONE : View.VISIBLE); - } - if (filteredPlaceholder != null) { - filteredPlaceholder.setVisibility(show ? View.VISIBLE : View.GONE); - } - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt index 309f17b72..26af3b8d8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt @@ -20,16 +20,17 @@ import android.view.ViewGroup import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.FilteredStatusViewHolder import com.keylesspalace.tusky.adapter.FollowRequestViewHolder import com.keylesspalace.tusky.adapter.PlaceholderViewHolder import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.databinding.ItemFollowBinding import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding -import com.keylesspalace.tusky.databinding.ItemStatusBinding +import com.keylesspalace.tusky.databinding.ItemStatusFilteredBinding import com.keylesspalace.tusky.databinding.ItemStatusNotificationBinding import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding -import com.keylesspalace.tusky.databinding.ItemStatusWrapperBinding import com.keylesspalace.tusky.databinding.ItemUnknownNotificationBinding import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Notification @@ -103,14 +104,13 @@ class NotificationsPagingAdapter( val inflater = LayoutInflater.from(parent.context) return when (viewType) { VIEW_TYPE_STATUS -> StatusViewHolder( - ItemStatusBinding.inflate(inflater, parent, false).root, + inflater.inflate(R.layout.item_status, parent, false), statusListener, accountId ) - VIEW_TYPE_STATUS_FILTERED -> StatusViewHolder( - ItemStatusWrapperBinding.inflate(inflater, parent, false).root, - statusListener, - accountId + VIEW_TYPE_STATUS_FILTERED -> FilteredStatusViewHolder( + ItemStatusFilteredBinding.inflate(inflater, parent, false), + statusListener ) VIEW_TYPE_STATUS_NOTIFICATION -> StatusNotificationViewHolder( ItemStatusNotificationBinding.inflate(inflater, parent, false), diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt index d4f9b3f6f..3af79a446 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt @@ -158,7 +158,7 @@ class NotificationsViewModel @Inject constructor( private fun shouldFilterStatus(notificationViewData: NotificationViewData): Filter.Action { return when ((notificationViewData as? NotificationViewData.Concrete)?.type) { - Notification.Type.MENTION, Notification.Type.STATUS, Notification.Type.POLL -> { + Notification.Type.MENTION, Notification.Type.POLL -> { notificationViewData.statusViewData?.let { statusViewData -> statusViewData.filterAction = filterModel.shouldFilterStatus(statusViewData.actionable) return statusViewData.filterAction diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt index bc74d868d..511c70d11 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt @@ -21,9 +21,11 @@ import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.FilteredStatusViewHolder import com.keylesspalace.tusky.adapter.PlaceholderViewHolder import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.databinding.ItemStatusFilteredBinding import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.interfaces.StatusActionListener @@ -51,7 +53,10 @@ class TimelinePagingAdapter( val inflater = LayoutInflater.from(viewGroup.context) return when (viewType) { VIEW_TYPE_STATUS_FILTERED -> { - StatusViewHolder(inflater.inflate(R.layout.item_status_wrapper, viewGroup, false)) + FilteredStatusViewHolder( + ItemStatusFilteredBinding.inflate(inflater, viewGroup, false), + statusListener + ) } VIEW_TYPE_PLACEHOLDER -> { PlaceholderViewHolder( @@ -82,18 +87,23 @@ class TimelinePagingAdapter( position: Int, payloads: List<*>? ) { - val status = getItem(position) - if (status is StatusViewData.Placeholder) { + val viewData = getItem(position) + if (viewData is StatusViewData.Placeholder) { val holder = viewHolder as PlaceholderViewHolder - holder.setup(status.isLoading) - } else if (status is StatusViewData.Concrete) { - val holder = viewHolder as StatusViewHolder - holder.setupWithStatus( - status, - statusListener, - statusDisplayOptions, - if (payloads != null && payloads.isNotEmpty()) payloads[0] else null - ) + holder.setup(viewData.isLoading) + } else if (viewData is StatusViewData.Concrete) { + if (viewData.filterAction == Filter.Action.WARN) { + val holder = viewHolder as FilteredStatusViewHolder + holder.bind(viewData) + } else { + val holder = viewHolder as StatusViewHolder + holder.setupWithStatus( + viewData, + statusListener, + statusDisplayOptions, + if (payloads != null && payloads.isNotEmpty()) payloads[0] else null + ) + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt index 0c1c7fc5b..823fb7cd4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt @@ -19,10 +19,13 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.FilteredStatusViewHolder import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.adapter.StatusDetailedViewHolder import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.databinding.ItemStatusFilteredBinding import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.util.StatusDisplayOptions @@ -31,29 +34,33 @@ import com.keylesspalace.tusky.viewdata.StatusViewData class ThreadAdapter( private val statusDisplayOptions: StatusDisplayOptions, private val statusActionListener: StatusActionListener -) : ListAdapter(ThreadDifferCallback) { +) : ListAdapter(ThreadDifferCallback) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusBaseViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { - VIEW_TYPE_STATUS -> { + VIEW_TYPE_STATUS -> StatusViewHolder(inflater.inflate(R.layout.item_status, parent, false)) - } - VIEW_TYPE_STATUS_FILTERED -> { - StatusViewHolder(inflater.inflate(R.layout.item_status_wrapper, parent, false)) - } - VIEW_TYPE_STATUS_DETAILED -> { + VIEW_TYPE_STATUS_FILTERED -> + FilteredStatusViewHolder( + ItemStatusFilteredBinding.inflate(inflater, parent, false), + statusActionListener + ) + VIEW_TYPE_STATUS_DETAILED -> StatusDetailedViewHolder( inflater.inflate(R.layout.item_status_detailed, parent, false) ) - } else -> error("Unknown item type: $viewType") } } - override fun onBindViewHolder(viewHolder: StatusBaseViewHolder, position: Int) { + override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { val status = getItem(position) - viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions) + if (viewHolder is FilteredStatusViewHolder) { + viewHolder.bind(status) + } else if (viewHolder is StatusBaseViewHolder) { + viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions) + } } override fun getItemViewType(position: Int): Int { @@ -68,7 +75,6 @@ class ThreadAdapter( } companion object { - private const val TAG = "ThreadAdapter" private const val VIEW_TYPE_STATUS = 0 private const val VIEW_TYPE_STATUS_DETAILED = 1 private const val VIEW_TYPE_STATUS_FILTERED = 2 diff --git a/app/src/main/res/layout/item_status_filtered.xml b/app/src/main/res/layout/item_status_filtered.xml index a091546c2..dfa687ce4 100644 --- a/app/src/main/res/layout/item_status_filtered.xml +++ b/app/src/main/res/layout/item_status_filtered.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/status_filtered_placeholder" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="wrap_content" android:orientation="vertical"> + tools:text="Filter: MyFilter" />