mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 02:10:44 +01:00
Release chat folders to internal users.
This commit is contained in:
committed by
Greyson Parrelli
parent
e5c122d972
commit
c4fc32988c
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -> ""
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user