mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-07-17 22:14:08 +00:00
fix some ui issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
8fc8907a87
commit
722466cc34
4 changed files with 247 additions and 83 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
148
test/services/messages_stream_test.dart
Normal file
148
test/services/messages_stream_test.dart
Normal 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();
|
||||
},
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue