From c59828df9074687a859dc65482ee5d32c341c788 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 7 Aug 2023 00:55:57 -0400 Subject: [PATCH] local messages --- assets/i18n/en.json | 1 + lib/components/chat_component.dart | 147 ++++++++++++------ lib/components/chat_list_widget.dart | 92 ----------- .../chat_single_contact_item_widget.dart | 3 - .../chat_single_contact_list_widget.dart | 80 ++++++++++ lib/components/contact_item_widget.dart | 21 ++- lib/components/empty_chat_list_widget.dart | 2 +- lib/pages/home.dart | 74 ++++++++- lib/pages/main_pager/chats_page.dart | 15 +- lib/providers/chat.dart | 5 + lib/providers/conversation.dart | 106 +++++++++---- .../dht_support/dht_record_pool.dart | 30 ++-- 12 files changed, 375 insertions(+), 201 deletions(-) delete mode 100644 lib/components/chat_list_widget.dart create mode 100644 lib/components/chat_single_contact_list_widget.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 4fe6ee6..f5ad568 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -99,6 +99,7 @@ "invitation": "Invitation" }, "chat_list": { + "search": "Search", "start_a_conversation": "Start a conversation", "conversations": "Conversations", "groups": "Groups" diff --git a/lib/components/chat_component.dart b/lib/components/chat_component.dart index d250cd8..4ed6433 100644 --- a/lib/components/chat_component.dart +++ b/lib/components/chat_component.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; @@ -6,13 +8,22 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; import '../../entities/proto.dart' as proto; -import '../providers/chat.dart'; -import '../providers/contact.dart'; +import '../entities/identity.dart'; +import '../providers/account.dart'; +import '../providers/conversation.dart'; import '../tools/theme_service.dart'; -import 'empty_chat_widget.dart'; +import '../veilid_support/veilid_support.dart'; class ChatComponent extends ConsumerStatefulWidget { - const ChatComponent({super.key}); + const ChatComponent( + {required this.activeAccountInfo, + required this.activeChat, + required this.activeChatContact, + super.key}); + + final ActiveAccountInfo activeAccountInfo; + final TypedKey activeChat; + final proto.Contact activeChatContact; @override ChatComponentState createState() => ChatComponentState(); @@ -21,11 +32,26 @@ class ChatComponent extends ConsumerStatefulWidget { class ChatComponentState extends ConsumerState { List _messages = []; final _unfocusNode = FocusNode(); + late final types.User _localUser; + late final types.User _remoteUser; @override void initState() { super.initState(); - _loadMessages(); + + _localUser = types.User( + id: widget.activeAccountInfo.localAccount.identityMaster + .identityPublicTypedKey() + .toString(), + firstName: widget.activeAccountInfo.account.profile.name, + ); + _remoteUser = types.User( + id: proto.TypedKeyProto.fromProto( + widget.activeChatContact.identityPublicKey) + .toString(), + firstName: widget.activeChatContact.remoteProfile.name); + + unawaited(_loadMessages()); } @override @@ -34,39 +60,76 @@ class ChatComponentState extends ConsumerState { super.dispose(); } - void _loadMessages() { - final messages = [ - types.TextMessage( - id: "abcd", - text: "Hello!", - author: types.User( - id: "1234", - firstName: "Foo", - lastName: "Bar", - role: types.Role.user)) - ]; - _messages = messages; - } - - final _user = const types.User( - id: '82091008-a484-4a89-ae75-a22bf8d6f3ac', - ); - - void _addMessage(types.Message message) { + Future _loadMessages() async { + final localConversationOwned = proto.OwnedDHTRecordPointerProto.fromProto( + widget.activeChatContact.localConversation); + final remoteIdentityPublicKey = proto.TypedKeyProto.fromProto( + widget.activeChatContact.identityPublicKey); + final protoMessages = await getLocalConversationMessages( + activeAccountInfo: widget.activeAccountInfo, + localConversationOwned: localConversationOwned, + remoteIdentityPublicKey: remoteIdentityPublicKey); + if (protoMessages == null) { + return; + } setState(() { - _messages.insert(0, message); + _messages = []; + for (final protoMessage in protoMessages) { + final message = protoMessageToMessage(protoMessage); + _messages.insert(0, message); + } }); } - void _handleSendPressed(types.PartialText message) { + types.Message protoMessageToMessage(proto.Message message) { + final isLocal = message.author == + widget.activeAccountInfo.localAccount.identityMaster + .identityPublicTypedKey() + .toProto(); + final textMessage = types.TextMessage( - author: _user, + author: isLocal ? _localUser : _remoteUser, createdAt: DateTime.now().millisecondsSinceEpoch, id: const Uuid().v4(), text: message.text, ); + return textMessage; + } - _addMessage(textMessage); + Future _addMessage(proto.Message protoMessage) async { + if (protoMessage.text.isEmpty) { + return; + } + + final message = protoMessageToMessage(protoMessage); + + setState(() { + _messages.insert(0, message); + }); + + // Now add the message to the conversation messages + final localConversationOwned = proto.OwnedDHTRecordPointerProto.fromProto( + widget.activeChatContact.localConversation); + final remoteIdentityPublicKey = proto.TypedKeyProto.fromProto( + widget.activeChatContact.identityPublicKey); + + await addLocalConversationMessage( + activeAccountInfo: widget.activeAccountInfo, + localConversationOwned: localConversationOwned, + remoteIdentityPublicKey: remoteIdentityPublicKey, + message: protoMessage); + } + + Future _handleSendPressed(types.PartialText message) async { + final protoMessage = proto.Message() + ..author = widget.activeAccountInfo.localAccount.identityMaster + .identityPublicTypedKey() + .toProto() + ..timestamp = (await eventualVeilid.future).now().toInt64() + ..text = message.text; + //..signature = signature; + + await _addMessage(protoMessage); } void _handleAttachmentPressed() { @@ -80,25 +143,7 @@ class ChatComponentState extends ConsumerState { final scale = theme.extension()!; final chatTheme = scale.toChatTheme(); final textTheme = Theme.of(context).textTheme; - - final contactList = ref.watch(fetchContactListProvider).asData?.value ?? - const IListConst([]); - - final activeChat = ref.watch(activeChatStateProvider).asData?.value; - - if (activeChat == null) { - return const EmptyChatWidget(); - } - - final activeChatContactIdx = contactList.indexWhere( - (c) => - proto.TypedKeyProto.fromProto(c.remoteConversationKey) == activeChat, - ); - if (activeChatContactIdx == -1) { - activeChatState.add(null); - return const EmptyChatWidget(); - } - final activeChatContact = contactList[activeChatContactIdx]; + final contactName = widget.activeChatContact.editedProfile.name; return DefaultTextStyle( style: textTheme.bodySmall!, @@ -118,7 +163,7 @@ class ChatComponentState extends ConsumerState { child: Padding( padding: const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0), - child: Text(activeChatContact.editedProfile.name, + child: Text(contactName, textAlign: TextAlign.start, style: textTheme.titleMedium), ), @@ -134,10 +179,12 @@ class ChatComponentState extends ConsumerState { //onMessageTap: _handleMessageTap, //onPreviewDataFetched: _handlePreviewDataFetched, - onSendPressed: _handleSendPressed, + onSendPressed: (message) { + unawaited(_handleSendPressed(message)); + }, showUserAvatars: true, showUserNames: true, - user: _user, + user: _localUser, ), ), ), diff --git a/lib/components/chat_list_widget.dart b/lib/components/chat_list_widget.dart deleted file mode 100644 index 5246737..0000000 --- a/lib/components/chat_list_widget.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:searchable_listview/searchable_listview.dart'; - -import '../../entities/proto.dart' as proto; -import '../tools/tools.dart'; -import 'chat_single_contact_item_widget.dart'; -import 'contact_item_widget.dart'; -import 'empty_chat_list_widget.dart'; -import 'empty_contact_list_widget.dart'; - -class ChatListWidget extends ConsumerWidget { - ChatListWidget( - {required IList contactList, - required this.chatList, - super.key}) - : contactMap = IMap.fromIterable(contactList, - keyMapper: (c) => c.remoteConversationKey, valueMapper: (c) => c); - - final IMap contactMap; - final IList chatList; - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; - final scale = theme.extension()!; - - return Container( - width: double.infinity, - constraints: const BoxConstraints( - minHeight: 64, - ), - child: Column(children: [ - Text( - 'Chats', - style: textTheme.bodyLarge, - ).paddingAll(8), - Container( - width: double.infinity, - decoration: ShapeDecoration( - color: scale.grayScale.appBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - )), - child: (chatList.isEmpty) - ? const EmptyChatListWidget().toCenter() - : SearchableList( - initialList: chatList.toList(), - builder: (c) { - final contact = contactMap[c.remoteConversationKey]; - if (contact == null) { - return const Text('...'); - } - return ChatSingleContactItemWidget(contact: contact); - }, - filter: (value) { - final lowerValue = value.toLowerCase(); - return chatList.where((c) { - final contact = contactMap[c.remoteConversationKey]; - if (contact == null) { - return false; - } - return contact.editedProfile.name - .toLowerCase() - .contains(lowerValue) || - contact.editedProfile.title - .toLowerCase() - .contains(lowerValue); - }).toList(); - }, - inputDecoration: InputDecoration( - labelText: translate('chat_list.search'), - fillColor: Colors.white, - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide( - color: Colors.blue, - ), - borderRadius: BorderRadius.circular(10), - ), - ), - ), - ).expanded() - ]), - ).paddingLTRB(8, 0, 8, 65); - } -} diff --git a/lib/components/chat_single_contact_item_widget.dart b/lib/components/chat_single_contact_item_widget.dart index 18931f3..964e494 100644 --- a/lib/components/chat_single_contact_item_widget.dart +++ b/lib/components/chat_single_contact_item_widget.dart @@ -74,9 +74,6 @@ class ChatSingleContactItemWidget extends ConsumerWidget { activeChatState.add(proto.TypedKeyProto.fromProto( contact.remoteConversationKey)); ref.invalidate(fetchChatListProvider); - // Click over to chats - await MainPager.of(context)?.pageController.animateToPage(1, - duration: 250.ms, curve: Curves.easeInOut); }, title: Text(contact.editedProfile.name), diff --git a/lib/components/chat_single_contact_list_widget.dart b/lib/components/chat_single_contact_list_widget.dart new file mode 100644 index 0000000..426402d --- /dev/null +++ b/lib/components/chat_single_contact_list_widget.dart @@ -0,0 +1,80 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:searchable_listview/searchable_listview.dart'; + +import '../../entities/proto.dart' as proto; +import '../tools/tools.dart'; +import 'chat_single_contact_item_widget.dart'; +import 'contact_item_widget.dart'; +import 'empty_chat_list_widget.dart'; +import 'empty_contact_list_widget.dart'; + +class ChatSingleContactListWidget extends ConsumerWidget { + ChatSingleContactListWidget( + {required IList contactList, + required this.chatList, + super.key}) + : contactMap = IMap.fromIterable(contactList, + keyMapper: (c) => c.remoteConversationKey, valueMapper: (c) => c); + + final IMap contactMap; + final IList chatList; + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final scale = theme.extension()!; + + return Container( + width: double.infinity, + decoration: ShapeDecoration( + color: scale.grayScale.appBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + )), + child: (chatList.isEmpty) + ? const EmptyChatListWidget() + : SearchableList( + initialList: chatList.toList(), + builder: (c) { + final contact = contactMap[c.remoteConversationKey]; + if (contact == null) { + return const Text('...'); + } + return ChatSingleContactItemWidget(contact: contact); + }, + filter: (value) { + final lowerValue = value.toLowerCase(); + return chatList.where((c) { + final contact = contactMap[c.remoteConversationKey]; + if (contact == null) { + return false; + } + return contact.editedProfile.name + .toLowerCase() + .contains(lowerValue) || + contact.editedProfile.title + .toLowerCase() + .contains(lowerValue); + }).toList(); + }, + inputDecoration: InputDecoration( + labelText: translate('chat_list.search'), + fillColor: Colors.white, + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide( + color: Colors.blue, + ), + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ).paddingLTRB(8, 8, 8, 65); + } +} diff --git a/lib/components/contact_item_widget.dart b/lib/components/contact_item_widget.dart index 374642b..6088e31 100644 --- a/lib/components/contact_item_widget.dart +++ b/lib/components/contact_item_widget.dart @@ -1,9 +1,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../entities/proto.dart' as proto; +import '../pages/main_pager/main_pager.dart'; import '../providers/account.dart'; import '../providers/chat.dart'; import '../providers/contact.dart'; @@ -21,6 +23,9 @@ class ContactItemWidget extends ConsumerWidget { final textTheme = theme.textTheme; final scale = theme.extension()!; + final remoteConversationKey = + proto.TypedKeyProto.fromProto(contact.remoteConversationKey); + return Container( margin: const EdgeInsets.fromLTRB(4, 4, 4, 0), clipBehavior: Clip.antiAlias, @@ -66,9 +71,19 @@ class ContactItemWidget extends ConsumerWidget { // component is not dragged. child: ListTile( onTap: () async { - // final activeAccountInfo = - // await ref.read(fetchActiveAccountProvider.future); - // if (activeAccountInfo != null) { + final activeAccountInfo = + await ref.read(fetchActiveAccountProvider.future); + if (activeAccountInfo != null) { + // Start a chat + await getOrCreateChatSingleContact( + activeAccountInfo: activeAccountInfo, + remoteConversationRecordKey: remoteConversationKey); + + // Click over to chats + await MainPager.of(context)?.pageController.animateToPage(1, + duration: 250.ms, curve: Curves.easeInOut); + } + // // ignore: use_build_context_synchronously // if (!context.mounted) { // return; diff --git a/lib/components/empty_chat_list_widget.dart b/lib/components/empty_chat_list_widget.dart index 9698b9e..b525666 100644 --- a/lib/components/empty_chat_list_widget.dart +++ b/lib/components/empty_chat_list_widget.dart @@ -30,6 +30,6 @@ class EmptyChatListWidget extends ConsumerWidget { ), ), ], - ).expanded(); + ); } } diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 6c087a0..507f6a5 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,13 +1,17 @@ import 'dart:async'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:split_view/split_view.dart'; import 'package:signal_strength_indicator/signal_strength_indicator.dart'; +import '../../entities/proto.dart' as proto; import '../components/chat_component.dart'; +import '../components/empty_chat_widget.dart'; import '../providers/account.dart'; +import '../providers/chat.dart'; import '../providers/contact.dart'; import '../providers/contact_invite.dart'; import '../providers/window_control.dart'; @@ -24,6 +28,7 @@ class HomePage extends ConsumerStatefulWidget { // XXX Eliminate this when we have ValueChanged const int ticksPerContactInvitationCheck = 5; +const int ticksPerNewMessageCheck = 5; class HomePageState extends ConsumerState with TickerProviderStateMixin { @@ -32,6 +37,7 @@ class HomePageState extends ConsumerState Timer? _homeTickTimer; bool _inHomeTick = false; int _contactInvitationCheckTick = 0; + int _newMessageCheckTick = 0; @override void initState() { @@ -63,11 +69,22 @@ class HomePageState extends ConsumerState Future _onHomeTick() async { _inHomeTick = true; try { - // Check extant contact invitations once every 5 seconds + final unord = >[]; + // Check extant contact invitations once every N seconds _contactInvitationCheckTick += 1; if (_contactInvitationCheckTick >= ticksPerContactInvitationCheck) { _contactInvitationCheckTick = 0; - await _doContactInvitationCheck(); + unord.add(_doContactInvitationCheck()); + } + + // Check new messages once every N seconds + _newMessageCheckTick += 1; + if (_newMessageCheckTick >= ticksPerNewMessageCheck) { + _newMessageCheckTick = 0; + unord.add(_doNewMessageCheck()); + } + if (unord.isNotEmpty) { + await Future.wait(unord); } } finally { _inHomeTick = false; @@ -112,6 +129,26 @@ class HomePageState extends ConsumerState await Future.wait(allChecks); } + Future _doNewMessageCheck() async { + final activeChat = activeChatState.currentState; + if (activeChat == null) { + return; + } + final contactList = ref.read(fetchContactListProvider).asData?.value ?? + const IListConst([]); + + final activeChatContactIdx = contactList.indexWhere( + (c) => + proto.TypedKeyProto.fromProto(c.remoteConversationKey) == activeChat, + ); + if (activeChatContactIdx == -1) { + return; + } + final activeChatContact = contactList[activeChatContactIdx]; + + //activeChatContact.rem + } + // ignore: prefer_expression_function_bodies Widget buildPhone(BuildContext context) { // @@ -129,7 +166,38 @@ class HomePageState extends ConsumerState // ignore: prefer_expression_function_bodies Widget buildTabletRightPane(BuildContext context) { // - return ChatComponent(); + return buildChatComponent(context); + } + + Widget buildChatComponent(BuildContext context) { + final contactList = ref.watch(fetchContactListProvider).asData?.value ?? + const IListConst([]); + + final activeChat = ref.watch(activeChatStateProvider).asData?.value; + if (activeChat == null) { + return const EmptyChatWidget(); + } + + final activeAccountInfo = + ref.watch(fetchActiveAccountProvider).asData?.value; + if (activeAccountInfo == null) { + return const EmptyChatWidget(); + } + + final activeChatContactIdx = contactList.indexWhere( + (c) => + proto.TypedKeyProto.fromProto(c.remoteConversationKey) == activeChat, + ); + if (activeChatContactIdx == -1) { + activeChatState.add(null); + return const EmptyChatWidget(); + } + final activeChatContact = contactList[activeChatContactIdx]; + + return ChatComponent( + activeAccountInfo: activeAccountInfo, + activeChat: activeChat, + activeChatContact: activeChatContact); } // ignore: prefer_expression_function_bodies diff --git a/lib/pages/main_pager/chats_page.dart b/lib/pages/main_pager/chats_page.dart index fbabc58..c12c2e4 100644 --- a/lib/pages/main_pager/chats_page.dart +++ b/lib/pages/main_pager/chats_page.dart @@ -1,9 +1,10 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../../components/chat_list_widget.dart'; +import '../../components/chat_single_contact_list_widget.dart'; import '../../components/empty_chat_list_widget.dart'; import '../../entities/local_account.dart'; import '../../entities/proto.dart' as proto; @@ -52,14 +53,10 @@ class ChatsPageState extends ConsumerState { return Column(children: [ if (chatList.isNotEmpty) - ExpansionTile( - title: Text(translate('chat_page.conversations')), - initiallyExpanded: true, - children: [ - ChatListWidget(contactList: contactList, chatList: chatList) - ], - ), - if (chatList.isEmpty) const EmptyChatListWidget(), + ChatSingleContactListWidget( + contactList: contactList, chatList: chatList) + .expanded(), + if (chatList.isEmpty) const EmptyChatListWidget().expanded(), ]); } diff --git a/lib/providers/chat.dart b/lib/providers/chat.dart index 29479c1..0865707 100644 --- a/lib/providers/chat.dart +++ b/lib/providers/chat.dart @@ -73,6 +73,11 @@ Future deleteChat( final c = Chat.fromBuffer(cbuf); if (c.remoteConversationKey == remoteConversationKey) { await chatList.tryRemoveItem(i); + + if (activeChatState.currentState == remoteConversationRecordKey) { + activeChatState.add(null); + } + return; } } diff --git a/lib/providers/conversation.dart b/lib/providers/conversation.dart index 8988088..11d8094 100644 --- a/lib/providers/conversation.dart +++ b/lib/providers/conversation.dart @@ -123,33 +123,85 @@ Future writeLocalConversation({ }); } +Future readLocalConversation({ + required ActiveAccountInfo activeAccountInfo, + required OwnedDHTRecordPointer localConversationOwned, + required TypedKey remoteIdentityPublicKey, +}) async { + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final pool = await DHTRecordPool.instance(); -/// Get most recent messages for this conversation -// @riverpod -// Future?> fetchConversationMessages(FetchContactListRef ref) async { -// // See if we've logged into this account or if it is locked -// final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future); -// if (activeAccountInfo == null) { -// return null; -// } -// final accountRecordKey = -// activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final crypto = await getConversationCrypto( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: remoteIdentityPublicKey); -// // Decode the contact list from the DHT -// IList out = const IListConst([]); -// await (await DHTShortArray.openOwned( -// proto.OwnedDHTRecordPointerProto.fromProto( -// activeAccountInfo.account.contactList), -// parent: accountRecordKey)) -// .scope((cList) async { -// for (var i = 0; i < cList.length; i++) { -// final cir = await cList.getItem(i); -// if (cir == null) { -// throw Exception('Failed to get contact'); -// } -// out = out.add(Contact.fromBuffer(cir)); -// } -// }); + return (await pool.openOwned(localConversationOwned, + parent: accountRecordKey, crypto: crypto)) + .scope((localConversation) async { + // + final update = await localConversation.getProtobuf(Conversation.fromBuffer); + if (update != null) { + return update; + } + return null; + }); +} -// return out; -// } +Future addLocalConversationMessage( + {required ActiveAccountInfo activeAccountInfo, + required OwnedDHTRecordPointer localConversationOwned, + required TypedKey remoteIdentityPublicKey, + required proto.Message message}) async { + final conversation = await readLocalConversation( + activeAccountInfo: activeAccountInfo, + localConversationOwned: localConversationOwned, + remoteIdentityPublicKey: remoteIdentityPublicKey); + if (conversation == null) { + return; + } + final messagesOwned = + proto.OwnedDHTRecordPointerProto.fromProto(conversation.messages); + final crypto = await getConversationCrypto( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: remoteIdentityPublicKey); + + await (await DHTShortArray.openOwned(messagesOwned, + parent: localConversationOwned.recordKey, crypto: crypto)) + .scope((messages) async { + await messages.tryAddItem(message.writeToBuffer()); + }); +} + +Future?> getLocalConversationMessages({ + required ActiveAccountInfo activeAccountInfo, + required OwnedDHTRecordPointer localConversationOwned, + required TypedKey remoteIdentityPublicKey, +}) async { + final conversation = await readLocalConversation( + activeAccountInfo: activeAccountInfo, + localConversationOwned: localConversationOwned, + remoteIdentityPublicKey: remoteIdentityPublicKey); + if (conversation == null) { + return null; + } + final messagesOwned = + proto.OwnedDHTRecordPointerProto.fromProto(conversation.messages); + final crypto = await getConversationCrypto( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: remoteIdentityPublicKey); + + return (await DHTShortArray.openOwned(messagesOwned, + parent: localConversationOwned.recordKey, crypto: crypto)) + .scope((messages) async { + var out = IList(); + for (var i = 0; i < messages.length; i++) { + final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); + if (msg == null) { + throw Exception('Failed to get message'); + } + out = out.add(msg); + } + return out; + }); +} diff --git a/lib/veilid_support/dht_support/dht_record_pool.dart b/lib/veilid_support/dht_support/dht_record_pool.dart index 9d90ce5..507796f 100644 --- a/lib/veilid_support/dht_support/dht_record_pool.dart +++ b/lib/veilid_support/dht_support/dht_record_pool.dart @@ -72,23 +72,27 @@ class DHTRecordPool with AsyncTableDBBacked { Object? valueToJson(DHTRecordPoolAllocations val) => val.toJson(); ////////////////////////////////////////////////////////////// + static Mutex instanceSetupMutex = Mutex(); + // ignore: prefer_expression_function_bodies static Future instance() async { - if (_singleton == null) { - final veilid = await eventualVeilid.future; - final routingContext = (await veilid.routingContext()) - .withPrivacy() - .withSequencing(Sequencing.preferOrdered); + return instanceSetupMutex.protect(() async { + if (_singleton == null) { + final veilid = await eventualVeilid.future; + final routingContext = (await veilid.routingContext()) + .withPrivacy() + .withSequencing(Sequencing.preferOrdered); - final globalPool = DHTRecordPool._(veilid, routingContext); - try { - globalPool._state = await globalPool.load(); - } on Exception catch (e) { - log.error('Failed to load DHTRecordPool: $e'); + final globalPool = DHTRecordPool._(veilid, routingContext); + try { + globalPool._state = await globalPool.load(); + } on Exception catch (e) { + log.error('Failed to load DHTRecordPool: $e'); + } + _singleton = globalPool; } - _singleton = globalPool; - } - return _singleton!; + return _singleton!; + }); } Veilid get veilid => _veilid;