Refactor notifications to Kotlin & paging (#4026)

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 https://github.com/tuskyapp/Tusky/issues/3429

---------

Co-authored-by: Zongle Wang <wangzongler@gmail.com>
This commit is contained in:
Konrad Pozniak 2024-05-03 18:27:10 +02:00 committed by GitHub
commit b2c0b18c8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
121 changed files with 6992 additions and 4654 deletions

View file

@ -0,0 +1,151 @@
/* Copyright 2018 Conny Duck
*
* 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.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.db.Converters
import com.keylesspalace.tusky.defaultTabs
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Status
@Entity(
indices = [
Index(
value = ["domain", "accountId"],
unique = true
)
]
)
@TypeConverters(Converters::class)
data class AccountEntity(
@field:PrimaryKey(autoGenerate = true) var id: Long,
val domain: String,
var accessToken: String,
// nullable for backward compatibility
var clientId: String?,
// nullable for backward compatibility
var clientSecret: String?,
var isActive: Boolean,
var accountId: String = "",
var username: String = "",
var displayName: String = "",
var profilePictureUrl: String = "",
var notificationsEnabled: Boolean = true,
var notificationsMentioned: Boolean = true,
var notificationsFollowed: Boolean = true,
var notificationsFollowRequested: Boolean = false,
var notificationsReblogged: Boolean = true,
var notificationsFavorited: Boolean = true,
var notificationsPolls: Boolean = true,
var notificationsSubscriptions: Boolean = true,
var notificationsSignUps: Boolean = true,
var notificationsUpdates: Boolean = true,
var notificationsReports: Boolean = true,
var notificationSound: Boolean = true,
var notificationVibration: Boolean = true,
var notificationLight: Boolean = true,
var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC,
var defaultMediaSensitivity: Boolean = false,
var defaultPostLanguage: String = "",
var alwaysShowSensitiveMedia: Boolean = false,
/** True if content behind a content warning is shown by default */
var alwaysOpenSpoiler: Boolean = false,
/**
* True if the "Download media previews" preference is true. This implies
* that media previews are shown as well as downloaded.
*/
var mediaPreviewEnabled: Boolean = true,
/**
* ID of the last notification the user read on the Notification, list, and should be restored
* to view when the user returns to the list.
*
* May not be the ID of the most recent notification if the user has scrolled down the list.
*/
var lastNotificationId: String = "0",
/**
* ID of the most recent Mastodon notification that Tusky has fetched to show as an
* Android notification.
*/
@ColumnInfo(defaultValue = "0")
var notificationMarkerId: String = "0",
var emojis: List<Emoji> = emptyList(),
var tabPreferences: List<TabData> = defaultTabs(),
var notificationsFilter: String = "[\"follow_request\"]",
// Scope cannot be changed without re-login, so store it in case
// the scope needs to be changed in the future
var oauthScopes: String = "",
var unifiedPushUrl: String = "",
var pushPubKey: String = "",
var pushPrivKey: String = "",
var pushAuth: String = "",
var pushServerKey: String = "",
/**
* ID of the status at the top of the visible list in the home timeline when the
* user navigated away.
*/
var lastVisibleHomeTimelineStatusId: String? = null,
/** true if the connected Mastodon account is locked (has to manually approve all follow requests **/
@ColumnInfo(defaultValue = "0")
var locked: Boolean = false,
@ColumnInfo(defaultValue = "0")
var hasDirectMessageBadge: Boolean = false,
var isShowHomeBoosts: Boolean = true,
var isShowHomeReplies: Boolean = true,
var isShowHomeSelfBoosts: Boolean = true
) {
val identifier: String
get() = "$domain:$accountId"
val fullName: String
get() = "@$username@$domain"
fun logout() {
// deleting credentials so they cannot be used again
accessToken = ""
clientId = null
clientSecret = null
}
fun isLoggedIn() = accessToken.isNotEmpty()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AccountEntity
if (id == other.id) return true
return domain == other.domain && accountId == other.accountId
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + domain.hashCode()
result = 31 * result + accountId.hashCode()
return result
}
}

View file

@ -0,0 +1,67 @@
/* Copyright 2020 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.db.entity
import android.net.Uri
import android.os.Parcelable
import androidx.core.net.toUri
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.keylesspalace.tusky.db.Converters
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status
import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize
@Entity
@TypeConverters(Converters::class)
data class DraftEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val accountId: Long,
val inReplyToId: String?,
val content: String?,
val contentWarning: String?,
val sensitive: Boolean,
val visibility: Status.Visibility,
val attachments: List<DraftAttachment>,
val poll: NewPoll?,
val failedToSend: Boolean,
val failedToSendNew: Boolean,
val scheduledAt: String?,
val language: String?,
val statusId: String?
)
@JsonClass(generateAdapter = true)
@Parcelize
data class DraftAttachment(
val uriString: String,
val description: String?,
val focus: Attachment.Focus?,
val type: Type
) : Parcelable {
val uri: Uri
get() = uriString.toUri()
@JsonClass(generateAdapter = false)
enum class Type {
IMAGE,
VIDEO,
AUDIO
}
}

View file

@ -0,0 +1,68 @@
/* 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.db.entity
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
/**
* Entity to store an item on the home timeline. Can be a standalone status, a reblog, or a placeholder.
*/
@Entity(
primaryKeys = ["id", "tuskyAccountId"],
foreignKeys = (
[
ForeignKey(
entity = TimelineStatusEntity::class,
parentColumns = ["serverId", "tuskyAccountId"],
childColumns = ["statusId", "tuskyAccountId"]
),
ForeignKey(
entity = TimelineAccountEntity::class,
parentColumns = ["serverId", "tuskyAccountId"],
childColumns = ["reblogAccountId", "tuskyAccountId"]
)
]
),
indices = [
Index("statusId", "tuskyAccountId"),
Index("reblogAccountId", "tuskyAccountId"),
]
)
data class HomeTimelineEntity(
val tuskyAccountId: Long,
// the id by which the timeline is sorted
val id: String,
// the id of the status, null when a placeholder
val statusId: String?,
// the id of the account who reblogged the status, null if no reblog
val reblogAccountId: String?,
// only relevant when this is a placeholder
val loading: Boolean = false
)
/**
* Helper class for queries that return HomeTimelineEntity including all references
*/
data class HomeTimelineData(
val id: String,
@Embedded val status: TimelineStatusEntity?,
@Embedded(prefix = "a_") val account: TimelineAccountEntity?,
@Embedded(prefix = "rb_") val reblogAccount: TimelineAccountEntity?,
val loading: Boolean
)

View file

@ -0,0 +1,69 @@
/* Copyright 2018 Conny Duck
*
* 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.db.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.keylesspalace.tusky.db.Converters
import com.keylesspalace.tusky.entity.Emoji
@Entity
@TypeConverters(Converters::class)
data class InstanceEntity(
@PrimaryKey val instance: String,
val emojiList: List<Emoji>?,
val maximumTootCharacters: Int?,
val maxPollOptions: Int?,
val maxPollOptionLength: Int?,
val minPollDuration: Int?,
val maxPollDuration: Int?,
val charactersReservedPerUrl: Int?,
val version: String?,
val videoSizeLimit: Int?,
val imageSizeLimit: Int?,
val imageMatrixLimit: Int?,
val maxMediaAttachments: Int?,
val maxFields: Int?,
val maxFieldNameLength: Int?,
val maxFieldValueLength: Int?,
val translationEnabled: Boolean?,
)
@TypeConverters(Converters::class)
data class EmojisEntity(
@PrimaryKey val instance: String,
val emojiList: List<Emoji>?
)
data class InstanceInfoEntity(
@PrimaryKey val instance: String,
val maximumTootCharacters: Int?,
val maxPollOptions: Int?,
val maxPollOptionLength: Int?,
val minPollDuration: Int?,
val maxPollDuration: Int?,
val charactersReservedPerUrl: Int?,
val version: String?,
val videoSizeLimit: Int?,
val imageSizeLimit: Int?,
val imageMatrixLimit: Int?,
val maxMediaAttachments: Int?,
val maxFields: Int?,
val maxFieldNameLength: Int?,
val maxFieldValueLength: Int?,
val translationEnabled: Boolean?,
)

View file

@ -0,0 +1,107 @@
/* 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.db.entity
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.TypeConverters
import com.keylesspalace.tusky.db.Converters
import com.keylesspalace.tusky.entity.Notification
import java.util.Date
data class NotificationDataEntity(
// id of the account logged into Tusky this notifications belongs to
val tuskyAccountId: Long,
// null when placeholder
val type: Notification.Type?,
val id: String,
@Embedded(prefix = "a_") val account: TimelineAccountEntity?,
@Embedded(prefix = "s_") val status: TimelineStatusEntity?,
@Embedded(prefix = "sa_") val statusAccount: TimelineAccountEntity?,
@Embedded(prefix = "r_") val report: NotificationReportEntity?,
@Embedded(prefix = "ra_") val reportTargetAccount: TimelineAccountEntity?,
// relevant when it is a placeholder
val loading: Boolean = false
)
@Entity(
primaryKeys = ["id", "tuskyAccountId"],
foreignKeys = (
[
ForeignKey(
entity = TimelineAccountEntity::class,
parentColumns = ["serverId", "tuskyAccountId"],
childColumns = ["accountId", "tuskyAccountId"]
),
ForeignKey(
entity = TimelineStatusEntity::class,
parentColumns = ["serverId", "tuskyAccountId"],
childColumns = ["statusId", "tuskyAccountId"]
),
ForeignKey(
entity = NotificationReportEntity::class,
parentColumns = ["serverId", "tuskyAccountId"],
childColumns = ["reportId", "tuskyAccountId"]
)
]
),
indices = [
Index("accountId", "tuskyAccountId"),
Index("statusId", "tuskyAccountId"),
Index("reportId", "tuskyAccountId"),
]
)
@TypeConverters(Converters::class)
data class NotificationEntity(
// id of the account logged into Tusky this notifications belongs to
val tuskyAccountId: Long,
// null when placeholder
val type: Notification.Type?,
val id: String,
val accountId: String?,
val statusId: String?,
val reportId: String?,
// relevant when it is a placeholder
val loading: Boolean = false
)
@Entity(
primaryKeys = ["serverId", "tuskyAccountId"],
foreignKeys = (
[
ForeignKey(
entity = TimelineAccountEntity::class,
parentColumns = ["serverId", "tuskyAccountId"],
childColumns = ["targetAccountId", "tuskyAccountId"]
)
]
),
indices = [
Index("targetAccountId", "tuskyAccountId"),
]
)
@TypeConverters(Converters::class)
data class NotificationReportEntity(
// id of the account logged into Tusky this report belongs to
val tuskyAccountId: Long,
val serverId: String,
val category: String,
val statusIds: List<String>?,
val createdAt: Date,
val targetAccountId: String?
)

View file

@ -0,0 +1,37 @@
/* 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.db.entity
import androidx.room.Entity
import androidx.room.TypeConverters
import com.keylesspalace.tusky.db.Converters
import com.keylesspalace.tusky.entity.Emoji
@Entity(
primaryKeys = ["serverId", "tuskyAccountId"]
)
@TypeConverters(Converters::class)
data class TimelineAccountEntity(
val serverId: String,
val tuskyAccountId: Long,
val localUsername: String,
val username: String,
val displayName: String,
val url: String,
val avatar: String,
val emojis: List<Emoji>,
val bot: Boolean
)

View file

@ -0,0 +1,87 @@
/* 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.db.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.TypeConverters
import com.keylesspalace.tusky.db.Converters
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Card
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.FilterResult
import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status
/**
* Entity for caching status data. Used within home timelines and notifications.
* The information if a status is a reblog is not stored here but in [HomeTimelineEntity].
*/
@Entity(
primaryKeys = ["serverId", "tuskyAccountId"],
foreignKeys = (
[
ForeignKey(
entity = TimelineAccountEntity::class,
parentColumns = ["serverId", "tuskyAccountId"],
childColumns = ["authorServerId", "tuskyAccountId"]
)
]
),
// Avoiding rescanning status table when accounts table changes. Recommended by Room(c).
indices = [Index("authorServerId", "tuskyAccountId")]
)
@TypeConverters(Converters::class)
data class TimelineStatusEntity(
// id never flips: we need it for sorting so it's a real id
val serverId: String,
val url: String?,
// our local id for the logged in user in case there are multiple accounts per instance
val tuskyAccountId: Long,
val authorServerId: String,
val inReplyToId: String?,
val inReplyToAccountId: String?,
val content: String,
val createdAt: Long,
val editedAt: Long?,
val emojis: List<Emoji>,
val reblogsCount: Int,
val favouritesCount: Int,
val repliesCount: Int,
val reblogged: Boolean,
val bookmarked: Boolean,
val favourited: Boolean,
val sensitive: Boolean,
val spoilerText: String,
val visibility: Status.Visibility,
val attachments: List<Attachment>,
val mentions: List<Status.Mention>,
val tags: List<HashTag>,
val application: Status.Application?,
// if it has a reblogged status, it's id is stored here
val poll: Poll?,
val muted: Boolean,
/** Also used as the "loading" attribute when this TimelineStatusEntity is a placeholder */
val expanded: Boolean,
val contentCollapsed: Boolean,
val contentShowing: Boolean,
val pinned: Boolean,
val card: Card?,
val language: String?,
val filtered: List<FilterResult>
)