From ba1f63ad05bcd03e03681049b7bdccafb86886a6 Mon Sep 17 00:00:00 2001 From: Jganenokk Date: Sun, 26 Jul 2026 13:19:23 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20=D1=84=D0=B8=D0=BA=D1=81=20=D0=BE=D1=82?= =?UTF-8?q?=D1=81=D1=82=D1=83=D0=BF=D0=BE=D0=B2,=20=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D1=81=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D1=89=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=87=D0=B0=D1=82=D0=BE=D0=B2=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/modules/account.dart | 1 + lib/backend/modules/chat_parsing.dart | 5 +- lib/frontend/widgets/message_bubble.dart | 5 +- test/chat_pin_order_test.dart | 89 ++++++++++++++++++++++++ test/message_bubble_layout_test.dart | 80 +++++++++++++++++++++ 5 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 test/chat_pin_order_test.dart diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart index c74e913..7863238 100644 --- a/lib/backend/modules/account.dart +++ b/lib/backend/modules/account.dart @@ -598,6 +598,7 @@ class AccountModule { profile.id, config.cast(), ); + await chats.applyFavorites(profile.id); final userConfig = config['user']; if (userConfig is Map) { await AppDatabase.savePrivacyConfig(profile.id, jsonEncode(userConfig)); diff --git a/lib/backend/modules/chat_parsing.dart b/lib/backend/modules/chat_parsing.dart index e5472f7..5c94c17 100644 --- a/lib/backend/modules/chat_parsing.dart +++ b/lib/backend/modules/chat_parsing.dart @@ -163,13 +163,14 @@ _resolveLastMessage(dynamic lastMsg) { Map existing, ) { final config = chatsConfig[id.toString()] ?? chatsConfig[id]; + final ex = existing[id]; if (config is Map) { + final configFav = config['favIndex'] as int?; return ( - favIndex: config['favIndex'] as int?, + favIndex: (configFav != null && configFav > 0) ? configFav : ex?.favIndex, dontDisturbUntil: (config['dontDisturbUntil'] as int?) ?? 0, ); } - final ex = existing[id]; if (ex != null) { return (favIndex: ex.favIndex, dontDisturbUntil: ex.dontDisturbUntil); } diff --git a/lib/frontend/widgets/message_bubble.dart b/lib/frontend/widgets/message_bubble.dart index a169183..79bacb5 100644 --- a/lib/frontend/widgets/message_bubble.dart +++ b/lib/frontend/widgets/message_bubble.dart @@ -745,10 +745,7 @@ class MessageBubble extends StatelessWidget { if (showAvatar) _buildLeadingAvatar(cs) else if (showAvatarSlot && chatType == "CHAT") - const CircleAvatar( - radius: 15, - backgroundColor: Color(0x00000000), - ), + const SizedBox(width: 30), Column( crossAxisAlignment: isMe ? CrossAxisAlignment.end diff --git a/test/chat_pin_order_test.dart b/test/chat_pin_order_test.dart new file mode 100644 index 0000000..035c762 --- /dev/null +++ b/test/chat_pin_order_test.dart @@ -0,0 +1,89 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:komet/backend/modules/chat_parsing.dart'; +import 'package:komet/backend/modules/chats.dart'; + +const int _me = 4242; +const int _chatId = -1000000000001; + +CachedChat _cached({int? favIndex}) => CachedChat( + id: _chatId, + accountId: _me, + type: 'CHAT', + title: 'pinned chat', + unreadCount: 0, + lastEventTime: 1700000000000, + cachedAt: 0, + favIndex: favIndex, + dontDisturbUntil: 0, + isOnline: false, + seenTime: 0, + participants: {_me: 1700000000000}, +); + +CachedChat _parse({ + Map chatsConfig = const {}, + CachedChat? existing, +}) => parseChatRow( + { + 'id': _chatId, + 'type': 'CHAT', + 'title': 'pinned chat', + 'lastEventTime': 1700000000000, + 'participants': {'$_me': 1700000000000}, + }, + _me, + _me, + const {}, + chatsConfig, + const {}, + existing == null ? const {} : {_chatId: existing}, + 0, +)!; + +void main() { + group('login sync keeps pins', () { + test('a zero favIndex in the config does not unpin a cached chat', () { + final parsed = _parse( + chatsConfig: { + '$_chatId': {'favIndex': 0, 'dontDisturbUntil': 0}, + }, + existing: _cached(favIndex: 3), + ); + expect(parsed.favIndex, 3); + }); + + test('a real favIndex from the config wins', () { + final parsed = _parse( + chatsConfig: { + '$_chatId': {'favIndex': 2, 'dontDisturbUntil': 0}, + }, + existing: _cached(favIndex: 3), + ); + expect(parsed.favIndex, 2); + }); + + test('mute settings still come from the config', () { + final parsed = _parse( + chatsConfig: { + '$_chatId': {'favIndex': 0, 'dontDisturbUntil': -1}, + }, + existing: _cached(favIndex: 3), + ); + expect(parsed.dontDisturbUntil, -1); + expect(parsed.isMuted, isTrue); + }); + + test('a chat with no cached pin stays unpinned', () { + final parsed = _parse( + chatsConfig: { + '$_chatId': {'favIndex': 0, 'dontDisturbUntil': 0}, + }, + ); + expect(parsed.favIndex, isNull); + }); + + test('without a config entry the cached pin survives', () { + expect(_parse(existing: _cached(favIndex: 5)).favIndex, 5); + }); + }); +} diff --git a/test/message_bubble_layout_test.dart b/test/message_bubble_layout_test.dart index 5f470d9..4e9802b 100644 --- a/test/message_bubble_layout_test.dart +++ b/test/message_bubble_layout_test.dart @@ -35,6 +35,56 @@ CachedMessage _message({ : null, ); +Future _pumpColumn( + WidgetTester tester, + List messages, { + required String chatType, + double textScale = 1, +}) async { + tester.view.physicalSize = const Size(1080, 2400); + tester.view.devicePixelRatio = 2.5; + addTearDown(tester.view.reset); + + await tester.pumpWidget( + MaterialApp( + locale: const Locale('ru'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: MediaQuery( + data: MediaQueryData(textScaler: TextScaler.linear(textScale)), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var i = 0; i < messages.length; i++) + MessageBubble( + key: ValueKey('bubble$i'), + message: messages[i], + prevMessage: i > 0 ? messages[i - 1] : null, + nextMessage: i < messages.length - 1 ? messages[i + 1] : null, + isMe: false, + myId: _me, + chatType: chatType, + ), + ], + ), + ), + ), + ), + ); + await tester.pump(); +} + +Rect _bubbleRect(WidgetTester tester, int index, String text) { + final label = find.descendant( + of: find.byKey(ValueKey('bubble$index')), + matching: find.text(text), + ); + final box = find.ancestor(of: label, matching: find.byType(Container)).first; + return tester.getTopLeft(box) & tester.getSize(box); +} + Future _pumpBubble(WidgetTester tester, CachedMessage message) async { tester.view.physicalSize = const Size(1080, 2400); tester.view.devicePixelRatio = 2.5; @@ -103,6 +153,36 @@ void main() { expect(clock.right, closeTo(header.right, 1)); }); + testWidgets('grouped bubbles keep the same gap with and without avatars', ( + tester, + ) async { + final stream = [ + for (var i = 0; i < 4; i++) + CachedMessage( + id: '$i', + accountId: _me, + chatId: 2, + senderId: 404, + text: 'm$i', + time: DateTime(2026, 1, 1, 12, 54).millisecondsSinceEpoch + i * 1000, + status: 'sent', + ), + ]; + + double gapAt(WidgetTester tester, int index) => + _bubbleRect(tester, index + 1, 'm${index + 1}').top - + _bubbleRect(tester, index, 'm$index').bottom; + + await _pumpColumn(tester, stream, chatType: 'DIALOG', textScale: 0.35); + final dialogGaps = [for (var i = 0; i < 3; i++) gapAt(tester, i)]; + + await _pumpColumn(tester, stream, chatType: 'CHAT', textScale: 0.35); + final groupGaps = [for (var i = 0; i < 3; i++) gapAt(tester, i)]; + + expect(dialogGaps, everyElement(2.0)); + expect(groupGaps, dialogGaps); + }); + testWidgets('a bubble without a header or reply still hugs its text', ( tester, ) async {