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.
This commit is contained in:
Konrad Pozniak 2024-07-05 10:13:37 +02:00 committed by GitHub
parent 8a57bcc3f4
commit 326676a9c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 107 additions and 96 deletions

View File

@ -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 <http://www.gnu.org/licenses>. */
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
)
}
}

View File

@ -45,8 +45,6 @@ import com.keylesspalace.tusky.entity.Attachment.Focus;
import com.keylesspalace.tusky.entity.Attachment.MetaData; import com.keylesspalace.tusky.entity.Attachment.MetaData;
import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Emoji; 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.HashTag;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.Translation; import com.keylesspalace.tusky.entity.Translation;
@ -120,9 +118,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private final TextView cardDescription; private final TextView cardDescription;
private final TextView cardUrl; private final TextView cardUrl;
private final PollAdapter pollAdapter; private final PollAdapter pollAdapter;
protected final LinearLayout filteredPlaceholder;
protected final TextView filteredPlaceholderLabel;
protected final Button filteredPlaceholderShowButton;
protected final ConstraintLayout statusContainer; protected final ConstraintLayout statusContainer;
private final TextView translationStatusView; private final TextView translationStatusView;
private final Button untranslateButton; private final Button untranslateButton;
@ -179,9 +174,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
cardDescription = itemView.findViewById(R.id.card_description); cardDescription = itemView.findViewById(R.id.card_description);
cardUrl = itemView.findViewById(R.id.card_link); 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); statusContainer = itemView.findViewById(R.id.status_container);
pollAdapter = new PollAdapter(); pollAdapter = new PollAdapter();
@ -822,8 +814,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setSpoilerAndContent(status, statusDisplayOptions, listener); setSpoilerAndContent(status, statusDisplayOptions, listener);
setupFilterPlaceholder(status, listener, statusDisplayOptions);
setDescriptionForStatus(status, statusDisplayOptions); setDescriptionForStatus(status, statusDisplayOptions);
// Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0 // 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<Attachment> attachments) { protected static boolean hasPreviewableAttachment(@NonNull List<Attachment> attachments) {
for (Attachment attachment : attachments) { for (Attachment attachment : attachments) {
if (attachment.getType() == Attachment.Type.AUDIO || attachment.getType() == Attachment.Type.UNKNOWN) { 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); bookmarkButton.setVisibility(visibility);
moreButton.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);
}
}
} }

View File

@ -20,16 +20,17 @@ import android.view.ViewGroup
import androidx.paging.PagingDataAdapter import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView 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.FollowRequestViewHolder
import com.keylesspalace.tusky.adapter.PlaceholderViewHolder import com.keylesspalace.tusky.adapter.PlaceholderViewHolder
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.databinding.ItemFollowBinding import com.keylesspalace.tusky.databinding.ItemFollowBinding
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding 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.ItemStatusNotificationBinding
import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding
import com.keylesspalace.tusky.databinding.ItemStatusWrapperBinding
import com.keylesspalace.tusky.databinding.ItemUnknownNotificationBinding import com.keylesspalace.tusky.databinding.ItemUnknownNotificationBinding
import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.Notification import com.keylesspalace.tusky.entity.Notification
@ -103,14 +104,13 @@ class NotificationsPagingAdapter(
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
VIEW_TYPE_STATUS -> StatusViewHolder( VIEW_TYPE_STATUS -> StatusViewHolder(
ItemStatusBinding.inflate(inflater, parent, false).root, inflater.inflate(R.layout.item_status, parent, false),
statusListener, statusListener,
accountId accountId
) )
VIEW_TYPE_STATUS_FILTERED -> StatusViewHolder( VIEW_TYPE_STATUS_FILTERED -> FilteredStatusViewHolder(
ItemStatusWrapperBinding.inflate(inflater, parent, false).root, ItemStatusFilteredBinding.inflate(inflater, parent, false),
statusListener, statusListener
accountId
) )
VIEW_TYPE_STATUS_NOTIFICATION -> StatusNotificationViewHolder( VIEW_TYPE_STATUS_NOTIFICATION -> StatusNotificationViewHolder(
ItemStatusNotificationBinding.inflate(inflater, parent, false), ItemStatusNotificationBinding.inflate(inflater, parent, false),

View File

@ -158,7 +158,7 @@ class NotificationsViewModel @Inject constructor(
private fun shouldFilterStatus(notificationViewData: NotificationViewData): Filter.Action { private fun shouldFilterStatus(notificationViewData: NotificationViewData): Filter.Action {
return when ((notificationViewData as? NotificationViewData.Concrete)?.type) { 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 -> notificationViewData.statusViewData?.let { statusViewData ->
statusViewData.filterAction = filterModel.shouldFilterStatus(statusViewData.actionable) statusViewData.filterAction = filterModel.shouldFilterStatus(statusViewData.actionable)
return statusViewData.filterAction return statusViewData.filterAction

View File

@ -21,9 +21,11 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.FilteredStatusViewHolder
import com.keylesspalace.tusky.adapter.PlaceholderViewHolder import com.keylesspalace.tusky.adapter.PlaceholderViewHolder
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.adapter.StatusViewHolder import com.keylesspalace.tusky.adapter.StatusViewHolder
import com.keylesspalace.tusky.databinding.ItemStatusFilteredBinding
import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding
import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
@ -51,7 +53,10 @@ class TimelinePagingAdapter(
val inflater = LayoutInflater.from(viewGroup.context) val inflater = LayoutInflater.from(viewGroup.context)
return when (viewType) { return when (viewType) {
VIEW_TYPE_STATUS_FILTERED -> { VIEW_TYPE_STATUS_FILTERED -> {
StatusViewHolder(inflater.inflate(R.layout.item_status_wrapper, viewGroup, false)) FilteredStatusViewHolder(
ItemStatusFilteredBinding.inflate(inflater, viewGroup, false),
statusListener
)
} }
VIEW_TYPE_PLACEHOLDER -> { VIEW_TYPE_PLACEHOLDER -> {
PlaceholderViewHolder( PlaceholderViewHolder(
@ -82,18 +87,23 @@ class TimelinePagingAdapter(
position: Int, position: Int,
payloads: List<*>? payloads: List<*>?
) { ) {
val status = getItem(position) val viewData = getItem(position)
if (status is StatusViewData.Placeholder) { if (viewData is StatusViewData.Placeholder) {
val holder = viewHolder as PlaceholderViewHolder val holder = viewHolder as PlaceholderViewHolder
holder.setup(status.isLoading) holder.setup(viewData.isLoading)
} else if (status is StatusViewData.Concrete) { } else if (viewData is StatusViewData.Concrete) {
val holder = viewHolder as StatusViewHolder if (viewData.filterAction == Filter.Action.WARN) {
holder.setupWithStatus( val holder = viewHolder as FilteredStatusViewHolder
status, holder.bind(viewData)
statusListener, } else {
statusDisplayOptions, val holder = viewHolder as StatusViewHolder
if (payloads != null && payloads.isNotEmpty()) payloads[0] else null holder.setupWithStatus(
) viewData,
statusListener,
statusDisplayOptions,
if (payloads != null && payloads.isNotEmpty()) payloads[0] else null
)
}
} }
} }

View File

@ -19,10 +19,13 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.FilteredStatusViewHolder
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.adapter.StatusDetailedViewHolder import com.keylesspalace.tusky.adapter.StatusDetailedViewHolder
import com.keylesspalace.tusky.adapter.StatusViewHolder import com.keylesspalace.tusky.adapter.StatusViewHolder
import com.keylesspalace.tusky.databinding.ItemStatusFilteredBinding
import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
@ -31,29 +34,33 @@ import com.keylesspalace.tusky.viewdata.StatusViewData
class ThreadAdapter( class ThreadAdapter(
private val statusDisplayOptions: StatusDisplayOptions, private val statusDisplayOptions: StatusDisplayOptions,
private val statusActionListener: StatusActionListener private val statusActionListener: StatusActionListener
) : ListAdapter<StatusViewData.Concrete, StatusBaseViewHolder>(ThreadDifferCallback) { ) : ListAdapter<StatusViewData.Concrete, RecyclerView.ViewHolder>(ThreadDifferCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusBaseViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
VIEW_TYPE_STATUS -> { VIEW_TYPE_STATUS ->
StatusViewHolder(inflater.inflate(R.layout.item_status, parent, false)) StatusViewHolder(inflater.inflate(R.layout.item_status, parent, false))
} VIEW_TYPE_STATUS_FILTERED ->
VIEW_TYPE_STATUS_FILTERED -> { FilteredStatusViewHolder(
StatusViewHolder(inflater.inflate(R.layout.item_status_wrapper, parent, false)) ItemStatusFilteredBinding.inflate(inflater, parent, false),
} statusActionListener
VIEW_TYPE_STATUS_DETAILED -> { )
VIEW_TYPE_STATUS_DETAILED ->
StatusDetailedViewHolder( StatusDetailedViewHolder(
inflater.inflate(R.layout.item_status_detailed, parent, false) inflater.inflate(R.layout.item_status_detailed, parent, false)
) )
}
else -> error("Unknown item type: $viewType") 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) 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 { override fun getItemViewType(position: Int): Int {
@ -68,7 +75,6 @@ class ThreadAdapter(
} }
companion object { companion object {
private const val TAG = "ThreadAdapter"
private const val VIEW_TYPE_STATUS = 0 private const val VIEW_TYPE_STATUS = 0
private const val VIEW_TYPE_STATUS_DETAILED = 1 private const val VIEW_TYPE_STATUS_DETAILED = 1
private const val VIEW_TYPE_STATUS_FILTERED = 2 private const val VIEW_TYPE_STATUS_FILTERED = 2

View File

@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/status_filtered_placeholder" android:id="@+id/status_filtered_placeholder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
@ -12,11 +12,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="0dp" android:layout_marginBottom="0dp"
android:text="Filter: MyFilter"
android:textAlignment="center" android:textAlignment="center"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
tools:ignore="HardcodedText" /> tools:text="Filter: MyFilter" />
<Button <Button
android:id="@+id/status_filter_show_anyway" android:id="@+id/status_filter_show_anyway"

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/item_status" />
<include
layout="@layout/item_status_filtered"
android:visibility="gone"
/>
</FrameLayout>