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

@ -18,6 +18,8 @@ package com.keylesspalace.tusky.db
import android.content.Context
import android.util.Log
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.db.dao.AccountDao
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.PrefKeys

View file

@ -27,6 +27,21 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
import com.keylesspalace.tusky.TabDataKt;
import com.keylesspalace.tusky.components.conversation.ConversationEntity;
import com.keylesspalace.tusky.db.dao.AccountDao;
import com.keylesspalace.tusky.db.dao.DraftDao;
import com.keylesspalace.tusky.db.dao.InstanceDao;
import com.keylesspalace.tusky.db.dao.NotificationsDao;
import com.keylesspalace.tusky.db.dao.TimelineAccountDao;
import com.keylesspalace.tusky.db.dao.TimelineDao;
import com.keylesspalace.tusky.db.dao.TimelineStatusDao;
import com.keylesspalace.tusky.db.entity.AccountEntity;
import com.keylesspalace.tusky.db.entity.DraftEntity;
import com.keylesspalace.tusky.db.entity.HomeTimelineEntity;
import com.keylesspalace.tusky.db.entity.InstanceEntity;
import com.keylesspalace.tusky.db.entity.NotificationEntity;
import com.keylesspalace.tusky.db.entity.NotificationReportEntity;
import com.keylesspalace.tusky.db.entity.TimelineAccountEntity;
import com.keylesspalace.tusky.db.entity.TimelineStatusEntity;
import java.io.File;
@ -40,11 +55,14 @@ import java.io.File;
InstanceEntity.class,
TimelineStatusEntity.class,
TimelineAccountEntity.class,
ConversationEntity.class
ConversationEntity.class,
NotificationEntity.class,
NotificationReportEntity.class,
HomeTimelineEntity.class
},
// Note: Starting with version 54, database versions in Tusky are always even.
// This is to reserve odd version numbers for use by forks.
version = 58,
version = 60,
autoMigrations = {
@AutoMigration(from = 48, to = 49),
@AutoMigration(from = 49, to = 50, spec = AppDatabase.MIGRATION_49_50.class),
@ -61,6 +79,9 @@ public abstract class AppDatabase extends RoomDatabase {
@NonNull public abstract ConversationsDao conversationDao();
@NonNull public abstract TimelineDao timelineDao();
@NonNull public abstract DraftDao draftDao();
@NonNull public abstract NotificationsDao notificationsDao();
@NonNull public abstract TimelineStatusDao timelineStatusDao();
@NonNull public abstract TimelineAccountDao timelineAccountDao();
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
@ -698,4 +719,126 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `isShowHomeSelfBoosts` INTEGER NOT NULL DEFAULT 1");
}
};
public static final Migration MIGRATION_58_60 = new Migration(58, 60) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// drop the old tables - they are only caches anyway
database.execSQL("DROP TABLE `TimelineStatusEntity`");
database.execSQL("DROP TABLE `TimelineAccountEntity`");
// create the new tables
database.execSQL("""
CREATE TABLE IF NOT EXISTS `TimelineAccountEntity` (
`serverId` TEXT NOT NULL,
`tuskyAccountId` INTEGER NOT NULL,
`localUsername` TEXT NOT NULL,
`username` TEXT NOT NULL,
`displayName` TEXT NOT NULL,
`url` TEXT NOT NULL,
`avatar` TEXT NOT NULL,
`emojis` TEXT NOT NULL,
`bot` INTEGER NOT NULL,
PRIMARY KEY(`serverId`, `tuskyAccountId`)
)"""
);
database.execSQL("""
CREATE TABLE IF NOT EXISTS `TimelineStatusEntity` (
`serverId` TEXT NOT NULL,
`url` TEXT,
`tuskyAccountId` INTEGER NOT NULL,
`authorServerId` TEXT NOT NULL,
`inReplyToId` TEXT,
`inReplyToAccountId` TEXT,
`content` TEXT NOT NULL,
`createdAt` INTEGER NOT NULL,
`editedAt` INTEGER,
`emojis` TEXT NOT NULL,
`reblogsCount` INTEGER NOT NULL,
`favouritesCount` INTEGER NOT NULL,
`repliesCount` INTEGER NOT NULL,
`reblogged` INTEGER NOT NULL,
`bookmarked` INTEGER NOT NULL,
`favourited` INTEGER NOT NULL,
`sensitive` INTEGER NOT NULL,
`spoilerText` TEXT NOT NULL,
`visibility` INTEGER NOT NULL,
`attachments` TEXT NOT NULL,
`mentions` TEXT NOT NULL,
`tags` TEXT NOT NULL,
`application` TEXT,
`poll` TEXT,
`muted` INTEGER NOT NULL,
`expanded` INTEGER NOT NULL,
`contentCollapsed` INTEGER NOT NULL,
`contentShowing` INTEGER NOT NULL,
`pinned` INTEGER NOT NULL,
`card` TEXT, `language` TEXT,
`filtered` TEXT NOT NULL,
PRIMARY KEY(`serverId`, `tuskyAccountId`),
FOREIGN KEY(`authorServerId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION
)"""
);
database.execSQL(
"CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_tuskyAccountId` ON `TimelineStatusEntity` (`authorServerId`, `tuskyAccountId`)"
);
database.execSQL("""
CREATE TABLE IF NOT EXISTS `HomeTimelineEntity` (
`tuskyAccountId` INTEGER NOT NULL,
`id` TEXT NOT NULL,
`statusId` TEXT,
`reblogAccountId` TEXT,
`loading` INTEGER NOT NULL,
PRIMARY KEY(`id`, `tuskyAccountId`),
FOREIGN KEY(`statusId`, `tuskyAccountId`) REFERENCES `TimelineStatusEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION,
FOREIGN KEY(`reblogAccountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION
)"""
);
database.execSQL(
"CREATE INDEX IF NOT EXISTS `index_HomeTimelineEntity_statusId_tuskyAccountId` ON `HomeTimelineEntity` (`statusId`, `tuskyAccountId`)"
);
database.execSQL(
"CREATE INDEX IF NOT EXISTS `index_HomeTimelineEntity_reblogAccountId_tuskyAccountId` ON `HomeTimelineEntity` (`reblogAccountId`, `tuskyAccountId`)"
);
database.execSQL("""
CREATE TABLE IF NOT EXISTS `NotificationReportEntity`(
`tuskyAccountId` INTEGER NOT NULL,
`serverId` TEXT NOT NULL,
`category` TEXT NOT NULL,
`statusIds` TEXT,
`createdAt` INTEGER NOT NULL,
`targetAccountId` TEXT,
PRIMARY KEY(`serverId`, `tuskyAccountId`),
FOREIGN KEY(`targetAccountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION
)"""
);
database.execSQL(
"CREATE INDEX IF NOT EXISTS `index_NotificationReportEntity_targetAccountId_tuskyAccountId` ON `NotificationReportEntity` (`targetAccountId`, `tuskyAccountId`)"
);
database.execSQL("""
CREATE TABLE IF NOT EXISTS `NotificationEntity` (
`tuskyAccountId` INTEGER NOT NULL,
`type` TEXT,
`id` TEXT NOT NULL,
`accountId` TEXT,
`statusId` TEXT,
`reportId` TEXT,
`loading` INTEGER NOT NULL,
PRIMARY KEY(`id`, `tuskyAccountId`),
FOREIGN KEY(`accountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION,
FOREIGN KEY(`statusId`, `tuskyAccountId`) REFERENCES `TimelineStatusEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION,
FOREIGN KEY(`reportId`, `tuskyAccountId`) REFERENCES `NotificationReportEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION
)"""
);
database.execSQL(
"CREATE INDEX IF NOT EXISTS `index_NotificationEntity_accountId_tuskyAccountId` ON `NotificationEntity` (`accountId`, `tuskyAccountId`)"
);
database.execSQL(
"CREATE INDEX IF NOT EXISTS `index_NotificationEntity_statusId_tuskyAccountId` ON `NotificationEntity` (`statusId`, `tuskyAccountId`)"
);
database.execSQL(
"CREATE INDEX IF NOT EXISTS `index_NotificationEntity_reportId_tuskyAccountId` ON `NotificationEntity` (`reportId`, `tuskyAccountId`)"
);
}
};
}

View file

@ -20,6 +20,7 @@ import androidx.room.TypeConverter
import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
import com.keylesspalace.tusky.createTabDataFromId
import com.keylesspalace.tusky.db.entity.DraftAttachment
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Card
import com.keylesspalace.tusky.entity.Emoji
@ -187,4 +188,29 @@ class Converters @Inject constructor(
fun cardToJson(card: Card?): String {
return moshi.adapter<Card?>().toJson(card)
}
@TypeConverter
fun jsonToCard(cardJson: String?): Card? {
return cardJson?.let { moshi.adapter<Card?>().fromJson(cardJson) }
}
@TypeConverter
fun stringListToJson(list: List<String>?): String? {
return moshi.adapter<List<String>?>().toJson(list)
}
@TypeConverter
fun jsonToStringList(listJson: String?): List<String>? {
return listJson?.let { moshi.adapter<List<String>?>().fromJson(it) }
}
@TypeConverter
fun applicationToJson(application: Status.Application?): String {
return moshi.adapter<Status.Application?>().toJson(application)
}
@TypeConverter
fun jsonToApplication(applicationJson: String?): Status.Application? {
return applicationJson?.let { moshi.adapter<Status.Application?>().fromJson(it) }
}
}

View file

@ -0,0 +1,66 @@
/* 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
import androidx.room.withTransaction
import com.keylesspalace.tusky.db.entity.HomeTimelineEntity
import com.keylesspalace.tusky.db.entity.NotificationEntity
import com.keylesspalace.tusky.db.entity.NotificationReportEntity
import com.keylesspalace.tusky.db.entity.TimelineAccountEntity
import com.keylesspalace.tusky.db.entity.TimelineStatusEntity
import javax.inject.Inject
class DatabaseCleaner @Inject constructor(
private val db: AppDatabase
) {
/**
* Cleans the [HomeTimelineEntity], [TimelineStatusEntity], [TimelineAccountEntity], [NotificationEntity] and [NotificationReportEntity] tables from old entries.
* Should be regularly run to prevent the database from growing too big.
* @param tuskyAccountId id of the account for which to clean tables
* @param timelineLimit how many timeline items to keep
* @param notificationLimit how many notifications to keep
*/
suspend fun cleanupOldData(
tuskyAccountId: Long,
timelineLimit: Int,
notificationLimit: Int
) {
db.withTransaction {
// the order here is important - foreign key constraints must not be violated
db.notificationsDao().cleanupNotifications(tuskyAccountId, notificationLimit)
db.notificationsDao().cleanupReports(tuskyAccountId)
db.timelineDao().cleanupHomeTimeline(tuskyAccountId, timelineLimit)
db.timelineStatusDao().cleanupStatuses(tuskyAccountId)
db.timelineAccountDao().cleanupAccounts(tuskyAccountId)
}
}
/**
* Deletes everything from the [HomeTimelineEntity], [TimelineStatusEntity], [TimelineAccountEntity], [NotificationEntity] and [NotificationReportEntity] tables for one user.
* Intended to be used when a user logs out.
* @param tuskyAccountId id of the account for which to clean tables
*/
suspend fun cleanupEverything(tuskyAccountId: Long) {
db.withTransaction {
// the order here is important - foreign key constraints must not be violated
db.notificationsDao().removeAllNotifications(tuskyAccountId)
db.notificationsDao().removeAllReports(tuskyAccountId)
db.timelineDao().removeAllHomeTimelineItems(tuskyAccountId)
db.timelineStatusDao().removeAllStatuses(tuskyAccountId)
db.timelineAccountDao().removeAllAccounts(tuskyAccountId)
}
}
}

View file

@ -24,6 +24,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.drafts.DraftsActivity
import com.keylesspalace.tusky.db.dao.DraftDao
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.launch

View file

@ -1,346 +0,0 @@
/* 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
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy.Companion.REPLACE
import androidx.room.Query
import androidx.room.TypeConverters
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
@Dao
abstract class TimelineDao {
@Insert(onConflict = REPLACE)
abstract suspend fun insertAccount(timelineAccountEntity: TimelineAccountEntity): Long
@Insert(onConflict = REPLACE)
abstract suspend fun insertStatus(timelineStatusEntity: TimelineStatusEntity): Long
@Query(
"""
SELECT s.serverId, s.url, s.timelineUserId,
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.reblogServerId,s.reblogAccountId,
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.timelineUserId as 'a_timelineUserId',
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.timelineUserId 'rb_timelineUserId',
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'
FROM TimelineStatusEntity s
LEFT JOIN TimelineAccountEntity a ON (s.timelineUserId = a.timelineUserId AND s.authorServerId = a.serverId)
LEFT JOIN TimelineAccountEntity rb ON (s.timelineUserId = rb.timelineUserId AND s.reblogAccountId = rb.serverId)
WHERE s.timelineUserId = :account
ORDER BY LENGTH(s.serverId) DESC, s.serverId DESC"""
)
abstract fun getStatuses(account: Long): PagingSource<Int, TimelineStatusWithAccount>
@Query(
"""
SELECT s.serverId, s.url, s.timelineUserId,
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.reblogServerId,s.reblogAccountId,
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.timelineUserId as 'a_timelineUserId',
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.timelineUserId 'rb_timelineUserId',
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'
FROM TimelineStatusEntity s
LEFT JOIN TimelineAccountEntity a ON (s.timelineUserId = a.timelineUserId AND s.authorServerId = a.serverId)
LEFT JOIN TimelineAccountEntity rb ON (s.timelineUserId = rb.timelineUserId AND s.reblogAccountId = rb.serverId)
WHERE (s.serverId = :statusId OR s.reblogServerId = :statusId)
AND s.authorServerId IS NOT NULL
AND s.timelineUserId = :accountId"""
)
abstract suspend fun getStatus(accountId: Long, statusId: String): TimelineStatusWithAccount?
@Query(
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND
(LENGTH(serverId) < LENGTH(:maxId) OR LENGTH(serverId) == LENGTH(:maxId) AND serverId <= :maxId)
AND
(LENGTH(serverId) > LENGTH(:minId) OR LENGTH(serverId) == LENGTH(:minId) AND serverId >= :minId)
"""
)
abstract suspend fun deleteRange(accountId: Long, minId: String, maxId: String): Int
suspend fun update(accountId: Long, status: Status) {
update(
accountId = accountId,
statusId = status.id,
content = status.content,
editedAt = status.editedAt?.time,
emojis = 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 = status.attachments,
mentions = status.mentions,
tags = status.tags,
poll = status.poll,
muted = status.muted,
pinned = status.pinned,
card = 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 timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
)
@TypeConverters(Converters::class)
protected abstract suspend fun update(
accountId: Long,
statusId: String,
content: String?,
editedAt: Long?,
emojis: List<Emoji>,
reblogsCount: Int,
favouritesCount: Int,
repliesCount: Int,
reblogged: Boolean,
bookmarked: Boolean,
favourited: Boolean,
sensitive: Boolean,
spoilerText: String,
visibility: Status.Visibility,
attachments: List<Attachment>,
mentions: List<Status.Mention>,
tags: List<HashTag>?,
poll: Poll?,
muted: Boolean?,
pinned: Boolean,
card: Card?,
language: String?
)
@Query(
"""UPDATE TimelineStatusEntity SET bookmarked = :bookmarked
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
)
abstract suspend fun setBookmarked(accountId: Long, statusId: String, bookmarked: Boolean)
@Query(
"""UPDATE TimelineStatusEntity SET reblogged = :reblogged
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
)
abstract suspend fun setReblogged(accountId: Long, statusId: String, reblogged: Boolean)
@Query(
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND
(authorServerId = :userId OR reblogAccountId = :userId)"""
)
abstract suspend fun removeAllByUser(accountId: Long, userId: String)
/**
* Removes everything in the TimelineStatusEntity and TimelineAccountEntity tables for one user account
* @param accountId id of the account for which to clean tables
*/
suspend fun removeAll(accountId: Long) {
removeAllStatuses(accountId)
removeAllAccounts(accountId)
}
@Query("DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId")
abstract suspend fun removeAllStatuses(accountId: Long)
@Query("DELETE FROM TimelineAccountEntity WHERE timelineUserId = :accountId")
abstract suspend fun removeAllAccounts(accountId: Long)
@Query(
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId
AND serverId = :statusId"""
)
abstract suspend fun delete(accountId: Long, statusId: String)
/**
* Cleans the TimelineStatusEntity and TimelineAccountEntity tables from old entries.
* @param accountId id of the account for which to clean tables
* @param limit how many statuses to keep
*/
suspend fun cleanup(accountId: Long, limit: Int) {
cleanupStatuses(accountId, limit)
cleanupAccounts(accountId)
}
/**
* Cleans the TimelineStatusEntity table from old status entries.
* @param accountId id of the account for which to clean statuses
* @param limit how many statuses to keep
*/
@Query(
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND serverId NOT IN
(SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT :limit)
"""
)
abstract suspend fun cleanupStatuses(accountId: Long, limit: Int)
/**
* Cleans the TimelineAccountEntity table from accounts that are no longer referenced in the TimelineStatusEntity table
* @param accountId id of the user account for which to clean timeline accounts
*/
@Query(
"""DELETE FROM TimelineAccountEntity WHERE timelineUserId = :accountId AND serverId NOT IN
(SELECT authorServerId FROM TimelineStatusEntity WHERE timelineUserId = :accountId)
AND serverId NOT IN
(SELECT reblogAccountId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND reblogAccountId IS NOT NULL)"""
)
abstract suspend fun cleanupAccounts(accountId: Long)
@Query(
"""UPDATE TimelineStatusEntity SET poll = :poll
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
)
@TypeConverters(Converters::class)
abstract suspend fun setVoted(accountId: Long, statusId: String, poll: Poll)
@Query(
"""UPDATE TimelineStatusEntity SET expanded = :expanded
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
)
abstract suspend fun setExpanded(accountId: Long, statusId: String, expanded: Boolean)
@Query(
"""UPDATE TimelineStatusEntity SET contentShowing = :contentShowing
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
)
abstract suspend fun setContentShowing(
accountId: Long,
statusId: String,
contentShowing: Boolean
)
@Query(
"""UPDATE TimelineStatusEntity SET contentCollapsed = :contentCollapsed
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
)
abstract suspend fun setContentCollapsed(
accountId: Long,
statusId: String,
contentCollapsed: Boolean
)
@Query(
"""UPDATE TimelineStatusEntity SET pinned = :pinned
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
)
abstract suspend fun setPinned(accountId: Long, statusId: String, pinned: Boolean)
@Query(
"""DELETE FROM TimelineStatusEntity
WHERE timelineUserId = :accountId AND authorServerId IN (
SELECT serverId FROM TimelineAccountEntity WHERE username LIKE '%@' || :instanceDomain
AND timelineUserId = :accountId
)"""
)
abstract suspend fun deleteAllFromInstance(accountId: Long, instanceDomain: String)
@Query(
"UPDATE TimelineStatusEntity SET filtered = NULL WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"
)
abstract suspend fun clearWarning(accountId: Long, statusId: String): Int
@Query(
"SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1"
)
abstract suspend fun getTopId(accountId: Long): String?
@Query(
"SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1"
)
abstract suspend fun getTopPlaceholderId(accountId: Long): String?
/**
* Returns the id directly above [serverId], or null if [serverId] is the id of the top status
*/
@Query(
"SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND (LENGTH(:serverId) < LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId < serverId)) ORDER BY LENGTH(serverId) ASC, serverId ASC LIMIT 1"
)
abstract suspend fun getIdAbove(accountId: Long, serverId: String): String?
/**
* Returns the ID directly below [serverId], or null if [serverId] is the ID of the bottom
* status
*/
@Query(
"SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND (LENGTH(:serverId) > LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId > serverId)) ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1"
)
abstract suspend fun getIdBelow(accountId: Long, serverId: String): String?
/**
* Returns the id of the next placeholder after [serverId]
*/
@Query(
"SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL AND (LENGTH(:serverId) > LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId > serverId)) ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1"
)
abstract suspend fun getNextPlaceholderIdAfter(accountId: Long, serverId: String): String?
@Query("SELECT COUNT(*) FROM TimelineStatusEntity WHERE timelineUserId = :accountId")
abstract suspend fun getStatusCount(accountId: Long): Int
/** Developer tools: Find N most recent status IDs */
@Query(
"SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT :count"
)
abstract suspend fun getMostRecentNStatusIds(accountId: Long, count: Int): List<String>
/** Developer tools: Convert a status to a placeholder */
@Query("UPDATE TimelineStatusEntity SET authorServerId = NULL WHERE serverId = :serverId")
abstract suspend fun convertStatustoPlaceholder(serverId: String)
}

View file

@ -13,13 +13,14 @@
* 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
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 {

View file

@ -13,13 +13,14 @@
* 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
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

View file

@ -13,12 +13,15 @@
* 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
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 {

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)
}

View file

@ -13,7 +13,7 @@
* 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
package com.keylesspalace.tusky.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
@ -21,6 +21,7 @@ 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

View file

@ -13,7 +13,7 @@
* 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
package com.keylesspalace.tusky.db.entity
import android.net.Uri
import android.os.Parcelable
@ -21,6 +21,7 @@ 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

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

@ -13,11 +13,12 @@
* 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
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

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

@ -13,40 +13,38 @@
* 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
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.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
/**
* We're trying to play smart here. Server sends us reblogs as two entities one embedded into
* another (reblogged status is a field inside of "reblog" status). But it's really inefficient from
* the DB perspective and doesn't matter much for the display/interaction purposes.
* What if when we store reblog we don't store almost empty "reblog status" but we store
* *reblogged* status and we embed "reblog status" into reblogged status. This reversed
* relationship takes much less space and is much faster to fetch (no N+1 type queries or JSON
* serialization).
* "Reblog status", if present, is marked by [reblogServerId], and [reblogAccountId]
* fields.
* 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", "timelineUserId"],
primaryKeys = ["serverId", "tuskyAccountId"],
foreignKeys = (
[
ForeignKey(
entity = TimelineAccountEntity::class,
parentColumns = ["serverId", "timelineUserId"],
childColumns = ["authorServerId", "timelineUserId"]
parentColumns = ["serverId", "tuskyAccountId"],
childColumns = ["authorServerId", "tuskyAccountId"]
)
]
),
// Avoiding rescanning status table when accounts table changes. Recommended by Room(c).
indices = [Index("authorServerId", "timelineUserId")]
indices = [Index("authorServerId", "tuskyAccountId")]
)
@TypeConverters(Converters::class)
data class TimelineStatusEntity(
@ -54,14 +52,14 @@ data class TimelineStatusEntity(
val serverId: String,
val url: String?,
// our local id for the logged in user in case there are multiple accounts per instance
val timelineUserId: Long,
val authorServerId: String?,
val tuskyAccountId: Long,
val authorServerId: String,
val inReplyToId: String?,
val inReplyToAccountId: String?,
val content: String?,
val content: String,
val createdAt: Long,
val editedAt: Long?,
val emojis: String?,
val emojis: List<Emoji>,
val reblogsCount: Int,
val favouritesCount: Int,
val repliesCount: Int,
@ -71,50 +69,19 @@ data class TimelineStatusEntity(
val sensitive: Boolean,
val spoilerText: String,
val visibility: Status.Visibility,
val attachments: String?,
val mentions: String?,
val tags: String?,
val application: String?,
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 reblogServerId: String?,
val reblogAccountId: String?,
val poll: String?,
val muted: Boolean?,
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: String?,
val card: Card?,
val language: String?,
val filtered: List<FilterResult>?
) {
val isPlaceholder: Boolean
get() = this.authorServerId == null
}
@Entity(
primaryKeys = ["serverId", "timelineUserId"]
)
data class TimelineAccountEntity(
val serverId: String,
val timelineUserId: Long,
val localUsername: String,
val username: String,
val displayName: String,
val url: String,
val avatar: String,
val emojis: String,
val bot: Boolean
)
data class TimelineStatusWithAccount(
@Embedded
val status: TimelineStatusEntity,
// null when placeholder
@Embedded(prefix = "a_")
val account: TimelineAccountEntity? = null,
// null when no reblog
@Embedded(prefix = "rb_")
val reblogAccount: TimelineAccountEntity? = null
val filtered: List<FilterResult>
)