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,35 @@
/* 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.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.keylesspalace.tusky.db.entity.AccountEntity
@Dao
interface AccountDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(account: AccountEntity): Long
@Delete
fun delete(account: AccountEntity)
@Query("SELECT * FROM AccountEntity ORDER BY id ASC")
fun loadAll(): List<AccountEntity>
}

View file

@ -0,0 +1,51 @@
/* 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.dao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.keylesspalace.tusky.db.entity.DraftEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface DraftDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrReplace(draft: DraftEntity)
@Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC")
fun draftsPagingSource(accountId: Long): PagingSource<Int, DraftEntity>
@Query("SELECT COUNT(*) FROM DraftEntity WHERE accountId = :accountId AND failedToSendNew = 1")
fun draftsNeedUserAlert(accountId: Long): Flow<Int>
@Query(
"UPDATE DraftEntity SET failedToSendNew = 0 WHERE accountId = :accountId AND failedToSendNew = 1"
)
suspend fun draftsClearNeedUserAlert(accountId: Long)
@Query("SELECT * FROM DraftEntity WHERE accountId = :accountId")
suspend fun loadDrafts(accountId: Long): List<DraftEntity>
@Query("DELETE FROM DraftEntity WHERE id = :id")
suspend fun delete(id: Int)
@Query("SELECT * FROM DraftEntity WHERE id = :id")
suspend fun find(id: Int): DraftEntity?
}

View file

@ -0,0 +1,42 @@
/* 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.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.RewriteQueriesToDropUnusedColumns
import androidx.room.Upsert
import com.keylesspalace.tusky.db.entity.EmojisEntity
import com.keylesspalace.tusky.db.entity.InstanceEntity
import com.keylesspalace.tusky.db.entity.InstanceInfoEntity
@Dao
interface InstanceDao {
@Upsert(entity = InstanceEntity::class)
suspend fun upsert(instance: InstanceInfoEntity)
@Upsert(entity = InstanceEntity::class)
suspend fun upsert(emojis: EmojisEntity)
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
suspend fun getInstanceInfo(instance: String): InstanceInfoEntity?
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
suspend fun getEmojiInfo(instance: String): EmojisEntity?
}

View file

@ -0,0 +1,175 @@
/* Copyright 2023 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.dao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy.Companion.REPLACE
import androidx.room.Query
import com.keylesspalace.tusky.db.entity.NotificationDataEntity
import com.keylesspalace.tusky.db.entity.NotificationEntity
import com.keylesspalace.tusky.db.entity.NotificationReportEntity
@Dao
abstract class NotificationsDao {
@Insert(onConflict = REPLACE)
abstract suspend fun insertNotification(notificationEntity: NotificationEntity): Long
@Insert(onConflict = REPLACE)
abstract suspend fun insertReport(notificationReportDataEntity: NotificationReportEntity): Long
@Query(
"""
SELECT n.tuskyAccountId, n.type, n.id, n.loading,
a.serverId as 'a_serverId', a.tuskyAccountId as 'a_tuskyAccountId',
a.localUsername as 'a_localUsername', a.username as 'a_username',
a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar',
a.emojis as 'a_emojis', a.bot as 'a_bot',
s.serverId as 's_serverId', s.url as 's_url', s.tuskyAccountId as 's_tuskyAccountId',
s.authorServerId as 's_authorServerId', s.inReplyToId as 's_inReplyToId', s.inReplyToAccountId as 's_inReplyToAccountId',
s.content as 's_content', s.createdAt as 's_createdAt', s.editedAt as 's_editedAt', s.emojis as 's_emojis', s.reblogsCount as 's_reblogsCount',
s.favouritesCount as 's_favouritesCount', s.repliesCount as 's_repliesCount', s.reblogged as 's_reblogged', s.favourited as 's_favourited',
s.bookmarked as 's_bookmarked', s.sensitive as 's_sensitive', s.spoilerText as 's_spoilerText', s.visibility as 's_visibility',
s.mentions as 's_mentions', s.tags as 's_tags', s.application as 's_application', s.content as 's_content', s.attachments as 's_attachments', s.poll as 's_poll',
s.card as 's_card', s.muted as 's_muted', s.expanded as 's_expanded', s.contentShowing as 's_contentShowing', s.contentCollapsed as 's_contentCollapsed',
s.pinned as 's_pinned', s.language as 's_language', s.filtered as 's_filtered',
sa.serverId as 'sa_serverId', sa.tuskyAccountId as 'sa_tuskyAccountId',
sa.localUsername as 'sa_localUsername', sa.username as 'sa_username',
sa.displayName as 'sa_displayName', sa.url as 'sa_url', sa.avatar as 'sa_avatar',
sa.emojis as 'sa_emojis', sa.bot as 'sa_bot',
r.serverId as 'r_serverId', r.tuskyAccountId as 'r_tuskyAccountId',
r.category as 'r_category', r.statusIds as 'r_statusIds',
r.createdAt as 'r_createdAt', r.targetAccountId as 'r_targetAccountId',
ra.serverId as 'ra_serverId', ra.tuskyAccountId as 'ra_tuskyAccountId',
ra.localUsername as 'ra_localUsername', ra.username as 'ra_username',
ra.displayName as 'ra_displayName', ra.url as 'ra_url', ra.avatar as 'ra_avatar',
ra.emojis as 'ra_emojis', ra.bot as 'ra_bot'
FROM NotificationEntity n
LEFT JOIN TimelineAccountEntity a ON (n.tuskyAccountId = a.tuskyAccountId AND n.accountId = a.serverId)
LEFT JOIN TimelineStatusEntity s ON (n.tuskyAccountId = s.tuskyAccountId AND n.statusId = s.serverId)
LEFT JOIN TimelineAccountEntity sa ON (n.tuskyAccountId = sa.tuskyAccountId AND s.authorServerId = sa.serverId)
LEFT JOIN NotificationReportEntity r ON (n.tuskyAccountId = r.tuskyAccountId AND n.reportId = r.serverId)
LEFT JOIN TimelineAccountEntity ra ON (n.tuskyAccountId = ra.tuskyAccountId AND r.targetAccountId = ra.serverId)
WHERE n.tuskyAccountId = :tuskyAccountId
ORDER BY LENGTH(n.id) DESC, n.id DESC"""
)
abstract fun getNotifications(tuskyAccountId: Long): PagingSource<Int, NotificationDataEntity>
@Query(
"""DELETE FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND id = :notificationId"""
)
abstract suspend fun delete(tuskyAccountId: Long, notificationId: String): Int
@Query(
"""DELETE FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND
(LENGTH(id) < LENGTH(:maxId) OR LENGTH(id) == LENGTH(:maxId) AND id <= :maxId)
AND
(LENGTH(id) > LENGTH(:minId) OR LENGTH(id) == LENGTH(:minId) AND id >= :minId)
"""
)
abstract suspend fun deleteRange(tuskyAccountId: Long, minId: String, maxId: String): Int
@Query(
"""DELETE FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId"""
)
internal abstract suspend fun removeAllNotifications(tuskyAccountId: Long)
/**
* Deletes all NotificationReportEntities for Tusky user with id [tuskyAccountId].
* Warning: This can violate foreign key constraints if reports are still referenced in the NotificationEntity table.
*/
@Query(
"""DELETE FROM NotificationReportEntity WHERE tuskyAccountId = :tuskyAccountId"""
)
internal abstract suspend fun removeAllReports(tuskyAccountId: Long)
@Query(
"""DELETE FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId = :statusId"""
)
abstract suspend fun deleteAllWithStatus(tuskyAccountId: Long, statusId: String)
/**
* Remove all notifications from user with id [userId] unless they are admin notifications.
*/
@Query(
"""DELETE FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND
statusId IN
(SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND
(authorServerId == :userId OR accountId == :userId))
AND type != "admin.sign_up" AND type != "admin.report"
"""
)
abstract suspend fun removeAllByUser(tuskyAccountId: Long, userId: String)
@Query(
"""DELETE FROM NotificationEntity
WHERE tuskyAccountId = :tuskyAccountId AND statusId IN (
SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND authorServerId in
( SELECT serverId FROM TimelineAccountEntity WHERE username LIKE '%@' || :instanceDomain
AND tuskyAccountId = :tuskyAccountId)
OR accountId IN ( SELECT serverId FROM TimelineAccountEntity WHERE username LIKE '%@' || :instanceDomain
AND tuskyAccountId = :tuskyAccountId)
)"""
)
abstract suspend fun deleteAllFromInstance(tuskyAccountId: Long, instanceDomain: String)
@Query("SELECT id FROM NotificationEntity WHERE tuskyAccountId = :accountId ORDER BY LENGTH(id) DESC, id DESC LIMIT 1")
abstract suspend fun getTopId(accountId: Long): String?
@Query("SELECT id FROM NotificationEntity WHERE tuskyAccountId = :accountId AND type IS NULL ORDER BY LENGTH(id) DESC, id DESC LIMIT 1")
abstract suspend fun getTopPlaceholderId(accountId: Long): String?
/**
* Cleans the NotificationEntity table from old entries.
* @param tuskyAccountId id of the account for which to clean tables
* @param limit how many timeline items to keep
*/
@Query(
"""DELETE FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND id NOT IN
(SELECT id FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(id) DESC, id DESC LIMIT :limit)
"""
)
internal abstract suspend fun cleanupNotifications(tuskyAccountId: Long, limit: Int)
/**
* Cleans the NotificationReportEntity table from unreferenced entries.
* @param tuskyAccountId id of the account for which to clean the table
*/
@Query(
"""DELETE FROM NotificationReportEntity WHERE tuskyAccountId = :tuskyAccountId
AND serverId NOT IN
(SELECT reportId FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId and reportId IS NOT NULL)"""
)
internal abstract suspend fun cleanupReports(tuskyAccountId: Long)
/**
* Returns the id directly above [id], or null if [id] is the id of the top item
*/
@Query(
"SELECT id FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND (LENGTH(:id) < LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id < id)) ORDER BY LENGTH(id) ASC, id ASC LIMIT 1"
)
abstract suspend fun getIdAbove(tuskyAccountId: Long, id: String): String?
/**
* Returns the ID directly below [id], or null if [id] is the ID of the bottom item
*/
@Query(
"SELECT id FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND (LENGTH(:id) > LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id > id)) ORDER BY LENGTH(id) DESC, id DESC LIMIT 1"
)
abstract suspend fun getIdBelow(tuskyAccountId: Long, id: String): String?
}

View file

@ -0,0 +1,56 @@
/* 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.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy.Companion.REPLACE
import androidx.room.Query
import com.keylesspalace.tusky.db.entity.TimelineAccountEntity
@Dao
abstract class TimelineAccountDao {
@Insert(onConflict = REPLACE)
abstract suspend fun insert(timelineAccountEntity: TimelineAccountEntity): Long
@Query(
"""SELECT * FROM TimelineAccountEntity a
WHERE a.serverId = :accountId
AND a.tuskyAccountId = :tuskyAccountId"""
)
internal abstract suspend fun getAccount(tuskyAccountId: Long, accountId: String): TimelineAccountEntity?
@Query("DELETE FROM TimelineAccountEntity WHERE tuskyAccountId = :tuskyAccountId")
abstract suspend fun removeAllAccounts(tuskyAccountId: Long)
/**
* Cleans the TimelineAccountEntity table from accounts that are no longer referenced by either TimelineStatusEntity, HomeTimelineEntity or NotificationEntity
* @param tuskyAccountId id of the user account for which to clean timeline accounts
*/
@Query(
"""DELETE FROM TimelineAccountEntity WHERE tuskyAccountId = :tuskyAccountId
AND serverId NOT IN
(SELECT authorServerId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId)
AND serverId NOT IN
(SELECT reblogAccountId FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND reblogAccountId IS NOT NULL)
AND serverId NOT IN
(SELECT accountId FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND accountId IS NOT NULL)
AND serverId NOT IN
(SELECT targetAccountId FROM NotificationReportEntity WHERE tuskyAccountId = :tuskyAccountId AND targetAccountId IS NOT NULL)"""
)
abstract suspend fun cleanupAccounts(tuskyAccountId: Long)
}

View file

@ -0,0 +1,169 @@
/* 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.dao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy.Companion.REPLACE
import androidx.room.Query
import com.keylesspalace.tusky.db.entity.HomeTimelineData
import com.keylesspalace.tusky.db.entity.HomeTimelineEntity
@Dao
abstract class TimelineDao {
@Insert(onConflict = REPLACE)
abstract suspend fun insertHomeTimelineItem(item: HomeTimelineEntity): Long
@Query(
"""
SELECT h.id, s.serverId, s.url, s.tuskyAccountId,
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, s.editedAt,
s.emojis, s.reblogsCount, s.favouritesCount, s.repliesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
s.spoilerText, s.visibility, s.mentions, s.tags, s.application,
s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned, s.language, s.filtered,
a.serverId as 'a_serverId', a.tuskyAccountId as 'a_tuskyAccountId',
a.localUsername as 'a_localUsername', a.username as 'a_username',
a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar',
a.emojis as 'a_emojis', a.bot as 'a_bot',
rb.serverId as 'rb_serverId', rb.tuskyAccountId 'rb_tuskyAccountId',
rb.localUsername as 'rb_localUsername', rb.username as 'rb_username',
rb.displayName as 'rb_displayName', rb.url as 'rb_url', rb.avatar as 'rb_avatar',
rb.emojis as 'rb_emojis', rb.bot as 'rb_bot',
h.loading
FROM HomeTimelineEntity h
LEFT JOIN TimelineStatusEntity s ON (h.statusId = s.serverId AND s.tuskyAccountId = :tuskyAccountId)
LEFT JOIN TimelineAccountEntity a ON (s.authorServerId = a.serverId AND a.tuskyAccountId = :tuskyAccountId)
LEFT JOIN TimelineAccountEntity rb ON (h.reblogAccountId = rb.serverId AND rb.tuskyAccountId = :tuskyAccountId)
WHERE h.tuskyAccountId = :tuskyAccountId
ORDER BY LENGTH(h.id) DESC, h.id DESC"""
)
abstract fun getHomeTimeline(tuskyAccountId: Long): PagingSource<Int, HomeTimelineData>
@Query(
"""DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND
(LENGTH(id) < LENGTH(:maxId) OR LENGTH(id) == LENGTH(:maxId) AND id <= :maxId)
AND
(LENGTH(id) > LENGTH(:minId) OR LENGTH(id) == LENGTH(:minId) AND id >= :minId)
"""
)
abstract suspend fun deleteRange(tuskyAccountId: Long, minId: String, maxId: String): Int
/**
* Remove all home timeline items that are statuses or reblogs by the user with id [userId], including reblogs from other people.
* (e.g. because user was blocked)
*/
@Query(
"""DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND
(statusId IN
(SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND authorServerId == :userId)
OR reblogAccountId == :userId)
"""
)
abstract suspend fun removeAllByUser(tuskyAccountId: Long, userId: String)
/**
* Remove all home timeline items that are statuses or reblogs by the user with id [userId], but not reblogs from other users.
* (e.g. because user was unfollowed)
*/
@Query(
"""DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND
((statusId IN
(SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND authorServerId == :userId)
AND reblogAccountId IS NULL)
OR reblogAccountId == :userId)
"""
)
abstract suspend fun removeStatusesAndReblogsByUser(tuskyAccountId: Long, userId: String)
@Query("DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId")
abstract suspend fun removeAllHomeTimelineItems(tuskyAccountId: Long)
@Query(
"""DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND id = :id"""
)
abstract suspend fun deleteHomeTimelineItem(tuskyAccountId: Long, id: String)
/**
* Deletes all hometimeline items that reference the status with it [statusId]. They can be regular statuses or reblogs.
*/
@Query(
"""DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId = :statusId"""
)
abstract suspend fun deleteAllWithStatus(tuskyAccountId: Long, statusId: String)
/**
* Trims the HomeTimelineEntity table down to [limit] entries by deleting the oldest in case there are more than [limit].
* @param tuskyAccountId id of the account for which to clean the home timeline
* @param limit how many timeline items to keep
*/
@Query(
"""DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND id NOT IN
(SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(id) DESC, id DESC LIMIT :limit)
"""
)
internal abstract suspend fun cleanupHomeTimeline(tuskyAccountId: Long, limit: Int)
@Query(
"""DELETE FROM HomeTimelineEntity
WHERE tuskyAccountId = :tuskyAccountId AND statusId IN (
SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND authorServerId in
( SELECT serverId FROM TimelineAccountEntity WHERE username LIKE '%@' || :instanceDomain
AND tuskyAccountId = :tuskyAccountId
))"""
)
abstract suspend fun deleteAllFromInstance(tuskyAccountId: Long, instanceDomain: String)
@Query(
"SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(id) DESC, id DESC LIMIT 1"
)
abstract suspend fun getTopId(tuskyAccountId: Long): String?
@Query(
"SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId IS NULL ORDER BY LENGTH(id) DESC, id DESC LIMIT 1"
)
abstract suspend fun getTopPlaceholderId(tuskyAccountId: Long): String?
/**
* Returns the id directly above [id], or null if [id] is the id of the top item
*/
@Query(
"SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND (LENGTH(:id) < LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id < id)) ORDER BY LENGTH(id) ASC, id ASC LIMIT 1"
)
abstract suspend fun getIdAbove(tuskyAccountId: Long, id: String): String?
/**
* Returns the ID directly below [id], or null if [id] is the ID of the bottom item
*/
@Query(
"SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND (LENGTH(:id) > LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id > id)) ORDER BY LENGTH(id) DESC, id DESC LIMIT 1"
)
abstract suspend fun getIdBelow(tuskyAccountId: Long, id: String): String?
@Query("SELECT COUNT(*) FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId")
abstract suspend fun getHomeTimelineItemCount(tuskyAccountId: Long): Int
/** Developer tools: Find N most recent status IDs */
@Query(
"SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(id) DESC, id DESC LIMIT :count"
)
abstract suspend fun getMostRecentNHomeTimelineIds(tuskyAccountId: Long, count: Int): List<String>
/** Developer tools: Convert a home timeline item to a placeholder */
@Query("UPDATE HomeTimelineEntity SET statusId = NULL, reblogAccountId = NULL WHERE id = :serverId")
abstract suspend fun convertHomeTimelineItemToPlaceholder(serverId: String)
}

View file

@ -0,0 +1,279 @@
/* 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.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy.Companion.REPLACE
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.TypeConverters
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.Converters
import com.keylesspalace.tusky.db.entity.TimelineAccountEntity
import com.keylesspalace.tusky.db.entity.TimelineStatusEntity
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Card
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
@Dao
abstract class TimelineStatusDao(
private val db: AppDatabase
) {
@Insert(onConflict = REPLACE)
abstract suspend fun insert(timelineStatusEntity: TimelineStatusEntity): Long
@Transaction
open suspend fun getStatusWithAccount(tuskyAccountId: Long, statusId: String): Pair<TimelineStatusEntity, TimelineAccountEntity>? {
val status = getStatus(tuskyAccountId, statusId) ?: return null
val account = db.timelineAccountDao().getAccount(tuskyAccountId, status.authorServerId) ?: return null
return status to account
}
@Query(
"""
SELECT * FROM TimelineStatusEntity s
WHERE s.serverId = :statusId
AND s.authorServerId IS NOT NULL
AND s.tuskyAccountId = :tuskyAccountId"""
)
abstract suspend fun getStatus(tuskyAccountId: Long, statusId: String): TimelineStatusEntity?
@OptIn(ExperimentalStdlibApi::class)
suspend fun update(tuskyAccountId: Long, status: Status, moshi: Moshi) {
update(
tuskyAccountId = tuskyAccountId,
statusId = status.id,
content = status.content,
editedAt = status.editedAt?.time,
emojis = moshi.adapter<List<Emoji>?>().toJson(status.emojis),
reblogsCount = status.reblogsCount,
favouritesCount = status.favouritesCount,
repliesCount = status.repliesCount,
reblogged = status.reblogged,
bookmarked = status.bookmarked,
favourited = status.favourited,
sensitive = status.sensitive,
spoilerText = status.spoilerText,
visibility = status.visibility,
attachments = moshi.adapter<List<Attachment>?>().toJson(status.attachments),
mentions = moshi.adapter<List<Status.Mention>?>().toJson(status.mentions),
tags = moshi.adapter<List<HashTag>?>().toJson(status.tags),
poll = moshi.adapter<Poll?>().toJson(status.poll),
muted = status.muted,
pinned = status.pinned,
card = moshi.adapter<Card?>().toJson(status.card),
language = status.language
)
}
@Query(
"""UPDATE TimelineStatusEntity
SET content = :content,
editedAt = :editedAt,
emojis = :emojis,
reblogsCount = :reblogsCount,
favouritesCount = :favouritesCount,
repliesCount = :repliesCount,
reblogged = :reblogged,
bookmarked = :bookmarked,
favourited = :favourited,
sensitive = :sensitive,
spoilerText = :spoilerText,
visibility = :visibility,
attachments = :attachments,
mentions = :mentions,
tags = :tags,
poll = :poll,
muted = :muted,
pinned = :pinned,
card = :card,
language = :language
WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId"""
)
@TypeConverters(Converters::class)
abstract suspend fun update(
tuskyAccountId: Long,
statusId: String,
content: String?,
editedAt: Long?,
emojis: String?,
reblogsCount: Int,
favouritesCount: Int,
repliesCount: Int,
reblogged: Boolean,
bookmarked: Boolean,
favourited: Boolean,
sensitive: Boolean,
spoilerText: String,
visibility: Status.Visibility,
attachments: String?,
mentions: String?,
tags: String?,
poll: String?,
muted: Boolean?,
pinned: Boolean,
card: String?,
language: String?
)
@Query(
"""UPDATE TimelineStatusEntity SET bookmarked = :bookmarked
WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId"""
)
abstract suspend fun setBookmarked(tuskyAccountId: Long, statusId: String, bookmarked: Boolean)
@Query(
"""UPDATE TimelineStatusEntity SET reblogged = :reblogged
WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId"""
)
abstract suspend fun setReblogged(tuskyAccountId: Long, statusId: String, reblogged: Boolean)
@Query("DELETE FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId")
abstract suspend fun removeAllStatuses(tuskyAccountId: Long)
@Query(
"""DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND id = :id"""
)
abstract suspend fun deleteHomeTimelineItem(tuskyAccountId: Long, id: String)
/**
* Deletes all hometimeline items that reference the status with it [statusId]. They can be regular statuses or reblogs.
*/
@Query(
"""DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId = :statusId"""
)
abstract suspend fun deleteAllWithStatus(tuskyAccountId: Long, statusId: String)
/**
* Cleans the TimelineStatusEntity table from unreferenced status entries.
* @param tuskyAccountId id of the account for which to clean statuses
*/
@Query(
"""DELETE FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId
AND serverId NOT IN
(SELECT statusId FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId IS NOT NULL)
AND serverId NOT IN
(SELECT statusId FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId IS NOT NULL)"""
)
internal abstract suspend fun cleanupStatuses(tuskyAccountId: Long)
@Query(
"""UPDATE TimelineStatusEntity SET poll = :poll
WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId"""
)
abstract suspend fun setVoted(tuskyAccountId: Long, statusId: String, poll: String)
@Query(
"""UPDATE TimelineStatusEntity SET expanded = :expanded
WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId"""
)
abstract suspend fun setExpanded(tuskyAccountId: Long, statusId: String, expanded: Boolean)
@Query(
"""UPDATE TimelineStatusEntity SET contentShowing = :contentShowing
WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId"""
)
abstract suspend fun setContentShowing(
tuskyAccountId: Long,
statusId: String,
contentShowing: Boolean
)
@Query(
"""UPDATE TimelineStatusEntity SET contentCollapsed = :contentCollapsed
WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId"""
)
abstract suspend fun setContentCollapsed(
tuskyAccountId: Long,
statusId: String,
contentCollapsed: Boolean
)
@Query(
"""UPDATE TimelineStatusEntity SET pinned = :pinned
WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId"""
)
abstract suspend fun setPinned(tuskyAccountId: Long, statusId: String, pinned: Boolean)
@Query(
"""DELETE FROM HomeTimelineEntity
WHERE tuskyAccountId = :tuskyAccountId AND statusId IN (
SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND authorServerId in
( SELECT serverId FROM TimelineAccountEntity WHERE username LIKE '%@' || :instanceDomain
AND tuskyAccountId = :tuskyAccountId
))"""
)
abstract suspend fun deleteAllFromInstance(tuskyAccountId: Long, instanceDomain: String)
@Query(
"UPDATE TimelineStatusEntity SET filtered = '[]' WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId"
)
abstract suspend fun clearWarning(tuskyAccountId: Long, statusId: String): Int
@Query(
"SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(id) DESC, id DESC LIMIT 1"
)
abstract suspend fun getTopId(tuskyAccountId: Long): String?
@Query(
"SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId IS NULL ORDER BY LENGTH(id) DESC, id DESC LIMIT 1"
)
abstract suspend fun getTopPlaceholderId(tuskyAccountId: Long): String?
/**
* Returns the id directly above [id], or null if [id] is the id of the top item
*/
@Query(
"SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND (LENGTH(:id) < LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id < id)) ORDER BY LENGTH(id) ASC, id ASC LIMIT 1"
)
abstract suspend fun getIdAbove(tuskyAccountId: Long, id: String): String?
/**
* Returns the ID directly below [id], or null if [id] is the ID of the bottom item
*/
@Query(
"SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND (LENGTH(:id) > LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id > id)) ORDER BY LENGTH(id) DESC, id DESC LIMIT 1"
)
abstract suspend fun getIdBelow(tuskyAccountId: Long, id: String): String?
/**
* Returns the id of the next placeholder after [id], or null if there is no placeholder.
*/
@Query(
"SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId IS NULL AND (LENGTH(:id) > LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id > id)) ORDER BY LENGTH(id) DESC, id DESC LIMIT 1"
)
abstract suspend fun getNextPlaceholderIdAfter(tuskyAccountId: Long, id: String): String?
@Query("SELECT COUNT(*) FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId")
abstract suspend fun getHomeTimelineItemCount(tuskyAccountId: Long): Int
/** Developer tools: Find N most recent status IDs */
@Query(
"SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(id) DESC, id DESC LIMIT :count"
)
abstract suspend fun getMostRecentNStatusIds(tuskyAccountId: Long, count: Int): List<String>
/** Developer tools: Convert a home timeline item to a placeholder */
@Query("UPDATE HomeTimelineEntity SET statusId = NULL, reblogAccountId = NULL WHERE id = :serverId")
abstract suspend fun convertStatusToPlaceholder(serverId: String)
}