mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-07-17 23:54: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> {
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
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