diff --git a/lib/backend/modules/chats.dart b/lib/backend/modules/chats.dart index 2308163..8157055 100644 --- a/lib/backend/modules/chats.dart +++ b/lib/backend/modules/chats.dart @@ -38,6 +38,7 @@ class CachedChat { final bool isOnline; final int seenTime; final Map participants; + final Set options; const CachedChat({ required this.id, @@ -57,8 +58,11 @@ class CachedChat { required this.isOnline, required this.seenTime, required this.participants, + this.options = const {}, }); + bool get isOfficial => options.contains('OFFICIAL'); + factory CachedChat.fromDbRow(Map row) => CachedChat( id: row['id'] as int, accountId: row['account_id'] as int, @@ -76,9 +80,15 @@ class CachedChat { dontDisturbUntil: row['dont_disturb_until'] as int, isOnline: (row['is_online'] as int) == 1, seenTime: row['seen_time'] as int, - participants: _parseParticipants(row['participants']) + participants: _parseParticipants(row['participants']), + options: _decodeOptions(row['options']), ); + static Set _decodeOptions(dynamic raw) { + if (raw is! String || raw.isEmpty) return const {}; + return raw.split(',').where((s) => s.isNotEmpty).toSet(); + } + Map toDbRow() => { 'id': id, 'account_id': accountId, @@ -96,7 +106,8 @@ class CachedChat { 'dont_disturb_until': dontDisturbUntil, 'is_online': isOnline ? 1 : 0, 'seen_time': seenTime, - 'participants': jsonEncode(participants.map((k, v) => MapEntry(k.toString(), v))) + 'participants': jsonEncode(participants.map((k, v) => MapEntry(k.toString(), v))), + 'options': options.isEmpty ? null : options.join(','), }; } @@ -210,6 +221,7 @@ class ChatsModule { String? title; String? iconUrl; + Set options = const {}; if (type == 'DIALOG') { otherId = _otherParticipantId(chat['participants'], currentUserId); @@ -218,13 +230,25 @@ class ChatsModule { if (contact != null) { title = _nameFromContact(contact); iconUrl = contact['baseUrl'] as String?; + final contactOpts = contact['options']; + if (contactOpts is List) { + options = contactOpts.whereType().toSet(); + } } else { title = existing[id]?.title; iconUrl = existing[id]?.iconUrl; + options = existing[id]?.options ?? const {}; } } else { title = chat['title'] as String?; iconUrl = chat['baseIconUrl'] as String?; + final chatOpts = chat['options']; + if (chatOpts is Map) { + options = { + for (final entry in chatOpts.entries) + if (entry.value == true && entry.key is String) entry.key as String, + }; + } } final lastMsg = chat['lastMessage']; @@ -276,7 +300,8 @@ class ChatsModule { dontDisturbUntil: dontDisturbUntil, isOnline: isOnline, seenTime: seenTime, - participants: participants + participants: participants, + options: options, ); } catch (e) { logger.e("Ошибка при парсинге чата: $e"); diff --git a/lib/backend/modules/contacts.dart b/lib/backend/modules/contacts.dart index b4f103b..fda11db 100644 --- a/lib/backend/modules/contacts.dart +++ b/lib/backend/modules/contacts.dart @@ -11,6 +11,7 @@ class CachedContact { final String? baseUrl; final String? baseRawUrl; final int updateTime; + final Set options; const CachedContact({ required this.id, @@ -22,8 +23,14 @@ class CachedContact { this.baseUrl, this.baseRawUrl, required this.updateTime, + this.options = const {}, }); + bool get isOfficial => options.contains('OFFICIAL'); + bool get isBot => options.contains('BOT'); + bool get isServiceAccount => options.contains('SERVICE_ACCOUNT'); + bool get isVerified => isOfficial || isBot || isServiceAccount; + factory CachedContact.fromDbRow(Map row) => CachedContact( id: row['id'] as int, accountId: row['account_id'] as int, @@ -34,7 +41,13 @@ class CachedContact { baseUrl: row['base_url'] as String?, baseRawUrl: row['base_raw_url'] as String?, updateTime: row['update_time'] as int, + options: _decodeOptions(row['options']), ); + + static Set _decodeOptions(dynamic raw) { + if (raw is! String || raw.isEmpty) return const {}; + return raw.split(',').where((s) => s.isNotEmpty).toSet(); + } } class ContactsModule { @@ -126,6 +139,12 @@ class ContactsModule { lastName = name['lastName'] as String?; } + final optionsRaw = contact['options']; + String? optionsStr; + if (optionsRaw is List) { + optionsStr = optionsRaw.whereType().join(','); + } + return { 'id': id, 'account_id': accountId, @@ -136,6 +155,7 @@ class ContactsModule { 'base_url': contact['baseUrl'] as String?, 'base_raw_url': contact['baseRawUrl'] as String?, 'update_time': (contact['updateTime'] as int?) ?? 0, + 'options': optionsStr, }; } } diff --git a/lib/core/storage/app_database.dart b/lib/core/storage/app_database.dart index 24f02d9..7f7ff13 100644 --- a/lib/core/storage/app_database.dart +++ b/lib/core/storage/app_database.dart @@ -159,7 +159,7 @@ class AppDatabase { final dbPath = await getDatabasesPath(); return openDatabase( join(dbPath, 'komet.db'), - version: 8, + version: 9, onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'), onCreate: (db, _) => _createTables(db), onUpgrade: (db, oldVersion, newVersion) async { @@ -193,6 +193,14 @@ class AppDatabase { 'ALTER TABLE chats_cache ADD COLUMN participants TEXT', ); } + if (oldVersion < 9) { + await db.execute( + 'ALTER TABLE contacts ADD COLUMN options TEXT', + ); + await db.execute( + 'ALTER TABLE chats_cache ADD COLUMN options TEXT', + ); + } }, ); } @@ -230,7 +238,8 @@ class AppDatabase { photo_id INTEGER, base_url TEXT, base_raw_url TEXT, - update_time INTEGER NOT NULL DEFAULT 0 + update_time INTEGER NOT NULL DEFAULT 0, + options TEXT ) '''; @@ -262,6 +271,7 @@ class AppDatabase { is_online INTEGER NOT NULL DEFAULT 0, seen_time INTEGER NOT NULL DEFAULT 0, participants TEXT NOT NULL DEFAULT "", + options TEXT, PRIMARY KEY (id, account_id) ) '''; diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index bb9d2fa..57eb64e 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -1102,6 +1102,7 @@ class _ChatListScreenState extends State isOnline: chat.isOnline, unreadCount: chat.unreadCount, isMuted: chat.dontDisturbUntil > 0, + isVerified: chat.isOfficial, chatType: "DIALOG", ); } else { @@ -1134,6 +1135,7 @@ class _ChatListScreenState extends State isOnline: chat.isOnline, unreadCount: chat.unreadCount, isMuted: chat.dontDisturbUntil > 0, + isVerified: chat.isOfficial, chatType: chat.type, ); } @@ -1749,6 +1751,7 @@ class _ChatListScreenState extends State bool isRead = false, int unreadCount = 0, bool isMuted = false, + bool isVerified = false, String chatType = "CHAT", }) { final cs = Theme.of(context).colorScheme; @@ -1849,16 +1852,33 @@ Navigator.push( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Text( - name, - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w600, - height: 1.1, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + name, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w600, + height: 1.1, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (isVerified) ...[ + const SizedBox(width: 4), + Icon( + Symbols.verified, + color: cs.primary, + size: 16, + weight: 600, + fill: 1, + ), + ], + ], ), ), if (isMuted) ...[ diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index 9d50c57..87a6bc8 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -304,14 +304,33 @@ class _ChatScreenState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.name, - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w600, - fontFamily: 'Outfit', - ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + widget.name, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (chat?.isOfficial ?? false) ...[ + const SizedBox(width: 4), + Icon( + Symbols.verified, + color: cs.primary, + size: 16, + weight: 600, + fill: 1, + ), + ], + ], ), Text( status ?? "", diff --git a/lib/frontend/screens/contacts/contacts_tab.dart b/lib/frontend/screens/contacts/contacts_tab.dart index a2d00d6..8acc382 100644 --- a/lib/frontend/screens/contacts/contacts_tab.dart +++ b/lib/frontend/screens/contacts/contacts_tab.dart @@ -100,15 +100,32 @@ class _ContactsTabState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - nameToDisplay, - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + nameToDisplay, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (contact.isVerified) ...[ + const SizedBox(width: 4), + Icon( + Symbols.verified, + color: cs.primary, + size: 16, + weight: 600, + fill: 1, + ), + ], + ], ), const SizedBox(height: 4), Text(