Release chat folders to internal users.

This commit is contained in:
Michelle Tang
2024-10-11 09:38:53 -07:00
committed by Greyson Parrelli
parent e5c122d972
commit c4fc32988c
64 changed files with 3166 additions and 251 deletions

View File

@@ -0,0 +1,321 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import androidx.core.content.contentValuesOf
import org.signal.core.util.SqlUtil
import org.signal.core.util.delete
import org.signal.core.util.groupBy
import org.signal.core.util.insertInto
import org.signal.core.util.readToList
import org.signal.core.util.readToSingleInt
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
/**
* Stores chat folders and the chats that belong in each chat folder
*/
class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : DatabaseTable(context, databaseHelper), ThreadIdDatabaseReference {
companion object {
@JvmField
val CREATE_TABLE: Array<String> = arrayOf(ChatFolderTable.CREATE_TABLE, ChatFolderMembershipTable.CREATE_TABLE)
@JvmField
val CREATE_INDEXES: Array<String> = ChatFolderTable.CREATE_INDEX + ChatFolderMembershipTable.CREATE_INDEXES
fun insertInitialChatFoldersAtCreationTime(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
db.insert(ChatFolderTable.TABLE_NAME, null, getAllChatsFolderContentValues())
}
private fun getAllChatsFolderContentValues(): ContentValues {
return contentValuesOf(
ChatFolderTable.POSITION to 0,
ChatFolderTable.FOLDER_TYPE to ChatFolderRecord.FolderType.ALL.value,
ChatFolderTable.SHOW_INDIVIDUAL to 1,
ChatFolderTable.SHOW_GROUPS to 1,
ChatFolderTable.SHOW_MUTED to 1
)
}
}
/**
* Represents the components of a chat folder and any chat types it contains
*/
object ChatFolderTable {
const val TABLE_NAME = "chat_folder"
const val ID = "_id"
const val NAME = "name"
const val POSITION = "position"
const val SHOW_UNREAD = "show_unread"
const val SHOW_MUTED = "show_muted"
const val SHOW_INDIVIDUAL = "show_individual"
const val SHOW_GROUPS = "show_groups"
const val IS_MUTED = "is_muted"
const val FOLDER_TYPE = "folder_type"
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
$NAME TEXT DEFAULT NULL,
$POSITION INTEGER DEFAULT 0,
$SHOW_UNREAD INTEGER DEFAULT 0,
$SHOW_MUTED INTEGER DEFAULT 0,
$SHOW_INDIVIDUAL INTEGER DEFAULT 0,
$SHOW_GROUPS INTEGER DEFAULT 0,
$IS_MUTED INTEGER DEFAULT 0,
$FOLDER_TYPE INTEGER DEFAULT ${ChatFolderRecord.FolderType.CUSTOM.value}
)
"""
val CREATE_INDEX = arrayOf(
"CREATE INDEX chat_folder_position_index ON $TABLE_NAME ($POSITION)"
)
}
/**
* Represents a thread that is associated with this chat folder. They are
* either included in the chat folder or explicitly excluded.
*/
object ChatFolderMembershipTable {
const val TABLE_NAME = "chat_folder_membership"
const val ID = "_id"
const val CHAT_FOLDER_ID = "chat_folder_id"
const val THREAD_ID = "thread_id"
const val MEMBERSHIP_TYPE = "membership_type"
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
$CHAT_FOLDER_ID INTEGER NOT NULL REFERENCES ${ChatFolderTable.TABLE_NAME} (${ChatFolderTable.ID}) ON DELETE CASCADE,
$THREAD_ID INTEGER NOT NULL REFERENCES ${ThreadTable.TABLE_NAME} (${ThreadTable.ID}) ON DELETE CASCADE,
$MEMBERSHIP_TYPE INTEGER DEFAULT 1
)
"""
val CREATE_INDEXES = arrayOf(
"CREATE INDEX chat_folder_membership_chat_folder_id_index ON $TABLE_NAME ($CHAT_FOLDER_ID)",
"CREATE INDEX chat_folder_membership_thread_id_index ON $TABLE_NAME ($THREAD_ID)",
"CREATE INDEX chat_folder_membership_membership_type_index ON $TABLE_NAME ($MEMBERSHIP_TYPE)"
)
}
override fun remapThread(fromId: Long, toId: Long) {
writableDatabase
.update(ChatFolderMembershipTable.TABLE_NAME)
.values(ChatFolderMembershipTable.THREAD_ID to toId)
.where("${ChatFolderMembershipTable.THREAD_ID} = ?", fromId)
.run()
}
/**
* Maps the chat folder ids to its corresponding chat folder
*/
fun getChatFolders(includeUnreads: Boolean = false): List<ChatFolderRecord> {
val includedChats: Map<Long, List<Long>> = getIncludedChats()
val excludedChats: Map<Long, List<Long>> = getExcludedChats()
val folders = readableDatabase
.select()
.from(ChatFolderTable.TABLE_NAME)
.orderBy(ChatFolderTable.POSITION)
.run()
.readToList { cursor ->
val id = cursor.requireLong(ChatFolderTable.ID)
ChatFolderRecord(
id = id,
name = cursor.requireString(ChatFolderTable.NAME) ?: "",
position = cursor.requireInt(ChatFolderTable.POSITION),
showUnread = cursor.requireBoolean(ChatFolderTable.SHOW_UNREAD),
showMutedChats = cursor.requireBoolean(ChatFolderTable.SHOW_MUTED),
showIndividualChats = cursor.requireBoolean(ChatFolderTable.SHOW_INDIVIDUAL),
showGroupChats = cursor.requireBoolean(ChatFolderTable.SHOW_GROUPS),
isMuted = cursor.requireBoolean(ChatFolderTable.IS_MUTED),
folderType = ChatFolderRecord.FolderType.deserialize(cursor.requireInt(ChatFolderTable.FOLDER_TYPE)),
includedChats = includedChats[id] ?: emptyList(),
excludedChats = excludedChats[id] ?: emptyList()
)
}
if (includeUnreads) {
return folders.map { folder ->
folder.copy(
unreadCount = SignalDatabase.threads.getUnreadCountByChatFolder(folder)
)
}
}
return folders
}
/**
* Maps chat folder ids to all of its corresponding included chats
*/
private fun getIncludedChats(): Map<Long, List<Long>> {
return readableDatabase
.select()
.from(ChatFolderMembershipTable.TABLE_NAME)
.where("${ChatFolderMembershipTable.MEMBERSHIP_TYPE} = ${MembershipType.INCLUDED.value}")
.run()
.groupBy { cursor ->
cursor.requireLong(ChatFolderMembershipTable.CHAT_FOLDER_ID) to cursor.requireLong(ChatFolderMembershipTable.THREAD_ID)
}
}
/**
* Maps the chat folder ids to all of its corresponding excluded chats
*/
private fun getExcludedChats(): Map<Long, List<Long>> {
return readableDatabase
.select()
.from(ChatFolderMembershipTable.TABLE_NAME)
.where("${ChatFolderMembershipTable.MEMBERSHIP_TYPE} = ${MembershipType.EXCLUDED.value}")
.run()
.groupBy { cursor ->
cursor.requireLong(ChatFolderMembershipTable.CHAT_FOLDER_ID) to cursor.requireLong(ChatFolderMembershipTable.THREAD_ID)
}
}
/**
* Adds a chat folder and its corresponding included/excluded chats
*/
fun createFolder(chatFolder: ChatFolderRecord) {
writableDatabase.withinTransaction { db ->
val position: Int = db
.select("MAX(${ChatFolderTable.POSITION})")
.from(ChatFolderTable.TABLE_NAME)
.run()
.readToSingleInt(0) + 1
val id = db.insertInto(ChatFolderTable.TABLE_NAME)
.values(
contentValuesOf(
ChatFolderTable.NAME to chatFolder.name,
ChatFolderTable.SHOW_UNREAD to chatFolder.showUnread,
ChatFolderTable.SHOW_MUTED to chatFolder.showMutedChats,
ChatFolderTable.SHOW_INDIVIDUAL to chatFolder.showIndividualChats,
ChatFolderTable.SHOW_GROUPS to chatFolder.showGroupChats,
ChatFolderTable.IS_MUTED to chatFolder.isMuted,
ChatFolderTable.POSITION to position
)
)
.run(SQLiteDatabase.CONFLICT_IGNORE)
val includedChatsQueries = SqlUtil.buildBulkInsert(
ChatFolderMembershipTable.TABLE_NAME,
arrayOf(ChatFolderMembershipTable.CHAT_FOLDER_ID, ChatFolderMembershipTable.THREAD_ID, ChatFolderMembershipTable.MEMBERSHIP_TYPE),
chatFolder.includedChats.toContentValues(chatFolderId = id, membershipType = MembershipType.INCLUDED)
)
val excludedChatsQueries = SqlUtil.buildBulkInsert(
ChatFolderMembershipTable.TABLE_NAME,
arrayOf(ChatFolderMembershipTable.CHAT_FOLDER_ID, ChatFolderMembershipTable.THREAD_ID, ChatFolderMembershipTable.MEMBERSHIP_TYPE),
chatFolder.excludedChats.toContentValues(chatFolderId = id, membershipType = MembershipType.EXCLUDED)
)
includedChatsQueries.forEach {
db.execSQL(it.where, it.whereArgs)
}
excludedChatsQueries.forEach {
db.execSQL(it.where, it.whereArgs)
}
AppDependencies.databaseObserver.notifyChatFolderObservers()
}
}
/**
* Updates the details for an existing folder like name, chat types, etc.
*/
fun updateFolder(chatFolder: ChatFolderRecord) {
writableDatabase.withinTransaction { db ->
db.update(ChatFolderTable.TABLE_NAME)
.values(
ChatFolderTable.NAME to chatFolder.name,
ChatFolderTable.SHOW_UNREAD to chatFolder.showUnread,
ChatFolderTable.SHOW_MUTED to chatFolder.showMutedChats,
ChatFolderTable.SHOW_INDIVIDUAL to chatFolder.showIndividualChats,
ChatFolderTable.SHOW_GROUPS to chatFolder.showGroupChats,
ChatFolderTable.IS_MUTED to chatFolder.isMuted
)
.where("${ChatFolderTable.ID} = ?", chatFolder.id)
.run(SQLiteDatabase.CONFLICT_IGNORE)
db
.delete(ChatFolderMembershipTable.TABLE_NAME)
.where("${ChatFolderMembershipTable.CHAT_FOLDER_ID} = ?", chatFolder.id)
.run()
val includedChats = SqlUtil.buildBulkInsert(
ChatFolderMembershipTable.TABLE_NAME,
arrayOf(ChatFolderMembershipTable.CHAT_FOLDER_ID, ChatFolderMembershipTable.THREAD_ID, ChatFolderMembershipTable.MEMBERSHIP_TYPE),
chatFolder.includedChats.toContentValues(chatFolderId = chatFolder.id, membershipType = MembershipType.INCLUDED)
)
val excludedChats = SqlUtil.buildBulkInsert(
ChatFolderMembershipTable.TABLE_NAME,
arrayOf(ChatFolderMembershipTable.CHAT_FOLDER_ID, ChatFolderMembershipTable.THREAD_ID, ChatFolderMembershipTable.MEMBERSHIP_TYPE),
chatFolder.excludedChats.toContentValues(chatFolderId = chatFolder.id, membershipType = MembershipType.EXCLUDED)
)
(includedChats + excludedChats).forEach {
db.execSQL(it.where, it.whereArgs)
}
AppDependencies.databaseObserver.notifyChatFolderObservers()
}
}
/**
* Deletes a chat folder
*/
fun deleteChatFolder(chatFolder: ChatFolderRecord) {
writableDatabase.withinTransaction { db ->
db.delete(ChatFolderTable.TABLE_NAME, "${ChatFolderTable.ID} = ?", SqlUtil.buildArgs(chatFolder.id))
AppDependencies.databaseObserver.notifyChatFolderObservers()
}
}
/**
* Updates the position of the chat folders
*/
fun updatePositions(folders: List<ChatFolderRecord>) {
writableDatabase.withinTransaction { db ->
folders.forEach { folder ->
db.update(ChatFolderTable.TABLE_NAME)
.values(ChatFolderTable.POSITION to folder.position)
.where("${ChatFolderTable.ID} = ?", folder.id)
.run(SQLiteDatabase.CONFLICT_IGNORE)
}
AppDependencies.databaseObserver.notifyChatFolderObservers()
}
}
private fun Collection<Long>.toContentValues(chatFolderId: Long, membershipType: MembershipType): List<ContentValues> {
return map {
contentValuesOf(
ChatFolderMembershipTable.CHAT_FOLDER_ID to chatFolderId,
ChatFolderMembershipTable.THREAD_ID to it,
ChatFolderMembershipTable.MEMBERSHIP_TYPE to membershipType.value
)
}
}
enum class MembershipType(val value: Int) {
/** Chat that should be included in the chat folder */
INCLUDED(0),
/** Chat that should be excluded from the chat folder */
EXCLUDED(1)
}
}

View File

@@ -47,6 +47,7 @@ public class DatabaseObserver {
private static final String KEY_CALL_UPDATES = "CallUpdates";
private static final String KEY_CALL_LINK_UPDATES = "CallLinkUpdates";
private static final String KEY_IN_APP_PAYMENTS = "InAppPayments";
private static final String KEY_CHAT_FOLDER = "ChatFolder";
private final Executor executor;
@@ -69,6 +70,7 @@ public class DatabaseObserver {
private final Set<Observer> callUpdateObservers;
private final Map<CallLinkRoomId, Set<Observer>> callLinkObservers;
private final Set<InAppPaymentObserver> inAppPaymentObservers;
private final Set<Observer> chatFolderObservers;
public DatabaseObserver() {
this.executor = new SerialExecutor(SignalExecutors.BOUNDED);
@@ -91,6 +93,7 @@ public class DatabaseObserver {
this.callUpdateObservers = new HashSet<>();
this.callLinkObservers = new HashMap<>();
this.inAppPaymentObservers = new HashSet<>();
this.chatFolderObservers = new HashSet<>();
}
public void registerConversationListObserver(@NonNull Observer listener) {
@@ -206,6 +209,10 @@ public class DatabaseObserver {
executor.execute(() -> inAppPaymentObservers.add(observer));
}
public void registerChatFolderObserver(@NonNull Observer observer) {
executor.execute(() -> chatFolderObservers.add(observer));
}
public void unregisterObserver(@NonNull Observer listener) {
executor.execute(() -> {
conversationListObservers.remove(listener);
@@ -223,6 +230,7 @@ public class DatabaseObserver {
unregisterMapped(conversationDeleteObservers, listener);
callUpdateObservers.remove(listener);
unregisterMapped(callLinkObservers, listener);
chatFolderObservers.remove(listener);
});
}
@@ -387,6 +395,10 @@ public class DatabaseObserver {
});
}
public void notifyChatFolderObservers() {
runPostSuccessfulTransaction(KEY_CHAT_FOLDER, () -> notifySet(chatFolderObservers));
}
private void runPostSuccessfulTransaction(@NonNull String dedupeKey, @NonNull Runnable runnable) {
SignalDatabase.runPostSuccessfulTransaction(dedupeKey, () -> {
executor.execute(runnable);

View File

@@ -1483,6 +1483,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
for (id in ids) {
AppDependencies.databaseObserver.notifyRecipientChanged(id)
}
AppDependencies.databaseObserver.notifyConversationListListeners()
StorageSyncHelper.scheduleSyncForDataChange()
}

View File

@@ -15,6 +15,7 @@ object RxDatabaseObserver {
val conversationList: Flowable<Unit> by lazy { conversationListFlowable() }
val notificationProfiles: Flowable<Unit> by lazy { notificationProfilesFlowable() }
val chatFolders: Flowable<Unit> by lazy { chatFoldersFlowable() }
private fun conversationListFlowable(): Flowable<Unit> {
return databaseFlowable { listener ->
@@ -36,6 +37,12 @@ object RxDatabaseObserver {
) { _, _ -> Unit }
}
private fun chatFoldersFlowable(): Flowable<Unit> {
return databaseFlowable { listener ->
AppDependencies.databaseObserver.registerChatFolderObserver(listener)
}
}
private fun databaseFlowable(registerObserver: (RxObserver) -> Unit): Flowable<Unit> {
val flowable = Flowable.create(
{

View File

@@ -76,6 +76,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val nameCollisionTables: NameCollisionTables = NameCollisionTables(context, this)
val inAppPaymentTable: InAppPaymentTable = InAppPaymentTable(context, this)
val inAppPaymentSubscriberTable: InAppPaymentSubscriberTable = InAppPaymentSubscriberTable(context, this)
val chatFoldersTable: ChatFolderTables = ChatFolderTables(context, this)
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true)
@@ -120,6 +121,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
executeStatements(db, MessageSendLogTables.CREATE_TABLE)
executeStatements(db, NotificationProfileDatabase.CREATE_TABLE)
executeStatements(db, DistributionListTables.CREATE_TABLE)
executeStatements(db, ChatFolderTables.CREATE_TABLE)
executeStatements(db, RecipientTable.CREATE_INDEXS)
executeStatements(db, MessageTable.CREATE_INDEXS)
@@ -141,6 +143,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
executeStatements(db, CallTable.CREATE_INDEXES)
executeStatements(db, ReactionTable.CREATE_INDEXES)
executeStatements(db, KyberPreKeyTable.CREATE_INDEXES)
executeStatements(db, ChatFolderTables.CREATE_INDEXES)
executeStatements(db, SearchTable.CREATE_TRIGGERS)
executeStatements(db, MessageSendLogTables.CREATE_TRIGGERS)
@@ -148,6 +151,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
NameCollisionTables.createIndexes(db)
DistributionListTables.insertInitialDistributionListAtCreationTime(db)
ChatFolderTables.insertInitialChatFoldersAtCreationTime(db)
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
val legacyHelper = ClassicOpenHelper(context)
@@ -558,5 +562,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
@get:JvmName("inAppPaymentSubscribers")
val inAppPaymentSubscribers: InAppPaymentSubscriberTable
get() = instance!!.inAppPaymentSubscriberTable
@get:JvmStatic
@get:JvmName("chatFolders")
val chatFolders: ChatFolderTables
get() = instance!!.chatFoldersTable
}
}

View File

@@ -18,6 +18,7 @@ import org.signal.core.util.exists
import org.signal.core.util.logging.Log
import org.signal.core.util.or
import org.signal.core.util.readToList
import org.signal.core.util.readToSingleInt
import org.signal.core.util.readToSingleLong
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
@@ -30,6 +31,7 @@ import org.signal.core.util.updateAll
import org.signal.core.util.withinTransaction
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.attachments
@@ -629,6 +631,39 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
return allCount + forcedUnreadCount
}
/**
* Returns the number of unread messages across all threads within a chat folder
* Threads that are forced-unread count as 1.
*/
fun getUnreadCountByChatFolder(folder: ChatFolderRecord): Int {
val chatFolderQuery = folder.toQuery()
val allCountQuery =
"""
SELECT SUM($UNREAD_COUNT)
FROM $TABLE_NAME
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
WHERE
$ARCHIVED = 0
$chatFolderQuery
"""
val allCount = readableDatabase.rawQuery(allCountQuery, null).readToSingleInt(0)
val forcedUnreadCountQuery =
"""
SELECT COUNT(*)
FROM $TABLE_NAME
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
WHERE
$ARCHIVED = 0 AND
$READ = ${ThreadTable.ReadStatus.FORCED_UNREAD.serialize()}
$chatFolderQuery
"""
val forcedUnreadCount = readableDatabase.rawQuery(forcedUnreadCountQuery, null).readToSingleInt(0)
return allCount + forcedUnreadCount
}
/**
* Returns the number of unread messages in a given thread.
*/
@@ -915,12 +950,13 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
return readableDatabase.rawQuery(query, arrayOf("1"))
}
fun getUnarchivedConversationList(conversationFilter: ConversationFilter, pinned: Boolean, offset: Long, limit: Long): Cursor {
fun getUnarchivedConversationList(conversationFilter: ConversationFilter, pinned: Boolean, offset: Long, limit: Long, chatFolder: ChatFolderRecord): Cursor {
val folderQuery = chatFolder.toQuery()
val filterQuery = conversationFilter.toQuery()
val where = if (pinned) {
"$ARCHIVED = 0 AND $PINNED != 0 $filterQuery"
"$ARCHIVED = 0 AND $PINNED != 0 $filterQuery $folderQuery"
} else {
"$ARCHIVED = 0 AND $PINNED = 0 AND $MEANINGFUL_MESSAGES != 0 $filterQuery"
"$ARCHIVED = 0 AND $PINNED = 0 AND $MEANINGFUL_MESSAGES != 0 $filterQuery $folderQuery"
}
val query = if (pinned) {
@@ -948,36 +984,61 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
}
}
fun getPinnedConversationListCount(conversationFilter: ConversationFilter): Int {
fun getPinnedConversationListCount(conversationFilter: ConversationFilter, chatFolder: ChatFolderRecord? = null): Int {
val filterQuery = conversationFilter.toQuery()
return readableDatabase
.select("COUNT(*)")
.from(TABLE_NAME)
.where("$ACTIVE = 1 AND $ARCHIVED = 0 AND $PINNED != 0 $filterQuery")
.run()
.use { cursor ->
if (cursor.moveToFirst()) {
cursor.getInt(0)
} else {
0
}
}
return if (chatFolder == null || chatFolder.folderType == ChatFolderRecord.FolderType.ALL) {
readableDatabase
.select("COUNT(*)")
.from(TABLE_NAME)
.where("$ACTIVE = 1 AND $ARCHIVED = 0 AND $PINNED != 0 $filterQuery")
.run()
.readToSingleInt(0)
} else {
val folderQuery = chatFolder.toQuery()
val query =
"""
SELECT COUNT(*)
FROM $TABLE_NAME
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
WHERE
$ACTIVE = 1 AND
$ARCHIVED = 0 AND
$PINNED != 0
$filterQuery
$folderQuery
"""
readableDatabase.rawQuery(query, null).readToSingleInt(0)
}
}
fun getUnarchivedConversationListCount(conversationFilter: ConversationFilter): Int {
fun getUnarchivedConversationListCount(conversationFilter: ConversationFilter, chatFolder: ChatFolderRecord? = null): Int {
val filterQuery = conversationFilter.toQuery()
return readableDatabase
.select("COUNT(*)")
.from(TABLE_NAME)
.where("$ACTIVE = 1 AND $ARCHIVED = 0 AND ($MEANINGFUL_MESSAGES != 0 OR $PINNED != 0) $filterQuery")
.run()
.use { cursor ->
if (cursor.moveToFirst()) {
cursor.getInt(0)
} else {
0
}
}
return if (chatFolder == null || chatFolder.folderType == ChatFolderRecord.FolderType.ALL) {
readableDatabase
.select("COUNT(*)")
.from(TABLE_NAME)
.where("$ACTIVE = 1 AND $ARCHIVED = 0 AND ($MEANINGFUL_MESSAGES != 0 OR $PINNED != 0) $filterQuery")
.run()
.readToSingleInt(0)
} else {
val folderQuery = chatFolder.toQuery()
val query =
"""
SELECT COUNT(*)
FROM $TABLE_NAME
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
WHERE
$ACTIVE = 1 AND
$ARCHIVED = 0 AND
($MEANINGFUL_MESSAGES != 0 OR $PINNED != 0)
$filterQuery
$folderQuery
"""
readableDatabase.rawQuery(query, null).readToSingleInt(0)
}
}
/**
@@ -1979,6 +2040,42 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
return Reader(cursor)
}
private fun ChatFolderRecord.toQuery(): String {
if (this.id == -1L || this.folderType == ChatFolderRecord.FolderType.ALL) {
return ""
}
val includedChatsQuery: MutableList<String> = mutableListOf()
includedChatsQuery.add("${TABLE_NAME}.$ID IN (${this.includedChats.joinToString(",")})")
if (this.showIndividualChats) {
includedChatsQuery.add("${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE} = ${RecipientTable.RecipientType.INDIVIDUAL.id}")
}
if (this.showGroupChats) {
includedChatsQuery.add("${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE} = ${RecipientTable.RecipientType.GV2.id}")
}
val includedQuery = includedChatsQuery.joinToString(" OR ") { "($it)" }
val fullQuery: MutableList<String> = mutableListOf()
fullQuery.add(includedQuery)
if (this.excludedChats.isNotEmpty()) {
fullQuery.add("${TABLE_NAME}.$ID NOT IN (${this.excludedChats.joinToString(",")})")
}
if (this.showUnread) {
fullQuery.add("$UNREAD_COUNT > 0 OR $READ == ${ReadStatus.FORCED_UNREAD.serialize()}")
}
if (!this.showMutedChats) {
fullQuery.add("${RecipientTable.TABLE_NAME}.${RecipientTable.MUTE_UNTIL} = 0")
}
return "AND ${fullQuery.joinToString(" AND ") { "($it)" }}"
}
private fun ConversationFilter.toQuery(): String {
return when (this) {
ConversationFilter.OFF -> ""

View File

@@ -109,6 +109,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V247_ClearUploadTim
import org.thoughtcrime.securesms.database.helpers.migration.V250_ClearUploadTimestampV2
import org.thoughtcrime.securesms.database.helpers.migration.V251_ArchiveTransferStateIndex
import org.thoughtcrime.securesms.database.helpers.migration.V252_AttachmentOffloadRestoredAtColumn
import org.thoughtcrime.securesms.database.helpers.migration.V253_CreateChatFolderTables
/**
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
@@ -220,10 +221,11 @@ object SignalDatabaseMigrations {
// 248 and 249 were originally in 7.18.0, but are now skipped because we needed to hotfix 7.17.6 after 7.18.0 was already released.
250 to V250_ClearUploadTimestampV2,
251 to V251_ArchiveTransferStateIndex,
252 to V252_AttachmentOffloadRestoredAtColumn
252 to V252_AttachmentOffloadRestoredAtColumn,
253 to V253_CreateChatFolderTables
)
const val DATABASE_VERSION = 252
const val DATABASE_VERSION = 253
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,47 @@
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.thoughtcrime.securesms.database.ChatFolderTables
/**
* Adds the tables for managing chat folders
*/
@Suppress("ClassName")
object V253_CreateChatFolderTables : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL(
"""
CREATE TABLE chat_folder (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT DEFAULT NULL,
position INTEGER DEFAULT 0,
show_unread INTEGER DEFAULT 0,
show_muted INTEGER DEFAULT 0,
show_individual INTEGER DEFAULT 0,
show_groups INTEGER DEFAULT 0,
is_muted INTEGER DEFAULT 0,
folder_type INTEGER DEFAULT 4
)
"""
)
db.execSQL(
"""
CREATE TABLE chat_folder_membership (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_folder_id INTEGER NOT NULL REFERENCES chat_folder (_id) ON DELETE CASCADE,
thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE,
membership_type INTEGER DEFAULT 1
)
"""
)
db.execSQL("CREATE INDEX chat_folder_position_index ON chat_folder (position)")
db.execSQL("CREATE INDEX chat_folder_membership_chat_folder_id_index ON chat_folder_membership (chat_folder_id)")
db.execSQL("CREATE INDEX chat_folder_membership_thread_id_index ON chat_folder_membership (thread_id)")
db.execSQL("CREATE INDEX chat_folder_membership_membership_type_index ON chat_folder_membership (membership_type)")
ChatFolderTables.insertInitialChatFoldersAtCreationTime(db)
}
}