Browse Source

show placeholders for null items in database backed recyclerviews (#5074)

When using the database as `PagingSource`, it can happen that there are
null items, e.g. when the user jumps to the top and the items are no
longer in memory and need to be re-queried. We have ignored this fact
until now, leading to subtle bugs where the adapter just shows a
completely empty `ViewHolder`, or worse, a recycled ViewHolder that has
not been updated and shows the wrong post. Usually these are only
visible for a split second but it can take longer in some cases e.g. on
slow devices.

Here is how the placeholders look:

<img
src="https://github.com/user-attachments/assets/58d3434f-916f-44a5-ad82-2a4a759e39d8"
width="320"/>


Note: I would prefer to turn this behavior with the null items off, but
we tried that once and it led to even worse bugs:
https://github.com/tuskyapp/Tusky/pull/4471
pull/5086/head
Konrad Pozniak 11 months ago committed by GitHub
parent
commit
9563e559e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 46
      app/src/main/java/com/keylesspalace/tusky/adapter/LoadMoreViewHolder.kt
  2. 45
      app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt
  3. 42
      app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationPagingAdapter.kt
  4. 8
      app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt
  5. 6
      app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationTypeMappers.kt
  6. 40
      app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt
  7. 4
      app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsRemoteMediator.kt
  8. 8
      app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt
  9. 48
      app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt
  10. 6
      app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt
  11. 4
      app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt
  12. 8
      app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt
  13. 2
      app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt
  14. 12
      app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt
  15. 4
      app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.kt
  16. 6
      app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt
  17. 5
      app/src/main/res/drawable/text_placeholder.xml
  18. 1
      app/src/main/res/layout/item_conversation.xml
  19. 4
      app/src/main/res/layout/item_follow.xml
  20. 4
      app/src/main/res/layout/item_follow_request.xml
  21. 0
      app/src/main/res/layout/item_load_more.xml
  22. 140
      app/src/main/res/layout/item_placeholder.xml
  23. 4
      app/src/main/res/layout/item_report_notification.xml
  24. 4
      app/src/main/res/layout/item_status_notification.xml
  25. 4
      app/src/main/res/layout/item_unknown_notification.xml
  26. 2
      app/src/main/res/values-night/theme_colors.xml
  27. 1
      app/src/main/res/values/attrs.xml
  28. 2
      app/src/main/res/values/dimens.xml
  29. 3
      app/src/main/res/values/styles.xml
  30. 2
      app/src/main/res/values/theme_colors.xml
  31. 4
      app/src/test/java/com/keylesspalace/tusky/components/notifications/NotificationFaker.kt
  32. 8
      app/src/test/java/com/keylesspalace/tusky/components/notifications/NotificationsRemoteMediatorTest.kt
  33. 2
      app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt
  34. 8
      app/src/test/java/com/keylesspalace/tusky/db/dao/NotificationsDaoTest.kt

46
app/src/main/java/com/keylesspalace/tusky/adapter/LoadMoreViewHolder.kt

@ -0,0 +1,46 @@
/* Copyright 2021 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.databinding.ItemLoadMoreBinding
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible
/**
* Placeholder for missing parts in timelines.
*
* Displays a "Load more" button to load the gap, or a
* circular progress bar if the missing page is being loaded.
*/
class LoadMoreViewHolder(
private val binding: ItemLoadMoreBinding,
listener: StatusActionListener
) : RecyclerView.ViewHolder(binding.root) {
init {
binding.loadMoreButton.setOnClickListener {
binding.loadMoreButton.hide()
binding.loadMoreProgressBar.show()
listener.onLoadMore(bindingAdapterPosition)
}
}
fun setup(loading: Boolean) {
binding.loadMoreButton.visible(!loading)
binding.loadMoreProgressBar.visible(loading)
}
}

45
app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt

@ -1,4 +1,4 @@
/* Copyright 2021 Tusky Contributors
/* Copyright 2025 Tusky Contributors
*
* This file is a part of Tusky.
*
@ -12,35 +12,40 @@
*
* 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 android.view.ViewGroup
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePaddingRelative
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemPlaceholderBinding
import com.keylesspalace.tusky.util.visible
/**
* Placeholder for missing parts in timelines.
*
* Displays a "Load more" button to load the gap, or a
* circular progress bar if the missing page is being loaded.
*/
class PlaceholderViewHolder(
private val binding: ItemStatusPlaceholderBinding,
listener: StatusActionListener
binding: ItemPlaceholderBinding,
mode: Mode,
) : RecyclerView.ViewHolder(binding.root) {
init {
binding.loadMoreButton.setOnClickListener {
binding.loadMoreButton.hide()
binding.loadMoreProgressBar.show()
listener.onLoadMore(bindingAdapterPosition)
val res = binding.root.context.resources
binding.topPlaceholder.visible(mode != Mode.STATUS)
binding.reblogButtonPlaceholder.visible(mode != Mode.CONVERSATION)
if (mode == Mode.NOTIFICATION) {
binding.topPlaceholder.updatePaddingRelative(
start = res.getDimensionPixelSize(R.dimen.status_info_padding_large)
)
}
if (mode == Mode.CONVERSATION) {
binding.moreButtonPlaceHolder.updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginEnd = res.getDimensionPixelSize(R.dimen.conversation_placeholder_more_button_inset)
}
}
}
fun setup(loading: Boolean) {
binding.loadMoreButton.visible(!loading)
binding.loadMoreProgressBar.visible(loading)
enum class Mode {
STATUS,
NOTIFICATION,
CONVERSATION
}
}

42
app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt → app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationPagingAdapter.kt

@ -19,15 +19,18 @@ import android.view.LayoutInflater
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.PlaceholderViewHolder
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.databinding.ItemPlaceholderBinding
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.StatusDisplayOptions
class ConversationAdapter(
class ConversationPagingAdapter(
private var statusDisplayOptions: StatusDisplayOptions,
private val listener: StatusActionListener
) : PagingDataAdapter<ConversationViewData, ConversationViewHolder>(CONVERSATION_COMPARATOR) {
) : PagingDataAdapter<ConversationViewData, RecyclerView.ViewHolder>(CONVERSATION_COMPARATOR) {
var mediaPreviewEnabled: Boolean
get() = statusDisplayOptions.mediaPreviewEnabled
@ -37,25 +40,42 @@ class ConversationAdapter(
)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
val view = LayoutInflater.from(
parent.context
).inflate(R.layout.item_conversation, parent, false)
return ConversationViewHolder(view, statusDisplayOptions, listener)
override fun getItemViewType(position: Int): Int {
return if (getItem(position) == null) {
VIEW_TYPE_PLACEHOLDER
} else {
VIEW_TYPE_CONVERSATION
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return if (viewType == VIEW_TYPE_CONVERSATION) {
ConversationViewHolder(layoutInflater.inflate(R.layout.item_conversation, parent, false), statusDisplayOptions, listener)
} else {
PlaceholderViewHolder(
ItemPlaceholderBinding.inflate(layoutInflater, parent, false),
mode = PlaceholderViewHolder.Mode.CONVERSATION
)
}
}
override fun onBindViewHolder(holder: ConversationViewHolder, position: Int) {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
onBindViewHolder(holder, position, emptyList())
}
override fun onBindViewHolder(holder: ConversationViewHolder, position: Int, payloads: List<Any>) {
getItem(position)?.let { conversationViewData ->
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any>) {
val conversationViewData = getItem(position)
if (holder is ConversationViewHolder && conversationViewData != null) {
holder.setupWithConversation(conversationViewData, payloads)
}
}
companion object {
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationViewData>() {
private const val VIEW_TYPE_PLACEHOLDER = 0
private const val VIEW_TYPE_CONVERSATION = 1
private val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationViewData>() {
override fun areItemsTheSame(
oldItem: ConversationViewData,
newItem: ConversationViewData

8
app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt

@ -77,7 +77,7 @@ class ConversationsFragment :
private val binding by viewBinding(FragmentTimelineBinding::bind)
private var adapter: ConversationAdapter? = null
private var adapter: ConversationPagingAdapter? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
@ -98,7 +98,7 @@ class ConversationsFragment :
openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
)
val adapter = ConversationAdapter(statusDisplayOptions, this)
val adapter = ConversationPagingAdapter(statusDisplayOptions, this)
this.adapter = adapter
setupRecyclerView(adapter)
@ -213,7 +213,7 @@ class ConversationsFragment :
}
}
private fun setupRecyclerView(adapter: ConversationAdapter) {
private fun setupRecyclerView(adapter: ConversationPagingAdapter) {
binding.recyclerView.ensureBottomPadding(fab = true)
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(context)
@ -372,7 +372,7 @@ class ConversationsFragment :
.show()
}
private fun onPreferenceChanged(adapter: ConversationAdapter, key: String) {
private fun onPreferenceChanged(adapter: ConversationPagingAdapter, key: String) {
when (key) {
PrefKeys.MEDIA_PREVIEW_ENABLED -> {
val enabled = accountManager.activeAccount!!.mediaPreviewEnabled

6
app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationTypeMappers.kt

@ -15,7 +15,7 @@
package com.keylesspalace.tusky.components.notifications
import com.keylesspalace.tusky.components.timeline.Placeholder
import com.keylesspalace.tusky.components.timeline.LoadMorePlaceholder
import com.keylesspalace.tusky.components.timeline.toAccount
import com.keylesspalace.tusky.components.timeline.toStatus
import com.keylesspalace.tusky.db.entity.NotificationDataEntity
@ -30,7 +30,7 @@ import com.keylesspalace.tusky.viewdata.NotificationViewData
import com.keylesspalace.tusky.viewdata.StatusViewData
import com.keylesspalace.tusky.viewdata.TranslationViewData
fun Placeholder.toNotificationEntity(
fun LoadMorePlaceholder.toNotificationEntity(
tuskyAccountId: Long
) = NotificationEntity(
id = this.id,
@ -93,7 +93,7 @@ fun NotificationDataEntity.toViewData(
translation: TranslationViewData? = null
): NotificationViewData {
if (type == null || account == null) {
return NotificationViewData.Placeholder(id = id, isLoading = loading)
return NotificationViewData.LoadMore(id = id, isLoading = loading)
}
return NotificationViewData.Concrete(

40
app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt

@ -23,16 +23,18 @@ 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.LoadMoreViewHolder
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.ItemLoadMoreBinding
import com.keylesspalace.tusky.databinding.ItemModerationWarningNotificationBinding
import com.keylesspalace.tusky.databinding.ItemPlaceholderBinding
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding
import com.keylesspalace.tusky.databinding.ItemSeveredRelationshipNotificationBinding
import com.keylesspalace.tusky.databinding.ItemStatusFilteredBinding
import com.keylesspalace.tusky.databinding.ItemStatusNotificationBinding
import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding
import com.keylesspalace.tusky.databinding.ItemUnknownNotificationBinding
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.Notification
@ -80,6 +82,7 @@ class NotificationsPagingAdapter(
override fun getItemViewType(position: Int): Int {
return when (val notification = getItem(position)) {
is NotificationViewData.LoadMore -> VIEW_TYPE_LOAD_MORE
is NotificationViewData.Concrete -> {
when (notification.type) {
Notification.Type.Mention,
@ -105,13 +108,17 @@ class NotificationsPagingAdapter(
else -> VIEW_TYPE_UNKNOWN
}
}
else -> VIEW_TYPE_PLACEHOLDER
null -> VIEW_TYPE_PLACEHOLDER
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
VIEW_TYPE_PLACEHOLDER -> PlaceholderViewHolder(
ItemPlaceholderBinding.inflate(inflater, parent, false),
mode = PlaceholderViewHolder.Mode.NOTIFICATION
)
VIEW_TYPE_STATUS -> StatusViewHolder(
inflater.inflate(R.layout.item_status, parent, false),
statusListener,
@ -137,8 +144,8 @@ class NotificationsPagingAdapter(
statusListener,
true
)
VIEW_TYPE_PLACEHOLDER -> PlaceholderViewHolder(
ItemStatusPlaceholderBinding.inflate(inflater, parent, false),
VIEW_TYPE_LOAD_MORE -> LoadMoreViewHolder(
ItemLoadMoreBinding.inflate(inflater, parent, false),
statusListener
)
VIEW_TYPE_REPORT -> ReportNotificationViewHolder(
@ -169,24 +176,25 @@ class NotificationsPagingAdapter(
when (notification) {
is NotificationViewData.Concrete ->
(viewHolder as NotificationsViewHolder).bind(notification, payloads, statusDisplayOptions)
is NotificationViewData.Placeholder -> {
(viewHolder as PlaceholderViewHolder).setup(notification.isLoading)
is NotificationViewData.LoadMore -> {
(viewHolder as LoadMoreViewHolder).setup(notification.isLoading)
}
}
}
}
companion object {
private const val VIEW_TYPE_STATUS = 0
private const val VIEW_TYPE_STATUS_FILTERED = 1
private const val VIEW_TYPE_STATUS_NOTIFICATION = 2
private const val VIEW_TYPE_FOLLOW = 3
private const val VIEW_TYPE_FOLLOW_REQUEST = 4
private const val VIEW_TYPE_PLACEHOLDER = 5
private const val VIEW_TYPE_REPORT = 6
private const val VIEW_TYPE_SEVERED_RELATIONSHIP = 7
private const val VIEW_TYPE_MODERATION_WARNING = 8
private const val VIEW_TYPE_UNKNOWN = 9
private const val VIEW_TYPE_PLACEHOLDER = 0
private const val VIEW_TYPE_STATUS = 1
private const val VIEW_TYPE_STATUS_FILTERED = 2
private const val VIEW_TYPE_STATUS_NOTIFICATION = 3
private const val VIEW_TYPE_FOLLOW = 4
private const val VIEW_TYPE_FOLLOW_REQUEST = 5
private const val VIEW_TYPE_LOAD_MORE = 6
private const val VIEW_TYPE_REPORT = 7
private const val VIEW_TYPE_SEVERED_RELATIONSHIP = 8
private const val VIEW_TYPE_MODERATION_WARNING = 9
private const val VIEW_TYPE_UNKNOWN = 10
val NotificationsDifferCallback = object : DiffUtil.ItemCallback<NotificationViewData>() {
override fun areItemsTheSame(

4
app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsRemoteMediator.kt

@ -22,7 +22,7 @@ import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.keylesspalace.tusky.components.systemnotifications.toTypes
import com.keylesspalace.tusky.components.timeline.Placeholder
import com.keylesspalace.tusky.components.timeline.LoadMorePlaceholder
import com.keylesspalace.tusky.components.timeline.toEntity
import com.keylesspalace.tusky.components.timeline.util.ifExpected
import com.keylesspalace.tusky.db.AccountManager
@ -119,7 +119,7 @@ class NotificationsRemoteMediator(
to guarantee the placeholder has an id that exists on the server as not all
servers handle client generated ids as expected */
notificationsDao.insertNotification(
Placeholder(notifications.last().id, loading = false).toNotificationEntity(activeAccount.id)
LoadMorePlaceholder(notifications.last().id, loading = false).toNotificationEntity(activeAccount.id)
)
}
}

8
app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt

@ -36,7 +36,7 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder
import com.keylesspalace.tusky.components.systemnotifications.NotificationChannelData
import com.keylesspalace.tusky.components.systemnotifications.toTypes
import com.keylesspalace.tusky.components.timeline.Placeholder
import com.keylesspalace.tusky.components.timeline.LoadMorePlaceholder
import com.keylesspalace.tusky.components.timeline.toEntity
import com.keylesspalace.tusky.components.timeline.util.ifExpected
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
@ -312,7 +312,7 @@ class NotificationsViewModel @Inject constructor(
val notificationsDao = db.notificationsDao()
notificationsDao.insertNotification(
Placeholder(placeholderId, loading = true).toNotificationEntity(
LoadMorePlaceholder(placeholderId, loading = true).toNotificationEntity(
accountId
)
)
@ -401,7 +401,7 @@ class NotificationsViewModel @Inject constructor(
ReadingOrder.NEWEST_FIRST -> notifications.last().id
}
notificationsDao.insertNotification(
Placeholder(
LoadMorePlaceholder(
idToConvert,
loading = false
).toNotificationEntity(accountId)
@ -421,7 +421,7 @@ class NotificationsViewModel @Inject constructor(
val activeAccount = accountManager.activeAccount!!
db.notificationsDao()
.insertNotification(
Placeholder(placeholderId, loading = false).toNotificationEntity(activeAccount.id)
LoadMorePlaceholder(placeholderId, loading = false).toNotificationEntity(activeAccount.id)
)
}

48
app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt

@ -22,11 +22,13 @@ 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.LoadMoreViewHolder
import com.keylesspalace.tusky.adapter.PlaceholderViewHolder
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.adapter.StatusViewHolder
import com.keylesspalace.tusky.databinding.ItemLoadMoreBinding
import com.keylesspalace.tusky.databinding.ItemPlaceholderBinding
import com.keylesspalace.tusky.databinding.ItemStatusFilteredBinding
import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.StatusDisplayOptions
@ -49,23 +51,29 @@ class TimelinePagingAdapter(
stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(viewGroup.context)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
VIEW_TYPE_PLACEHOLDER -> {
PlaceholderViewHolder(
ItemPlaceholderBinding.inflate(inflater, parent, false),
mode = PlaceholderViewHolder.Mode.STATUS
)
}
VIEW_TYPE_STATUS_FILTERED -> {
FilteredStatusViewHolder(
ItemStatusFilteredBinding.inflate(inflater, viewGroup, false),
ItemStatusFilteredBinding.inflate(inflater, parent, false),
statusListener
)
}
VIEW_TYPE_PLACEHOLDER -> {
PlaceholderViewHolder(
ItemStatusPlaceholderBinding.inflate(inflater, viewGroup, false),
VIEW_TYPE_LOAD_MORE -> {
LoadMoreViewHolder(
ItemLoadMoreBinding.inflate(inflater, parent, false),
statusListener
)
}
else -> {
StatusViewHolder(inflater.inflate(R.layout.item_status, viewGroup, false))
StatusViewHolder(inflater.inflate(R.layout.item_status, parent, false))
}
}
}
@ -80,8 +88,8 @@ class TimelinePagingAdapter(
payloads: List<Any>
) {
val viewData = getItem(position)
if (viewData is StatusViewData.Placeholder) {
val holder = viewHolder as PlaceholderViewHolder
if (viewData is StatusViewData.LoadMore) {
val holder = viewHolder as LoadMoreViewHolder
holder.setup(viewData.isLoading)
} else if (viewData is StatusViewData.Concrete) {
if (viewData.filter?.action == Filter.Action.WARN) {
@ -102,21 +110,21 @@ class TimelinePagingAdapter(
override fun getItemViewType(position: Int): Int {
val viewData = getItem(position)
return if (viewData is StatusViewData.Placeholder) {
VIEW_TYPE_PLACEHOLDER
} else if (viewData?.filter?.action == Filter.Action.WARN) {
VIEW_TYPE_STATUS_FILTERED
} else {
VIEW_TYPE_STATUS
return when {
viewData == null -> VIEW_TYPE_PLACEHOLDER
viewData is StatusViewData.LoadMore -> VIEW_TYPE_LOAD_MORE
viewData.filter?.action == Filter.Action.WARN -> VIEW_TYPE_STATUS_FILTERED
else -> VIEW_TYPE_STATUS
}
}
companion object {
private const val VIEW_TYPE_STATUS = 0
private const val VIEW_TYPE_STATUS_FILTERED = 1
private const val VIEW_TYPE_PLACEHOLDER = 2
private const val VIEW_TYPE_PLACEHOLDER = 0
private const val VIEW_TYPE_STATUS = 1
private const val VIEW_TYPE_STATUS_FILTERED = 2
private const val VIEW_TYPE_LOAD_MORE = 3
val TimelineDifferCallback = object : DiffUtil.ItemCallback<StatusViewData>() {
private val TimelineDifferCallback = object : DiffUtil.ItemCallback<StatusViewData>() {
override fun areItemsTheSame(
oldItem: StatusViewData,
newItem: StatusViewData

6
app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt

@ -26,7 +26,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData
import com.keylesspalace.tusky.viewdata.TranslationViewData
import java.util.Date
data class Placeholder(
data class LoadMorePlaceholder(
val id: String,
val loading: Boolean
)
@ -60,7 +60,7 @@ fun TimelineAccountEntity.toAccount(): TimelineAccount {
)
}
fun Placeholder.toEntity(tuskyAccountId: Long): HomeTimelineEntity {
fun LoadMorePlaceholder.toEntity(tuskyAccountId: Long): HomeTimelineEntity {
return HomeTimelineEntity(
id = this.id,
tuskyAccountId = tuskyAccountId,
@ -150,7 +150,7 @@ fun HomeTimelineData.toViewData(
filter: Filter? = null,
): StatusViewData {
if (this.account == null || this.status == null) {
return StatusViewData.Placeholder(this.id, loading)
return StatusViewData.LoadMore(this.id, loading)
}
val originalStatus = status.toStatus(account)

4
app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt

@ -21,7 +21,7 @@ import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.keylesspalace.tusky.components.timeline.Placeholder
import com.keylesspalace.tusky.components.timeline.LoadMorePlaceholder
import com.keylesspalace.tusky.components.timeline.toEntity
import com.keylesspalace.tusky.components.timeline.util.ifExpected
import com.keylesspalace.tusky.db.AppDatabase
@ -113,7 +113,7 @@ class CachedTimelineRemoteMediator(
to guarantee the placeholder has an id that exists on the server as not all
servers handle client generated ids as expected */
timelineDao.insertHomeTimelineItem(
Placeholder(statuses.last().id, loading = false).toEntity(activeAccount.id)
LoadMorePlaceholder(statuses.last().id, loading = false).toEntity(activeAccount.id)
)
}
}

8
app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt

@ -32,7 +32,7 @@ import at.connyduck.calladapter.networkresult.onFailure
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder.NEWEST_FIRST
import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder.OLDEST_FIRST
import com.keylesspalace.tusky.components.timeline.Placeholder
import com.keylesspalace.tusky.components.timeline.LoadMorePlaceholder
import com.keylesspalace.tusky.components.timeline.toEntity
import com.keylesspalace.tusky.components.timeline.toStatus
import com.keylesspalace.tusky.components.timeline.toViewData
@ -153,7 +153,7 @@ class CachedTimelineViewModel @Inject constructor(
val accountDao = db.timelineAccountDao()
timelineDao.insertHomeTimelineItem(
Placeholder(placeholderId, loading = true).toEntity(tuskyAccountId = accountId)
LoadMorePlaceholder(placeholderId, loading = true).toEntity(tuskyAccountId = accountId)
)
val (idAbovePlaceholder, idBelowPlaceholder) = db.withTransaction {
@ -240,7 +240,7 @@ class CachedTimelineViewModel @Inject constructor(
NEWEST_FIRST -> statuses.last().id
}
timelineDao.insertHomeTimelineItem(
Placeholder(
LoadMorePlaceholder(
idToConvert,
loading = false
).toEntity(accountId)
@ -259,7 +259,7 @@ class CachedTimelineViewModel @Inject constructor(
Log.w(TAG, "failed loading statuses", e)
val activeAccount = accountManager.activeAccount!!
db.timelineDao()
.insertHomeTimelineItem(Placeholder(placeholderId, loading = false).toEntity(activeAccount.id))
.insertHomeTimelineItem(LoadMorePlaceholder(placeholderId, loading = false).toEntity(activeAccount.id))
}
override fun fullReload() {

2
app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt

@ -103,7 +103,7 @@ class NetworkTimelineRemoteMediator(
viewModel.statusData.addAll(0, data)
if (insertPlaceholder) {
viewModel.statusData[statuses.size - 1] = StatusViewData.Placeholder(statuses.last().id, false)
viewModel.statusData[statuses.size - 1] = StatusViewData.LoadMore(statuses.last().id, false)
}
} else {
val linkHeader = statusResponse.headers()["Link"]

12
app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt

@ -200,9 +200,9 @@ class NetworkTimelineViewModel @Inject constructor(
viewModelScope.launch {
try {
val placeholderIndex =
statusData.indexOfFirst { it is StatusViewData.Placeholder && it.id == placeholderId }
statusData.indexOfFirst { it is StatusViewData.LoadMore && it.id == placeholderId }
statusData[placeholderIndex] =
StatusViewData.Placeholder(placeholderId, isLoading = true)
StatusViewData.LoadMore(placeholderId, isLoading = true)
val idAbovePlaceholder = statusData.getOrNull(placeholderIndex - 1)?.id
@ -258,7 +258,7 @@ class NetworkTimelineViewModel @Inject constructor(
statusData.removeAll { status ->
when (status) {
is StatusViewData.Placeholder -> lastId.isLessThan(status.id) && status.id.isLessThanOrEqual(
is StatusViewData.LoadMore -> lastId.isLessThan(status.id) && status.id.isLessThanOrEqual(
firstId
)
@ -269,7 +269,7 @@ class NetworkTimelineViewModel @Inject constructor(
}
} else {
data[data.size - 1] =
StatusViewData.Placeholder(statuses.last().id, isLoading = false)
StatusViewData.LoadMore(statuses.last().id, isLoading = false)
}
}
@ -288,8 +288,8 @@ class NetworkTimelineViewModel @Inject constructor(
Log.w("NetworkTimelineVM", "failed loading statuses", e)
val index =
statusData.indexOfFirst { it is StatusViewData.Placeholder && it.id == placeholderId }
statusData[index] = StatusViewData.Placeholder(placeholderId, isLoading = false)
statusData.indexOfFirst { it is StatusViewData.LoadMore && it.id == placeholderId }
statusData[index] = StatusViewData.LoadMore(placeholderId, isLoading = false)
currentSource?.invalidate()
}

4
app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.kt

@ -25,7 +25,7 @@ sealed class NotificationViewData {
abstract val id: String
abstract fun asStatusOrNull(): StatusViewData.Concrete?
abstract fun asPlaceholderOrNull(): Placeholder?
abstract fun asPlaceholderOrNull(): LoadMore?
data class Concrete(
override val id: String,
@ -41,7 +41,7 @@ sealed class NotificationViewData {
override fun asPlaceholderOrNull() = null
}
data class Placeholder(
data class LoadMore(
override val id: String,
val isLoading: Boolean
) : NotificationViewData() {

6
app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt

@ -38,7 +38,7 @@ sealed interface TranslationViewData {
* Created by charlag on 11/07/2017.
*
* Class to represent data required to display either a notification or a placeholder.
* It is either a [StatusViewData.Concrete] or a [StatusViewData.Placeholder].
* It is either a [StatusViewData.Concrete] or a [StatusViewData.LoadMore].
*/
sealed class StatusViewData {
abstract val id: String
@ -133,12 +133,12 @@ sealed class StatusViewData {
}
}
data class Placeholder(
data class LoadMore(
override val id: String,
val isLoading: Boolean
) : StatusViewData()
fun asStatusOrNull() = this as? Concrete
fun asPlaceholderOrNull() = this as? Placeholder
fun asPlaceholderOrNull() = this as? LoadMore
}

5
app/src/main/res/drawable/text_placeholder.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?attr/placeholderColor" />
<corners android:radius="6dp"/>
</shape>

1
app/src/main/res/layout/item_conversation.xml

@ -314,7 +314,6 @@
style="@style/TuskyImageButton"
android:layout_width="52dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:contentDescription="@string/action_more"
app:layout_constraintBottom_toBottomOf="@id/status_reply"
app:layout_constraintEnd_toEndOf="parent"

4
app/src/main/res/layout/item_follow.xml

@ -13,11 +13,11 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:drawablePadding="10dp"
android:drawablePadding="@dimen/status_info_drawable_padding_large"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="28dp"
android:paddingStart="@dimen/status_info_padding_large"
android:paddingEnd="0dp"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"

4
app/src/main/res/layout/item_follow_request.xml

@ -13,11 +13,11 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:drawablePadding="10dp"
android:drawablePadding="@dimen/status_info_drawable_padding_large"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="28dp"
android:paddingStart="@dimen/status_info_padding_large"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
app:drawableStartCompat="@drawable/ic_person_add_24dp_mirrored_filled"

0
app/src/main/res/layout/item_status_placeholder.xml → app/src/main/res/layout/item_load_more.xml

140
app/src/main/res/layout/item_placeholder.xml

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAccessibility="noHideDescendants">
<TextView
android:id="@+id/topPlaceholder"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="14dp"
android:background="@drawable/text_placeholder"
android:lines="1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/avatarPlaceholder"
android:layout_width="@dimen/timeline_status_avatar_width"
android:layout_height="@dimen/timeline_status_avatar_height"
android:layout_marginStart="14dp"
android:layout_marginTop="@dimen/account_avatar_margin"
android:importantForAccessibility="no"
android:src="@drawable/text_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topPlaceholder" />
<TextView
android:id="@+id/namePlaceholder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="14dp"
android:background="@drawable/text_placeholder"
android:lines="1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_medium"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/avatarPlaceholder"
app:layout_constraintTop_toBottomOf="@id/topPlaceholder" />
<TextView
android:id="@+id/contentPlaceholder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="14dp"
android:background="@drawable/text_placeholder"
android:lines="3"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/avatarPlaceholder"
app:layout_constraintTop_toBottomOf="@id/namePlaceholder" />
<ImageButton
android:id="@+id/replyButtonPlaceholder"
style="@style/TuskyImageButton"
android:layout_width="52dp"
android:layout_height="48dp"
android:layout_marginStart="-14dp"
android:layout_marginTop="2dp"
android:clickable="false"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/reblogButtonPlaceholder"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="@id/namePlaceholder"
app:layout_constraintTop_toBottomOf="@id/contentPlaceholder"
app:srcCompat="@drawable/ic_reply_24dp"
app:tint="?attr/placeholderColor"
tools:ignore="NegativeMargin" />
<ImageButton
android:id="@+id/reblogButtonPlaceholder"
style="@style/TuskyImageButton"
android:layout_width="52dp"
android:layout_height="48dp"
android:clickable="false"
android:importantForAccessibility="no"
app:layout_constraintEnd_toStartOf="@id/favouriteButtonPlaceholder"
app:layout_constraintStart_toEndOf="@id/replyButtonPlaceholder"
app:layout_constraintTop_toTopOf="@id/replyButtonPlaceholder"
app:srcCompat="@drawable/ic_repeat_24dp"
app:tint="?attr/placeholderColor" />
<ImageButton
android:id="@+id/favouriteButtonPlaceholder"
style="@style/TuskyImageButton"
android:layout_width="52dp"
android:layout_height="48dp"
android:clickable="false"
android:importantForAccessibility="no"
app:layout_constraintEnd_toStartOf="@id/status_bookmark"
app:layout_constraintStart_toEndOf="@id/reblogButtonPlaceholder"
app:layout_constraintTop_toTopOf="@id/reblogButtonPlaceholder"
app:srcCompat="@drawable/ic_star_24dp"
app:tint="?attr/placeholderColor" />
<ImageButton
android:id="@+id/status_bookmark"
style="@style/TuskyImageButton"
android:layout_width="52dp"
android:layout_height="48dp"
android:clickable="false"
android:clipToPadding="false"
android:importantForAccessibility="no"
app:layout_constraintEnd_toStartOf="@id/moreButtonPlaceHolder"
app:layout_constraintStart_toEndOf="@id/favouriteButtonPlaceholder"
app:layout_constraintTop_toTopOf="@id/replyButtonPlaceholder"
app:srcCompat="@drawable/ic_bookmark_24dp"
app:tint="?attr/placeholderColor"
sparkbutton:activeImage="@drawable/ic_bookmark_24dp_filled" />
<ImageButton
android:id="@+id/moreButtonPlaceHolder"
style="@style/TuskyImageButton"
android:layout_width="52dp"
android:layout_height="48dp"
android:clickable="false"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="@id/replyButtonPlaceholder"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/status_bookmark"
app:layout_constraintTop_toTopOf="@id/replyButtonPlaceholder"
app:srcCompat="@drawable/ic_more_horiz_24dp"
app:tint="?attr/placeholderColor" />
</androidx.constraintlayout.widget.ConstraintLayout>

4
app/src/main/res/layout/item_report_notification.xml

@ -15,11 +15,11 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:drawablePadding="10dp"
android:drawablePadding="@dimen/status_info_drawable_padding_large"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="28dp"
android:paddingStart="@dimen/status_info_padding_large"
android:paddingEnd="0dp"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"

4
app/src/main/res/layout/item_status_notification.xml

@ -15,11 +15,11 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="6dp"
android:drawablePadding="10dp"
android:drawablePadding="@dimen/status_info_drawable_padding_large"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="28dp"
android:paddingStart="@dimen/status_info_padding_large"
android:paddingEnd="0dp"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"

4
app/src/main/res/layout/item_unknown_notification.xml

@ -14,11 +14,11 @@ android:paddingBottom="14dp">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:drawablePadding="10dp"
android:drawablePadding="@dimen/status_info_drawable_padding_large"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="28dp"
android:paddingStart="@dimen/status_info_padding_large"
android:paddingEnd="0dp"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"

2
app/src/main/res/values-night/theme_colors.xml

@ -37,6 +37,8 @@
<color name="toolbarIconBackgroundColor">#CC444B5D</color>
<color name="placeholderColor">@color/tusky_grey_40</color>
<!-- colors used to show inserted/deleted text -->
<color name="view_edits_background_insert">@color/tusky_green_dark</color>
<color name="view_edits_background_delete">@color/tusky_red</color>

1
app/src/main/res/values/attrs.xml

@ -42,6 +42,7 @@
<attr name="windowBackgroundColor" format="reference|color" />
<attr name="dividerColor" format="reference|color" />
<attr name="toolbarIconBackgroundColor" format="reference|color" />
<attr name="placeholderColor" format="reference|color" />
<attr name="status_text_small" format="dimension" />
<attr name="status_text_medium" format="dimension" />

2
app/src/main/res/values/dimens.xml

@ -98,4 +98,6 @@
<dimen name="dialog_activity_vertical_inset">64dp</dimen>
<dimen name="conversation_placeholder_more_button_inset">14dp</dimen>
</resources>

3
app/src/main/res/values/styles.xml

@ -115,6 +115,8 @@
<item name="searchViewStyle">@style/TuskySearchViewStyle</item>
<item name="toolbarIconBackgroundColor">@color/toolbarIconBackgroundColor</item>
<item name="placeholderColor">@color/placeholderColor</item>
</style>
<style name="TuskyBlackThemeBase" parent="TuskyBaseTheme">
@ -142,6 +144,7 @@
<item name="colorOutlineVariant">@color/tusky_grey_20</item>
<item name="toolbarIconBackgroundColor">@color/transparent_tusky_grey_10</item>
<item name="placeholderColor">@color/tusky_grey_30</item>
</style>
<style name="TuskyBlackTheme" parent="TuskyBlackThemeBase" />

2
app/src/main/res/values/theme_colors.xml

@ -37,6 +37,8 @@
<color name="toolbarIconBackgroundColor">#CCEBEFF4</color>
<color name="placeholderColor">@color/tusky_grey_90</color>
<!-- colors used to show inserted/deleted text -->
<color name="view_edits_background_insert">@color/tusky_green_lighter</color>
<color name="view_edits_background_delete">@color/tusky_red_lighter</color>

4
app/src/test/java/com/keylesspalace/tusky/components/notifications/NotificationFaker.kt

@ -2,7 +2,7 @@ package com.keylesspalace.tusky.components.notifications
import androidx.paging.PagingSource
import androidx.room.withTransaction
import com.keylesspalace.tusky.components.timeline.Placeholder
import com.keylesspalace.tusky.components.timeline.LoadMorePlaceholder
import com.keylesspalace.tusky.components.timeline.fakeAccount
import com.keylesspalace.tusky.components.timeline.fakeStatus
import com.keylesspalace.tusky.components.timeline.toEntity
@ -66,7 +66,7 @@ fun Notification.toNotificationDataEntity(
moderationWarning = null,
)
fun Placeholder.toNotificationDataEntity(
fun LoadMorePlaceholder.toNotificationDataEntity(
tuskyAccountId: Long
) = NotificationDataEntity(
tuskyAccountId = tuskyAccountId,

8
app/src/test/java/com/keylesspalace/tusky/components/notifications/NotificationsRemoteMediatorTest.kt

@ -10,7 +10,7 @@ import androidx.paging.RemoteMediator
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.keylesspalace.tusky.components.timeline.Placeholder
import com.keylesspalace.tusky.components.timeline.LoadMorePlaceholder
import com.keylesspalace.tusky.components.timeline.fakeStatus
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
@ -196,7 +196,7 @@ class NotificationsRemoteMediatorTest {
listOf(
fakeNotification(id = "8").toNotificationDataEntity(1),
fakeNotification(id = "7").toNotificationDataEntity(1),
Placeholder(id = "5", loading = false).toNotificationDataEntity(1),
LoadMorePlaceholder(id = "5", loading = false).toNotificationDataEntity(1),
fakeNotification(id = "3").toNotificationDataEntity(1),
fakeNotification(id = "2").toNotificationDataEntity(1),
fakeNotification(id = "1").toNotificationDataEntity(1)
@ -449,7 +449,7 @@ class NotificationsRemoteMediatorTest {
)
db.insert(notificationsAlreadyInDb)
val placeholder = Placeholder(id = "6", loading = false).toNotificationEntity(1)
val placeholder = LoadMorePlaceholder(id = "6", loading = false).toNotificationEntity(1)
db.notificationsDao().insertNotification(placeholder)
val remoteMediator = NotificationsRemoteMediator(
@ -493,7 +493,7 @@ class NotificationsRemoteMediatorTest {
fakeNotification(id = "9").toNotificationDataEntity(1),
fakeNotification(id = "8").toNotificationDataEntity(1),
fakeNotification(id = "7").toNotificationDataEntity(1),
Placeholder(id = "6", loading = false).toNotificationDataEntity(1),
LoadMorePlaceholder(id = "6", loading = false).toNotificationDataEntity(1),
fakeNotification(id = "1").toNotificationDataEntity(1)
)
)

2
app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt

@ -237,7 +237,7 @@ class NetworkTimelineRemoteMediatorTest {
val newStatusData = mutableListOf(
fakeStatusViewData("10"),
fakeStatusViewData("9"),
StatusViewData.Placeholder("7", false),
StatusViewData.LoadMore("7", false),
fakeStatusViewData("3"),
fakeStatusViewData("2"),
fakeStatusViewData("1")

8
app/src/test/java/com/keylesspalace/tusky/db/dao/NotificationsDaoTest.kt

@ -9,7 +9,7 @@ import com.keylesspalace.tusky.components.notifications.fakeReport
import com.keylesspalace.tusky.components.notifications.insert
import com.keylesspalace.tusky.components.notifications.toNotificationDataEntity
import com.keylesspalace.tusky.components.notifications.toNotificationEntity
import com.keylesspalace.tusky.components.timeline.Placeholder
import com.keylesspalace.tusky.components.timeline.LoadMorePlaceholder
import com.keylesspalace.tusky.components.timeline.fakeAccount
import com.keylesspalace.tusky.components.timeline.fakeStatus
import com.keylesspalace.tusky.db.AppDatabase
@ -191,9 +191,9 @@ class NotificationsDaoTest {
)
db.insert(notifications)
notificationsDao.insertNotification(Placeholder(id = "99", loading = false).toNotificationEntity(1))
notificationsDao.insertNotification(Placeholder(id = "96", loading = false).toNotificationEntity(1))
notificationsDao.insertNotification(Placeholder(id = "80", loading = false).toNotificationEntity(1))
notificationsDao.insertNotification(LoadMorePlaceholder(id = "99", loading = false).toNotificationEntity(1))
notificationsDao.insertNotification(LoadMorePlaceholder(id = "96", loading = false).toNotificationEntity(1))
notificationsDao.insertNotification(LoadMorePlaceholder(id = "80", loading = false).toNotificationEntity(1))
assertEquals("99", notificationsDao.getTopPlaceholderId(1))
}

Loading…
Cancel
Save