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> {
bool _isVerified = false;
late bool _isVerified = widget.contact?.verified ?? true;
bool _isSharedVerified = false;
int _verifiedByTransferredTrustCount = 0;
int _sharedByVerifiedCount = 0;

View file

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

View file

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