fix some ui issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-07-13 23:04:49 +02:00
parent 8fc8907a87
commit 722466cc34
4 changed files with 247 additions and 83 deletions

View file

@ -33,7 +33,7 @@ class VerificationBadgeComp extends StatefulWidget {
} }
class _VerificationBadgeCompState extends State<VerificationBadgeComp> { class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
bool _isVerified = false; late bool _isVerified = widget.contact?.verified ?? true;
bool _isSharedVerified = false; bool _isSharedVerified = false;
int _verifiedByTransferredTrustCount = 0; int _verifiedByTransferredTrustCount = 0;
int _sharedByVerifiedCount = 0; int _sharedByVerifiedCount = 0;

View file

@ -4,7 +4,6 @@ import 'dart:collection';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:mutex/mutex.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
@ -88,8 +87,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
super.dispose(); super.dispose();
} }
Mutex protectMessageUpdating = Mutex();
bool _isViewActive() { bool _isViewActive() {
if (!mounted) return false; if (!mounted) return false;
return !AppState.isAppInBackground && return !AppState.isAppInBackground &&
@ -105,33 +102,29 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
_group = newGroup; _group = newGroup;
}); });
protectMessageUpdating.protect(() async { if (groupActionsSub == null) {
if (groupActionsSub == null) { final actionsStream = twonlyDB.groupsDao.watchGroupActions(
final actionsStream = twonlyDB.groupsDao.watchGroupActions( newGroup.groupId,
newGroup.groupId, );
); groupActionsSub = actionsStream.listen((update) async {
groupActionsSub = actionsStream.listen((update) async { groupActions = update;
groupActions = update; await setMessages(allMessages, update);
await setMessages(allMessages, update); });
});
final contactsStream = twonlyDB.contactsDao.watchAllContacts(); final contactsStream = twonlyDB.contactsDao.watchAllContacts();
contactSub = contactsStream.listen((contacts) { contactSub = contactsStream.listen((contacts) {
for (final contact in contacts) { for (final contact in contacts) {
userIdToContact[contact.userId] = contact; userIdToContact[contact.userId] = contact;
} }
}); });
} }
});
}); });
final msgStream = await twonlyDB.messagesDao.watchByGroupId(widget.groupId); final msgStream = await twonlyDB.messagesDao.watchByGroupId(widget.groupId);
messageSub = msgStream.listen((update) async { messageSub = msgStream.listen((update) async {
allMessages = update; allMessages = update;
await protectMessageUpdating.protect(() async { await setMessages(update, groupActions);
await setMessages(update, groupActions); _hasReceivedFirstMessageBatch = true;
_hasReceivedFirstMessageBatch = true;
});
}); });
final groupContacts = await twonlyDB.groupsDao.getGroupContact( final groupContacts = await twonlyDB.groupsDao.getGroupContact(
@ -158,7 +151,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
List<GroupHistory> groupActions, List<GroupHistory> groupActions,
) async { ) async {
if (_isViewActive()) { if (_isViewActive()) {
await flutterLocalNotificationsPlugin.cancelAll(); unawaited(flutterLocalNotificationsPlugin.cancelAll());
} }
for (final msg in newMessages) { for (final msg in newMessages) {
@ -222,18 +215,34 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
if (_isViewActive()) { if (_isViewActive()) {
for (final contactId in openedMessages.keys) { for (final contactId in openedMessages.keys) {
await notifyContactAboutOpeningMessage( unawaited(
contactId, notifyContactAboutOpeningMessage(
openedMessages[contactId]!, contactId,
openedMessages[contactId]!,
),
); );
} }
} }
final wasSentByMe =
_hasReceivedFirstMessageBatch &&
newMessages.isNotEmpty &&
newMessages.last.senderId == null;
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
messages = chatItems.reversed.toList(); messages = chatItems.reversed.toList();
}); });
if (wasSentByMe && itemScrollController.isAttached) {
unawaited(
itemScrollController.scrollTo(
index: 0,
duration: const Duration(milliseconds: 150),
),
);
}
final items = await MemoryItem.convertFromMessages(storedMediaFiles); final items = await MemoryItem.convertFromMessages(storedMediaFiles);
if (!mounted) return; if (!mounted) return;
galleryItems = items.values.toList(); galleryItems = items.values.toList();
@ -326,63 +335,66 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
itemCount: messages.length + 1 + 1, itemCount: messages.length + 1 + 1,
itemScrollController: itemScrollController, itemScrollController: itemScrollController,
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i == 0) { if (i == 0) {
return userService.currentUser.typingIndicators return userService.currentUser.typingIndicators
? TypingIndicator(group: group) ? TypingIndicator(group: group)
: Container(); : Container();
} }
i -= 1; i -= 1;
if (i == messages.length) { if (i == messages.length) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 10), key: Key('overview_${group.groupId}'),
child: InChatGroupOverview(group: group), padding: const EdgeInsets.only(top: 10),
); child: InChatGroupOverview(
}
if (messages[i].isDate) {
return ChatDateChip(
item: messages[i],
);
} else if (messages[i].isGroupAction) {
return ChatGroupAction(
key: Key(messages[i].groupAction!.groupHistoryId),
action: messages[i].groupAction!,
);
} else {
final chatMessage = messages[i].message!;
return BlinkWidget(
key: Key('blink_${chatMessage.messageId}'),
enabled: focusedScrollItem == i,
child: AnimatedNewMessage(
key: Key('anim_${chatMessage.messageId}'),
messageId: chatMessage.messageId,
animateIds: _animateMessageIds,
child: ChatListEntry(
key: Key(chatMessage.messageId),
message: messages[i].message!,
nextMessage: (i > 0)
? messages[i - 1].message
: null,
prevMessage: ((i + 1) < messages.length)
? messages[i + 1].message
: null,
group: group, group: group,
galleryItems: galleryItems,
userIdToContact: userIdToContact,
scrollToMessage: scrollToMessage,
onResponseTriggered: () {
setState(() {
quotesMessage = chatMessage;
});
textFieldFocus?.requestFocus();
},
), ),
), );
); }
} if (messages[i].isDate) {
}, return ChatDateChip(
item: messages[i],
);
} else if (messages[i].isGroupAction) {
return ChatGroupAction(
key: Key(messages[i].groupAction!.groupHistoryId),
action: messages[i].groupAction!,
);
} else {
final chatMessage = messages[i].message!;
return BlinkWidget(
key: Key('blink_${chatMessage.messageId}'),
enabled: focusedScrollItem == i,
child: AnimatedNewMessage(
key: Key('anim_${chatMessage.messageId}'),
messageId: chatMessage.messageId,
animateIds: _animateMessageIds,
child: ChatListEntry(
key: Key(chatMessage.messageId),
message: messages[i].message!,
nextMessage: (i > 0)
? messages[i - 1].message
: null,
prevMessage: ((i + 1) < messages.length)
? messages[i + 1].message
: null,
group: group,
galleryItems: galleryItems,
userIdToContact: userIdToContact,
scrollToMessage: scrollToMessage,
onResponseTriggered: () {
setState(() {
quotesMessage = chatMessage;
});
textFieldFocus?.requestFocus();
},
),
),
);
}
},
),
), ),
), ),
),
if (quotesMessage != null) if (quotesMessage != null)
Container( Container(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(

View file

@ -23,7 +23,10 @@ class InChatGroupOverview extends StatefulWidget {
State<InChatGroupOverview> createState() => _InChatGroupOverviewState(); State<InChatGroupOverview> createState() => _InChatGroupOverviewState();
} }
class _InChatGroupOverviewState extends State<InChatGroupOverview> { class _InChatGroupOverviewState extends State<InChatGroupOverview>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
Contact? _directContact; Contact? _directContact;
StreamSubscription<dynamic>? _verificationSub; StreamSubscription<dynamic>? _verificationSub;
StreamSubscription<List<(Contact, DateTime)>>? _transferredTrustSub; StreamSubscription<List<(Contact, DateTime)>>? _transferredTrustSub;
@ -113,6 +116,7 @@ class _InChatGroupOverviewState extends State<InChatGroupOverview> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
child: Center( child: Center(

View file

@ -0,0 +1,148 @@
import 'package:drift/drift.dart' hide isNotNull, isNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/services/user.service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() async {
await locator.reset();
locator
..registerSingleton<TwonlyDB>(
TwonlyDB.forTesting(
DatabaseConnection(
NativeDatabase.memory(),
closeStreamsSynchronously: true,
),
),
)
..registerSingleton<UserService>(UserService());
userService.currentUser = UserData(
userId: 1,
username: 'test_user',
displayName: 'Test User',
subscriptionPlan: 'Free',
currentSetupPage: null,
appVersion: 100,
);
userService.isUserCreated = true;
});
tearDown(() async {
await twonlyDB.close();
});
test(
'watchByGroupId stream emits updates when a new text message is inserted',
() async {
// 1. Create a group
await twonlyDB.groupsDao.createNewGroup(
GroupsCompanion.insert(
groupId: 'test_group_stream',
groupName: 'Test Group Stream',
),
);
// 2. Start watching
final stream = await twonlyDB.messagesDao.watchByGroupId(
'test_group_stream',
);
final emissions = <List<Message>>[];
final subscription = stream.listen(emissions.add);
// Wait briefly for initial empty emission
await Future.delayed(const Duration(milliseconds: 100));
expect(emissions, hasLength(1));
expect(emissions.first, isEmpty);
// 3. Insert a message
await twonlyDB.messagesDao.insertMessage(
MessagesCompanion.insert(
messageId: 'msg_1',
groupId: 'test_group_stream',
type: 'text',
content: const Value('Hello world'),
),
);
// Wait for the stream to propagate the event
await Future.delayed(const Duration(milliseconds: 100));
// Verify it emitted the new list containing our message
expect(emissions, hasLength(2));
expect(emissions[1], hasLength(1));
expect(emissions[1].first.messageId, equals('msg_1'));
await subscription.cancel();
},
);
test(
'simulate sending flow with group updates and verify messages stream emits',
() async {
await twonlyDB.groupsDao.createNewGroup(
GroupsCompanion.insert(
groupId: 'test_group_2',
groupName: 'Test Group 2',
),
);
final msgStream = await twonlyDB.messagesDao.watchByGroupId(
'test_group_2',
);
final messagesEmissions = <List<Message>>[];
final msgSub = msgStream.listen(messagesEmissions.add);
final groupStream = twonlyDB.groupsDao.watchGroup('test_group_2');
final groupEmissions = <Group?>[];
final groupSub = groupStream.listen(groupEmissions.add);
await Future.delayed(const Duration(milliseconds: 100));
expect(messagesEmissions, hasLength(1));
expect(groupEmissions, hasLength(1));
// Simulate sending:
// 1. Update group draftMessage to null (as done in insertAndSendTextMessage)
await twonlyDB.groupsDao.updateGroup(
'test_group_2',
const GroupsCompanion(
draftMessage: Value(null),
),
);
// 2. Insert message
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
groupId: const Value('test_group_2'),
content: const Value('Hello from simulation'),
type: Value(MessageType.text.name),
),
);
expect(message, isNotNull);
await Future.delayed(const Duration(milliseconds: 100));
// Verify group stream received updates
expect(groupEmissions.length, greaterThan(1));
// Verify message stream received updates containing the new message
expect(messagesEmissions.length, greaterThan(1));
expect(
messagesEmissions.last.any((m) => m.messageId == message!.messageId),
isTrue,
);
await msgSub.cancel();
await groupSub.cancel();
},
);
}