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.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<Attachment> 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);
}
}
}

View File

@ -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),

View File

@ -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

View File

@ -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
)
}
}
}

View File

@ -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<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)
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

View File

@ -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">
<TextView
@ -12,11 +12,10 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="0dp"
android:text="Filter: MyFilter"
android:textAlignment="center"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
tools:ignore="HardcodedText" />
tools:text="Filter: MyFilter" />
<Button
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>