feat: Enhance group chat functionality and UI improvements
- Added support for group action system messages in MessageBubble. - Implemented group invite handling with inline cards for joining groups. - Updated MessageBubble to display group sender labels and admin badges. - Enhanced image decryption logic for group attachments. - Modified BlurredAvatarBackground to load system avatars based on public keys. - Improved SwipeBackContainer with layer management for better swipe effects. - Updated VerifiedBadge to use dynamic icons based on user verification status. - Added new drawable resource for admin badge icon.
This commit is contained in:
@@ -141,6 +141,39 @@ data class DialogEntity(
|
||||
"[]" // 📎 JSON attachments последнего сообщения (кэш из messages)
|
||||
)
|
||||
|
||||
/** Entity для групповых диалогов */
|
||||
@Entity(
|
||||
tableName = "groups",
|
||||
indices =
|
||||
[
|
||||
Index(value = ["account", "group_id"], unique = true)]
|
||||
)
|
||||
data class GroupEntity(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
@ColumnInfo(name = "account") val account: String,
|
||||
@ColumnInfo(name = "group_id") val groupId: String,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "description") val description: String = "",
|
||||
@ColumnInfo(name = "key") val key: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface GroupDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertGroup(group: GroupEntity): Long
|
||||
|
||||
@Query(
|
||||
"SELECT * FROM groups WHERE account = :account AND group_id = :groupId LIMIT 1"
|
||||
)
|
||||
suspend fun getGroup(account: String, groupId: String): GroupEntity?
|
||||
|
||||
@Query("DELETE FROM groups WHERE account = :account AND group_id = :groupId")
|
||||
suspend fun deleteGroup(account: String, groupId: String): Int
|
||||
|
||||
@Query("DELETE FROM groups WHERE account = :account")
|
||||
suspend fun deleteAllByAccount(account: String): Int
|
||||
}
|
||||
|
||||
/** DAO для работы с сообщениями */
|
||||
@Dao
|
||||
interface MessageDao {
|
||||
@@ -476,8 +509,12 @@ interface DialogDao {
|
||||
"""
|
||||
SELECT * FROM dialogs
|
||||
WHERE account = :account
|
||||
AND i_have_sent = 1
|
||||
AND last_message_timestamp > 0
|
||||
AND (
|
||||
i_have_sent = 1
|
||||
OR opponent_key = '0x000000000000000000000000000000000000000001'
|
||||
OR opponent_key = '0x000000000000000000000000000000000000000002'
|
||||
)
|
||||
AND (last_message != '' OR last_message_attachments != '[]')
|
||||
ORDER BY last_message_timestamp DESC
|
||||
LIMIT 30
|
||||
"""
|
||||
@@ -489,8 +526,12 @@ interface DialogDao {
|
||||
"""
|
||||
SELECT * FROM dialogs
|
||||
WHERE account = :account
|
||||
AND i_have_sent = 1
|
||||
AND last_message_timestamp > 0
|
||||
AND (
|
||||
i_have_sent = 1
|
||||
OR opponent_key = '0x000000000000000000000000000000000000000001'
|
||||
OR opponent_key = '0x000000000000000000000000000000000000000002'
|
||||
)
|
||||
AND (last_message != '' OR last_message_attachments != '[]')
|
||||
ORDER BY last_message_timestamp DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""
|
||||
@@ -506,7 +547,9 @@ interface DialogDao {
|
||||
SELECT * FROM dialogs
|
||||
WHERE account = :account
|
||||
AND i_have_sent = 0
|
||||
AND last_message_timestamp > 0
|
||||
AND opponent_key != '0x000000000000000000000000000000000000000001'
|
||||
AND opponent_key != '0x000000000000000000000000000000000000000002'
|
||||
AND (last_message != '' OR last_message_attachments != '[]')
|
||||
ORDER BY last_message_timestamp DESC
|
||||
LIMIT 30
|
||||
"""
|
||||
@@ -519,7 +562,9 @@ interface DialogDao {
|
||||
SELECT COUNT(*) FROM dialogs
|
||||
WHERE account = :account
|
||||
AND i_have_sent = 0
|
||||
AND last_message_timestamp > 0
|
||||
AND opponent_key != '0x000000000000000000000000000000000000000001'
|
||||
AND opponent_key != '0x000000000000000000000000000000000000000002'
|
||||
AND (last_message != '' OR last_message_attachments != '[]')
|
||||
"""
|
||||
)
|
||||
fun getRequestsCountFlow(account: String): Flow<Int>
|
||||
@@ -532,7 +577,8 @@ interface DialogDao {
|
||||
@Query("""
|
||||
SELECT * FROM dialogs
|
||||
WHERE account = :account
|
||||
AND last_message_timestamp > 0
|
||||
AND (last_message != '' OR last_message_attachments != '[]')
|
||||
AND opponent_key NOT LIKE '#group:%'
|
||||
AND (
|
||||
opponent_title = ''
|
||||
OR opponent_title = opponent_key
|
||||
@@ -709,6 +755,32 @@ interface DialogDao {
|
||||
)
|
||||
suspend fun hasSentToOpponent(account: String, opponentKey: String): Boolean
|
||||
|
||||
/** 🚀 Универсальный подсчет непрочитанных входящих по dialog_key (direct + group) */
|
||||
@Query(
|
||||
"""
|
||||
SELECT COUNT(*) FROM messages
|
||||
WHERE account = :account
|
||||
AND dialog_key = :dialogKey
|
||||
AND from_me = 0
|
||||
AND read = 0
|
||||
"""
|
||||
)
|
||||
suspend fun countUnreadByDialogKey(account: String, dialogKey: String): Int
|
||||
|
||||
/** 🚀 Универсальная проверка исходящих сообщений по dialog_key (direct + group) */
|
||||
@Query(
|
||||
"""
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM messages
|
||||
WHERE account = :account
|
||||
AND dialog_key = :dialogKey
|
||||
AND from_me = 1
|
||||
LIMIT 1
|
||||
)
|
||||
"""
|
||||
)
|
||||
suspend fun hasSentByDialogKey(account: String, dialogKey: String): Boolean
|
||||
|
||||
/**
|
||||
* 🚀 ОПТИМИЗИРОВАННЫЙ updateDialogFromMessages Заменяет монолитный SQL с 9 коррелированными
|
||||
* подзапросами на:
|
||||
@@ -720,31 +792,40 @@ interface DialogDao {
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun updateDialogFromMessages(account: String, opponentKey: String) {
|
||||
val normalizedOpponentKey = opponentKey.trim()
|
||||
val normalizedOpponentKeyLower = normalizedOpponentKey.lowercase()
|
||||
val isGroupDialog =
|
||||
normalizedOpponentKeyLower.startsWith("#group:") ||
|
||||
normalizedOpponentKeyLower.startsWith("group:")
|
||||
val isSystemDialog =
|
||||
normalizedOpponentKey == "0x000000000000000000000000000000000000000001" ||
|
||||
normalizedOpponentKey == "0x000000000000000000000000000000000000000002"
|
||||
|
||||
// 📁 Для saved messages dialogKey = account (не "$account:$account")
|
||||
val dialogKey =
|
||||
if (account == opponentKey) account
|
||||
else if (account < opponentKey) "$account:$opponentKey"
|
||||
else "$opponentKey:$account"
|
||||
if (account == normalizedOpponentKey) account
|
||||
else if (isGroupDialog) normalizedOpponentKey
|
||||
else if (account < normalizedOpponentKey) "$account:$normalizedOpponentKey"
|
||||
else "$normalizedOpponentKey:$account"
|
||||
|
||||
// 1. Получаем последнее сообщение — O(1) по индексу (account, dialog_key, timestamp)
|
||||
val lastMsg = getLastMessageByDialogKey(account, dialogKey) ?: return
|
||||
|
||||
// 2. Получаем существующий диалог для сохранения метаданных (online, verified, title...)
|
||||
val existing = getDialog(account, opponentKey)
|
||||
val existing = getDialog(account, normalizedOpponentKey)
|
||||
|
||||
// 3. Считаем непрочитанные — O(N) по индексу (account, from_public_key, to_public_key,
|
||||
// timestamp)
|
||||
val unread = countUnreadFromOpponent(account, opponentKey)
|
||||
// 3. Считаем непрочитанные входящие по dialog_key.
|
||||
val unread = countUnreadByDialogKey(account, dialogKey)
|
||||
|
||||
// 4. Проверяем были ли исходящие — O(1)
|
||||
val hasSent = hasSentToOpponent(account, opponentKey)
|
||||
val hasSent = hasSentByDialogKey(account, dialogKey)
|
||||
|
||||
// 5. Один INSERT OR REPLACE с вычисленными данными
|
||||
insertDialog(
|
||||
DialogEntity(
|
||||
id = existing?.id ?: 0,
|
||||
account = account,
|
||||
opponentKey = opponentKey,
|
||||
opponentKey = normalizedOpponentKey,
|
||||
opponentTitle = existing?.opponentTitle ?: "",
|
||||
opponentUsername = existing?.opponentUsername ?: "",
|
||||
lastMessage = lastMsg.plainMessage,
|
||||
@@ -753,7 +834,8 @@ interface DialogDao {
|
||||
isOnline = existing?.isOnline ?: 0,
|
||||
lastSeen = existing?.lastSeen ?: 0,
|
||||
verified = existing?.verified ?: 0,
|
||||
iHaveSent = if (hasSent) 1 else (existing?.iHaveSent ?: 0),
|
||||
// Desktop parity: request flag is always derived from message history.
|
||||
iHaveSent = if (hasSent || isSystemDialog) 1 else 0,
|
||||
lastMessageFromMe = lastMsg.fromMe,
|
||||
lastMessageDelivered = if (lastMsg.fromMe == 1) lastMsg.delivered else 0,
|
||||
lastMessageRead = if (lastMsg.fromMe == 1) lastMsg.read else 0,
|
||||
|
||||
@@ -16,8 +16,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
BlacklistEntity::class,
|
||||
AvatarCacheEntity::class,
|
||||
AccountSyncTimeEntity::class,
|
||||
GroupEntity::class,
|
||||
PinnedMessageEntity::class],
|
||||
version = 13,
|
||||
version = 14,
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class RosettaDatabase : RoomDatabase() {
|
||||
@@ -27,6 +28,7 @@ abstract class RosettaDatabase : RoomDatabase() {
|
||||
abstract fun blacklistDao(): BlacklistDao
|
||||
abstract fun avatarDao(): AvatarDao
|
||||
abstract fun syncTimeDao(): SyncTimeDao
|
||||
abstract fun groupDao(): GroupDao
|
||||
abstract fun pinnedMessageDao(): PinnedMessageDao
|
||||
|
||||
companion object {
|
||||
@@ -176,6 +178,30 @@ abstract class RosettaDatabase : RoomDatabase() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 👥 МИГРАЦИЯ 13->14: Таблица groups для групповых диалогов
|
||||
*/
|
||||
private val MIGRATION_13_14 =
|
||||
object : Migration(13, 14) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
account TEXT NOT NULL,
|
||||
group_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
`key` TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
database.execSQL(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS index_groups_account_group_id ON groups (account, group_id)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDatabase(context: Context): RosettaDatabase {
|
||||
return INSTANCE
|
||||
?: synchronized(this) {
|
||||
@@ -197,7 +223,8 @@ abstract class RosettaDatabase : RoomDatabase() {
|
||||
MIGRATION_9_10,
|
||||
MIGRATION_10_11,
|
||||
MIGRATION_11_12,
|
||||
MIGRATION_12_13
|
||||
MIGRATION_12_13,
|
||||
MIGRATION_13_14
|
||||
)
|
||||
.fallbackToDestructiveMigration() // Для разработки - только
|
||||
// если миграция не
|
||||
|
||||
Reference in New Issue
Block a user