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:
parent
3bbf96b057
commit
b2c0b18c8e
121 changed files with 6992 additions and 4654 deletions
|
|
@ -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>
|
||||
}
|
||||
51
app/src/main/java/com/keylesspalace/tusky/db/dao/DraftDao.kt
Normal file
51
app/src/main/java/com/keylesspalace/tusky/db/dao/DraftDao.kt
Normal 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?
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
169
app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineDao.kt
Normal file
169
app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineDao.kt
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue