From 7dab8f09f7e2736e8518fc97e32636b298ecdd83 Mon Sep 17 00:00:00 2001 From: klockky Date: Thu, 9 Jul 2026 19:12:41 +0300 Subject: [PATCH] =?UTF-8?q?feat(folders):=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=81=D0=BE=D1=80=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=B0=D0=BF=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/modules/account.dart | 9 ++- lib/backend/modules/contacts.dart | 8 ++ lib/backend/modules/folders.dart | 80 ++++++++++++++----- lib/core/config/komet_settings.dart | 9 +++ lib/core/protocol/opcode_map.dart | 2 + .../screens/chats/chat_list_screen.dart | 30 ++++++- .../profile/komet_settings_screen.dart | 22 +++++ 7 files changed, 136 insertions(+), 24 deletions(-) diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart index 0f3724b..96da788 100644 --- a/lib/backend/modules/account.dart +++ b/lib/backend/modules/account.dart @@ -544,7 +544,8 @@ class AccountModule { payload['lastLogin'] = sync.lastLogin; if (sync.configHash != null) payload['configHash'] = sync.configHash; } else { - payload['presenceSync'] = 0; + payload['presenceSync'] = -1; + payload['chatsSync'] = -1; } return payload; @@ -586,6 +587,12 @@ class AccountModule { await ContactsModule.syncFromLoginPayload(data, profile.id); await chats.syncFromLoginPayload(data, profile.id, profile.id); + try { + await ContactsModule.syncFromServer(_api, profile.id); + } catch (e) { + logger.w('Контакты: $e'); + } + final config = data['config']; if (config is Map) { await FoldersModule.applyFromLoginConfig( diff --git a/lib/backend/modules/contacts.dart b/lib/backend/modules/contacts.dart index 527dc6a..29b3c71 100644 --- a/lib/backend/modules/contacts.dart +++ b/lib/backend/modules/contacts.dart @@ -183,6 +183,14 @@ class ContactsModule { ); } + static Future syncFromServer(Api api, int accountId) async { + final map = await api.sendRequestMap(Opcode.contactsGet, { + 'contactsSync': 0, + }); + if (map == null) return; + await syncFromLoginPayload(map.cast(), accountId); + } + static void _primeContactCache(Map contact) { final id = contact['id']; if (id is! int) return; diff --git a/lib/backend/modules/folders.dart b/lib/backend/modules/folders.dart index 74eb3ee..c179c75 100644 --- a/lib/backend/modules/folders.dart +++ b/lib/backend/modules/folders.dart @@ -55,31 +55,69 @@ class FoldersModule { }); } - static bool chatMatchesFolder(CachedChat chat, ChatFolder folder) { - if (folder.include != null && folder.include!.isNotEmpty) { - return folder.include!.contains(chat.id); + static const int filterUnread = 0; + static const int filterChannel = 2; + static const int filterGroup = 3; + static const int filterContact = 8; + static const int filterNotContact = 9; + static const int filterBot = 10; + + static int? _filterCode(dynamic raw) { + if (raw is int) return raw; + if (raw is String) { + final n = int.tryParse(raw); + if (n != null) return n; + switch (raw) { + case 'UNREAD': + return filterUnread; + case 'CHANNEL': + return filterChannel; + case 'GROUP': + case 'CHAT': + return filterGroup; + case 'CONTACT': + return filterContact; + case 'NOT_CONTACT': + return filterNotContact; + case 'BOT': + return filterBot; + } + } + return null; + } + + static bool chatMatchesFolder( + CachedChat chat, + ChatFolder folder, { + required int myId, + required Set contactIds, + }) { + if (folder.include != null && folder.include!.contains(chat.id)) { + return true; } if (folder.filters.isEmpty) return false; - final hasContact = folder.filters.any( - (f) => f == 9 || f == '9' || f == 'CONTACT', - ); - final hasNotContact = folder.filters.any( - (f) => f == 8 || f == '8' || f == 'NOT_CONTACT', - ); + final isDialog = chat.type == 'DIALOG'; + final isBot = isDialog && chat.options.contains('BOT'); + final peerId = isDialog ? chat.id ^ myId : null; + final isSelf = peerId != null && peerId == myId; + final isContact = + isDialog && !isSelf && peerId != null && contactIds.contains(peerId); - if (hasContact && hasNotContact) { - if (chat.type != 'DIALOG') return false; - return true; - } - - for (final filter in folder.filters) { - if (filter == 0 || filter == '0' || filter == 'UNREAD') { - if (chat.unreadCount > 0) return true; - } else if (filter == 9 || filter == '9' || filter == 'CONTACT') { - if (chat.type == 'DIALOG') return true; - } else if (filter == 8 || filter == '8' || filter == 'NOT_CONTACT') { - if (chat.type == 'CHAT' || chat.type == 'CHANNEL') return true; + for (final raw in folder.filters) { + switch (_filterCode(raw)) { + case filterUnread: + if (chat.unreadCount > 0) return true; + case filterChannel: + if (chat.type == 'CHANNEL') return true; + case filterGroup: + if (chat.type == 'CHAT' || chat.type == 'GROUP') return true; + case filterContact: + if (isDialog && !isBot && isContact) return true; + case filterNotContact: + if (isDialog && !isBot && !isSelf && !isContact) return true; + case filterBot: + if (isBot) return true; } } return false; diff --git a/lib/core/config/komet_settings.dart b/lib/core/config/komet_settings.dart index d257dee..9e3b849 100644 --- a/lib/core/config/komet_settings.dart +++ b/lib/core/config/komet_settings.dart @@ -8,6 +8,7 @@ class KometSettings { static const _kGhostMode = 'komet_ghost_mode'; static const _kAntiRead = 'komet_anti_read'; static const _kSelfOnlineCheck = 'komet_self_online_check'; + static const _kHideAllChatsFolder = 'komet_hide_all_chats_folder'; static final ValueNotifier viewDeleted = ValueNotifier(false); static final ValueNotifier viewRedacted = ValueNotifier(false); @@ -15,6 +16,7 @@ class KometSettings { static final ValueNotifier ghostMode = ValueNotifier(false); static final ValueNotifier antiRead = ValueNotifier(false); static final ValueNotifier selfOnlineCheck = ValueNotifier(true); + static final ValueNotifier hideAllChatsFolder = ValueNotifier(false); static Future load() async { final prefs = await SharedPreferences.getInstance(); @@ -24,6 +26,7 @@ class KometSettings { ghostMode.value = prefs.getBool(_kGhostMode) ?? false; antiRead.value = prefs.getBool(_kAntiRead) ?? false; selfOnlineCheck.value = prefs.getBool(_kSelfOnlineCheck) ?? true; + hideAllChatsFolder.value = prefs.getBool(_kHideAllChatsFolder) ?? false; } static Future setViewDeleted(bool value) async { @@ -61,4 +64,10 @@ class KometSettings { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_kSelfOnlineCheck, value); } + + static Future setHideAllChatsFolder(bool value) async { + hideAllChatsFolder.value = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_kHideAllChatsFolder, value); + } } diff --git a/lib/core/protocol/opcode_map.dart b/lib/core/protocol/opcode_map.dart index fbb9f9c..193752f 100644 --- a/lib/core/protocol/opcode_map.dart +++ b/lib/core/protocol/opcode_map.dart @@ -11,6 +11,7 @@ abstract class Opcode { static const int reconnect = 3; // Реконнект static const int log = 5; // Аналитика / события static const int sessionInit = 6; // Инициализация сессии (хэндшейк) + static const int contactsGet = 8; // Синхронизация списка контактов // ── Profile ──────────────────────────────────────────────────────── static const int profile = 16; // Обновление профиля @@ -226,6 +227,7 @@ abstract class Opcode { reconnect: 'RECONNECT', log: 'LOG', sessionInit: 'SESSION_INIT', + contactsGet: 'CONTACTS_GET', profile: 'PROFILE', authRequest: 'AUTH_REQUEST', auth: 'AUTH', diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index ae0a7d0..04f107d 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -36,10 +36,12 @@ import '../../../core/utils/haptics.dart'; import '../../../core/config/app_animations.dart'; import '../../../core/config/app_stories.dart'; import '../../../core/config/app_colors.dart'; +import '../../../core/config/komet_settings.dart'; import '../../../backend/models/chat_folder.dart'; import '../../../backend/modules/account.dart'; import '../../../backend/modules/chats.dart'; import '../../../backend/modules/cloud_storage.dart'; +import '../../../backend/modules/contacts.dart'; import '../../../backend/modules/folders.dart'; import '../../../core/storage/app_database.dart'; import '../../../core/storage/draft_store.dart'; @@ -133,6 +135,7 @@ class _ChatListScreenState extends State String? _selectedFolderId; List _folders = []; + Set _contactIds = {}; int _currentNavIndex = 0; @@ -545,6 +548,7 @@ class _ChatListScreenState extends State DraftStore.instance.revision.addListener(_onDraftsChanged); AppStories.current.addListener(_onStoriesEnabledChanged); storiesModule.storiesChanged.addListener(_onStoriesDataChanged); + KometSettings.hideAllChatsFolder.addListener(_requestReload); _maybeLoadStories(); _typingSub = api.pushStream .where((p) => p.opcode == Opcode.notifTyping) @@ -708,6 +712,9 @@ class _ChatListScreenState extends State final loadedChats = await chats.getChats(p.id); var folders = await FoldersModule.loadFolders(p.id); final foldersKnown = await FoldersModule.hasReceivedFoldersList(p.id); + final contactIds = (await ContactsModule.getContacts(p.id)) + .map((c) => c.id) + .toSet(); final allChatsFolder = ChatFolder( id: 'all.chat.folder', @@ -717,7 +724,14 @@ class _ChatListScreenState extends State widgets: [], ); - if (!folders.any((f) => FoldersModule.isAllChatsFolder(f))) { + final hasRealFolders = folders.any( + (f) => !FoldersModule.isAllChatsFolder(f), + ); + if (KometSettings.hideAllChatsFolder.value && hasRealFolders) { + folders = folders + .where((f) => !FoldersModule.isAllChatsFolder(f)) + .toList(); + } else if (!folders.any((f) => FoldersModule.isAllChatsFolder(f))) { folders = [allChatsFolder, ...folders]; } @@ -727,6 +741,7 @@ class _ChatListScreenState extends State final filteredChats = loadedChats .where((c) => !CloudStorageModule.isCloudStorageGroup(c)) .toList(); + final newIds = filteredChats.map((c) => c.id.toString()).toSet(); final entering = _didInitialChatLoad ? newIds.difference(_knownChatIds) @@ -740,6 +755,7 @@ class _ChatListScreenState extends State setState(() { _profile = p; _chats = filteredChats; + _contactIds = contactIds; _enteringChatIds = entering; _chatListRevision++; _folders = folders; @@ -858,6 +874,7 @@ class _ChatListScreenState extends State final baseKey = Object.hash( identityHashCode(_chats), identityHashCode(_folders), + identityHashCode(_contactIds), ); if (_pageChatsBaseKey != baseKey) { _pageChatsBaseKey = baseKey; @@ -873,10 +890,18 @@ class _ChatListScreenState extends State base = _chats; } else { final folder = _folders[pageIndex]; + final myId = _profile?.id ?? 0; base = FoldersModule.isAllChatsFolder(folder) ? _chats : _chats - .where((c) => FoldersModule.chatMatchesFolder(c, folder)) + .where( + (c) => FoldersModule.chatMatchesFolder( + c, + folder, + myId: myId, + contactIds: _contactIds, + ), + ) .toList(); } final pinned = base.where((c) => (c.favIndex ?? 0) > 0).toList() @@ -1127,6 +1152,7 @@ class _ChatListScreenState extends State DraftStore.instance.revision.removeListener(_onDraftsChanged); AppStories.current.removeListener(_onStoriesEnabledChanged); storiesModule.storiesChanged.removeListener(_onStoriesDataChanged); + KometSettings.hideAllChatsFolder.removeListener(_requestReload); _loginSub?.cancel(); _stateSub?.cancel(); _typingSub?.cancel(); diff --git a/lib/frontend/screens/profile/komet_settings_screen.dart b/lib/frontend/screens/profile/komet_settings_screen.dart index fea5314..1bcdc14 100644 --- a/lib/frontend/screens/profile/komet_settings_screen.dart +++ b/lib/frontend/screens/profile/komet_settings_screen.dart @@ -67,6 +67,28 @@ class KometSettingsScreen extends StatelessWidget { ], ), const SizedBox(height: 20), + const SectionHeader( + 'Папки', + padding: EdgeInsets.fromLTRB(8, 0, 8, 8), + fontSize: 14, + ), + SettingsCard( + children: [ + ValueListenableBuilder( + valueListenable: KometSettings.hideAllChatsFolder, + builder: (context, value, _) => SettingsToggleTile( + icon: Symbols.folder_off, + label: 'Hide "All" folder', + subtitle: + 'Скрыть папку «Все», когда есть другие папки. ' + 'Чаты сортируются только по вашим папкам', + value: value, + onChanged: KometSettings.setHideAllChatsFolder, + ), + ), + ], + ), + const SizedBox(height: 20), const SectionHeader( 'Ghost Mode', padding: EdgeInsets.fromLTRB(8, 0, 8, 8),