Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor notifications to Kotlin & paging #4026

Merged
merged 105 commits into from
May 3, 2024

Conversation

connyduck
Copy link
Collaborator

@connyduck connyduck commented Sep 19, 2023

This refactors the NotificationsFragment and related classes to Kotlin & paging.
While trying to preserve as much of the original behavior as possible, this adds the following improvements as well:

  • The "show notifications filter" preference was added again
  • The "load more" button now has a background ripple effect when clicked
  • The "legal" report category of Mastodon 4.2 is now supported in report notifications
  • Unknown notifications now display "unknown notification type" instead of an empty line

Other code quality improvements:

  • All views from xml layouts are now referenced via ViewBindings
  • the classes responsible for showing system notifications were moved to a new package systemnotifications while the classes from this refactoring are in notifications
  • the id of the local Tusky account is now called tuskyAccountId in all places I could find

closes #3429

@connyduck
Copy link
Collaborator Author

The basic database setup is done, it is a bit more complicated than the timeline one and needs some cleanup but works fine. @charlag maybe you can have a look at it, I would be very interested in your opinion.
I think it would be nice to not have duplicate tables
Merging NotificationAccountEntity and TimelineAccountEntity should be easy.
NotificationStatusEntity and TimelineStatusEntity not so much, since the timeline table is used for the home timeline and I can't just mix other statuses in there. Maybe add a boolean flag isInHomeTimeline?
Thoughts?

# Conflicts:
#	app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt
#	app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
#	app/src/main/res/values/strings.xml
import com.keylesspalace.tusky.entity.Status
import java.util.Date

data class NotificationDataEntity(
Copy link
Collaborator

@charlag charlag Nov 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the end we should give each class here a one-sentence doc explaining what they are for

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused for a while before I realized that this class is not actually a DB table

@charlag
Copy link
Collaborator

charlag commented Nov 5, 2023

The basic database setup is done, it is a bit more complicated than the timeline one and needs some cleanup but works fine. @charlag maybe you can have a look at it, I would be very interested in your opinion. I think it would be nice to not have duplicate tables Merging NotificationAccountEntity and TimelineAccountEntity should be easy. NotificationStatusEntity and TimelineStatusEntity not so much, since the timeline table is used for the home timeline and I can't just mix other statuses in there. Maybe add a boolean flag isInHomeTimeline? Thoughts?

I couldn't get too deeply into it but as far as I understand you did the same trick as with posts, just more complicated. Nice!

I think merging accounts could work if we can take care of cleanup but it gets a bit complicated.

We could merge statuses but it would be a lot of work. We would need a table with just IDs of the posts in timeline and a separate table with just posts (that's how Masto work internally too, with timelines being in Redis and posts in db). Then we could cache arbitrary posts in there.

I think at least for now your approach makes a lot of sense?

# Conflicts:
#	app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
#	app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt
#	app/src/main/java/com/keylesspalace/tusky/components/notifications/ReportNotificationViewHolder.kt
#	app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt
#	app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
#	app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
@connyduck
Copy link
Collaborator Author

Ok I think I have addressed everything

@connyduck connyduck requested review from cbeyls and Tak April 18, 2024 19:50
Comment on lines +158 to +175
ListStatusAccessibilityDelegate(
binding.recyclerView,
this,
StatusProvider { pos: Int ->
if (pos in 0 until adapter.itemCount) {
val notification = adapter.peek(pos)
// We support replies only for now
if (notification is NotificationViewData.Concrete) {
return@StatusProvider notification.statusViewData
} else {
return@StatusProvider null
}
} else {
null
}
}
)
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ListStatusAccessibilityDelegate(
binding.recyclerView,
this,
StatusProvider { pos: Int ->
if (pos in 0 until adapter.itemCount) {
val notification = adapter.peek(pos)
// We support replies only for now
if (notification is NotificationViewData.Concrete) {
return@StatusProvider notification.statusViewData
} else {
return@StatusProvider null
}
} else {
null
}
}
)
)
ListStatusAccessibilityDelegate(
binding.recyclerView,
this
) { pos: Int ->
if (pos in 0 until adapter.itemCount) {
val notification = adapter.peek(pos)
// We support replies only for now
if (notification is NotificationViewData.Concrete) {
notification.statusViewData
} else {
null
}
} else {
null
}
}

import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.viewdata.NotificationViewData

interface NotificationActionListener {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
interface NotificationActionListener {
fun interface NotificationActionListener {

fun onViewReport(reportId: String?)
}

interface NotificationsViewHolder {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
interface NotificationsViewHolder {
fun interface NotificationsViewHolder {

Comment on lines 21 to 36
sealed class NotificationViewData {

abstract val id: String

data class Concrete(
override val id: String,
val type: Notification.Type,
val account: TimelineAccount,
val statusViewData: StatusViewData.Concrete?,
val report: Report?
) : NotificationViewData()

data class Placeholder(
override val id: String,
val isLoading: Boolean
) : NotificationViewData()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sealed class NotificationViewData {
abstract val id: String
data class Concrete(
override val id: String,
val type: Notification.Type,
val account: TimelineAccount,
val statusViewData: StatusViewData.Concrete?,
val report: Report?
) : NotificationViewData()
data class Placeholder(
override val id: String,
val isLoading: Boolean
) : NotificationViewData()
sealed interface NotificationViewData {
val id: String
data class Concrete(
override val id: String,
val type: Notification.Type,
val account: TimelineAccount,
val statusViewData: StatusViewData.Concrete?,
val report: Report?
) : NotificationViewData
data class Placeholder(
override val id: String,
val isLoading: Boolean
) : NotificationViewData

@Tak
Copy link
Collaborator

Tak commented Apr 19, 2024

Should the android notifications get dismissed when we open the notifications view?

@connyduck
Copy link
Collaborator Author

Should the android notifications get dismissed when we open the notifications view?

yes

bindViewHolder(viewHolder, position, payloads)
}

private fun bindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int, payloads: List<Any>?) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The payloads argument in a RecyclerView.Adapter is actually never null, it will be an empty list by default. You can change all the associated methods signatures to non-null.

I still recommend to change all the payloads arguments to be non-null, then all the associated .isNullOrEmpty() tests to .isEmpty()

shouldFilterStatus(notificationViewData) != Filter.Action.HIDE
}
}
.flowOn(Dispatchers.Default)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to change the upstream dispatcher if you already specify it in pagingData.map(<Dispatcher>) and pagingData.filter(<Dispatcher>).
It's probably better to keep this line but remove the custom dispatcher for map and filter.

bindingAdapterPosition
)
}
binding.notificationContent.visibility =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also use ktx extension properties for view visibility:

binding.notificationContent.isVisible = statusViewData.isExpanded

Note that the custom View.visible extension method used in the project serves the same purpose with a different syntax.

val translation = translations[timelineStatus.status.serverId]
timelineStatus.toViewData(
moshi,
pagingData.map(Dispatchers.Default.asExecutor()) { timelineData ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark here, it's probably overkill to specify a dispatcher both here and in flowOn()

@Tak Tak mentioned this pull request Apr 26, 2024
@connyduck connyduck merged commit b2c0b18 into tuskyapp:develop May 3, 2024
3 checks passed
connyduck pushed a commit that referenced this pull request May 10, 2024
The menu doesn't show up anymore after merging #4026.
I assume this was unintentional since the Fragment still implements
`MenuProvider`.
@connyduck connyduck deleted the refactor_notifications branch May 10, 2024 14:23
connyduck added a commit that referenced this pull request May 31, 2024
The option was removed in #3877,
then forgotten to be added again in
#4015.
#4026 added it again, but took to
long to merge, so we made #4225 as
well and that is how we ended up with the option twice.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cache Notifications
6 participants