From 17211f3515f1f0ded8bfd30d9bf19fba6b9a3deb Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 20 Jun 2024 19:04:39 -0400 Subject: [PATCH] checkpoint --- ...per_account_collection_bloc_map_cubit.dart | 18 +- .../cubits/per_account_collection_cubit.dart | 45 ++-- .../views/edit_account_page.dart | 9 +- lib/chat/cubits/chat_component_cubit.dart | 46 +++- lib/chat/views/chat_component_widget.dart | 10 +- lib/chat_list/cubits/chat_list_cubit.dart | 42 +++- lib/chat_list/views/chat_list_widget.dart | 96 ++++++++ .../chat_single_contact_list_widget.dart | 76 ------- lib/chat_list/views/views.dart | 2 +- .../cubits/waiting_invitation_cubit.dart | 7 +- .../waiting_invitations_bloc_map_cubit.dart | 66 +++++- .../active_conversations_bloc_map_cubit.dart | 91 +++++--- ...ve_single_contact_chat_bloc_map_cubit.dart | 134 +++++------ .../main_pager/chats_page.dart | 2 +- lib/layout/home/home_screen.dart | 61 +---- lib/proto/extensions.dart | 13 ++ lib/proto/veilidchat.pb.dart | 209 +++++++++++++++--- lib/proto/veilidchat.pbjson.dart | 73 ++++-- lib/proto/veilidchat.proto | 30 ++- .../src/dht_log/dht_log_spine.dart | 2 +- .../dht_record/default_dht_record_cubit.dart | 8 - .../src/dht_record/dht_record_cubit.dart | 14 -- 22 files changed, 701 insertions(+), 353 deletions(-) create mode 100644 lib/chat_list/views/chat_list_widget.dart delete mode 100644 lib/chat_list/views/chat_single_contact_list_widget.dart diff --git a/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart b/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart index f1df6e6..f5334c1 100644 --- a/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart @@ -38,9 +38,23 @@ class PerAccountCollectionBlocMapCubit extends BlocMapCubit removeFromState(TypedKey key) => remove(key); @override - Future updateState(TypedKey key, LocalAccount value) async { + Future updateState( + TypedKey key, LocalAccount? oldValue, LocalAccount newValue) async { + // Don't replace unless this is a totally different account + // The sub-cubit's subscription will update our state later + if (oldValue != null) { + if (oldValue.superIdentity.recordKey != + newValue.superIdentity.recordKey) { + throw StateError( + 'should remove LocalAccount and make a new one, not change it, if ' + 'the superidentity record key has changed'); + } + // This never changes anything that should result in rebuildin the + // sub-cubit + return; + } await _addPerAccountCollectionCubit( - superIdentityRecordKey: value.superIdentity.recordKey); + superIdentityRecordKey: newValue.superIdentity.recordKey); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/account_manager/cubits/per_account_collection_cubit.dart b/lib/account_manager/cubits/per_account_collection_cubit.dart index 8b8600b..6cb8d5d 100644 --- a/lib/account_manager/cubits/per_account_collection_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_cubit.dart @@ -136,15 +136,17 @@ class PerAccountCollectionCubit extends Cubit { : (accountInfo, contactListRecordPointer)); // WaitingInvitationsBlocMapCubit - final waitingInvitationsBlocMapCubit = - waitingInvitationsBlocMapCubitUpdater.update( - accountInfo.userLogin == null || contactInvitationListCubit == null - ? null - : ( - accountInfo, - accountRecordCubit!, - contactInvitationListCubit - )); + final waitingInvitationsBlocMapCubit = waitingInvitationsBlocMapCubitUpdater + .update(accountInfo.userLogin == null || + contactInvitationListCubit == null || + contactListCubit == null + ? null + : ( + accountInfo, + accountRecordCubit!, + contactInvitationListCubit, + contactListCubit, + )); // ActiveChatCubit final activeChatCubit = activeChatCubitUpdater @@ -179,15 +181,11 @@ class PerAccountCollectionCubit extends Cubit { final activeSingleContactChatBlocMapCubit = activeSingleContactChatBlocMapCubitUpdater.update( accountInfo.userLogin == null || - activeConversationsBlocMapCubit == null || - chatListCubit == null || - contactListCubit == null + activeConversationsBlocMapCubit == null ? null : ( accountInfo, activeConversationsBlocMapCubit, - chatListCubit, - contactListCubit )); // Update available blocs in our state @@ -260,11 +258,18 @@ class PerAccountCollectionCubit extends Cubit { )); final waitingInvitationsBlocMapCubitUpdater = BlocUpdater< WaitingInvitationsBlocMapCubit, - (AccountInfo, AccountRecordCubit, ContactInvitationListCubit)>( + ( + AccountInfo, + AccountRecordCubit, + ContactInvitationListCubit, + ContactListCubit + )>( create: (params) => WaitingInvitationsBlocMapCubit( - accountInfo: params.$1, - accountRecordCubit: params.$2, - contactInvitationListCubit: params.$3)); + accountInfo: params.$1, + accountRecordCubit: params.$2, + contactInvitationListCubit: params.$3, + contactListCubit: params.$4, + )); final activeChatCubitUpdater = BlocUpdater(create: (_) => ActiveChatCubit(null)); final chatListCubitUpdater = BlocUpdater { ( AccountInfo, ActiveConversationsBlocMapCubit, - ChatListCubit, - ContactListCubit )>( create: (params) => ActiveSingleContactChatBlocMapCubit( accountInfo: params.$1, activeConversationsBlocMapCubit: params.$2, - chatListCubit: params.$3, - contactListCubit: params.$4, )); } diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 468bf48..0e57d77 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -116,7 +116,14 @@ class _EditAccountPageState extends State { }); try { // Look up account cubit for this specific account - final accountRecordCubit = context.read(); + final perAccountCollectionBlocMapCubit = + context.read(); + final accountRecordCubit = await perAccountCollectionBlocMapCubit + .operate(widget.superIdentityRecordKey, + closure: (c) async => c.accountRecordCubit); + if (accountRecordCubit == null) { + return; + } // Update account profile DHT record // This triggers ConversationCubits to update diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 05c722c..cd0f724 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -12,6 +12,7 @@ import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../contacts/contacts.dart'; import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; import '../models/chat_component_state.dart'; @@ -28,10 +29,12 @@ class ChatComponentCubit extends Cubit { ChatComponentCubit._({ required AccountInfo accountInfo, required AccountRecordCubit accountRecordCubit, + required ContactListCubit contactListCubit, required List conversationCubits, required SingleContactMessagesCubit messagesCubit, }) : _accountInfo = accountInfo, _accountRecordCubit = accountRecordCubit, + _contactListCubit = contactListCubit, _conversationCubits = conversationCubits, _messagesCubit = messagesCubit, super(ChatComponentState( @@ -51,11 +54,13 @@ class ChatComponentCubit extends Cubit { factory ChatComponentCubit.singleContact( {required AccountInfo accountInfo, required AccountRecordCubit accountRecordCubit, + required ContactListCubit contactListCubit, required ActiveConversationCubit activeConversationCubit, required SingleContactMessagesCubit messagesCubit}) => ChatComponentCubit._( accountInfo: accountInfo, accountRecordCubit: accountRecordCubit, + contactListCubit: contactListCubit, conversationCubits: [activeConversationCubit], messagesCubit: messagesCubit, ); @@ -82,6 +87,7 @@ class ChatComponentCubit extends Cubit { await _initWait(); await _accountRecordSubscription.cancel(); await _messagesSubscription.cancel(); + await _conversationSubscriptions.values.map((v) => v.cancel()).wait; await super.close(); } @@ -146,12 +152,12 @@ class ChatComponentCubit extends Cubit { // Private Implementation void _onChangedAccountRecord(AsyncValue avAccount) { + // Update local 'User' final account = avAccount.asData?.value; if (account == null) { emit(state.copyWith(localUser: null)); return; } - // Make local 'User' final localUser = types.User( id: _localUserIdentityKey.toString(), firstName: account.profile.name, @@ -168,15 +174,40 @@ class ChatComponentCubit extends Cubit { TypedKey remoteIdentityPublicKey, AsyncValue avConversationState, ) { - // + // Update remote 'User' + final activeConversationState = avConversationState.asData?.value; + if (activeConversationState == null) { + // Don't change user information on loading state + return; + } + emit(state.copyWith( + remoteUsers: state.remoteUsers.add( + remoteIdentityPublicKey, + _convertRemoteUser( + remoteIdentityPublicKey, activeConversationState)))); } types.User _convertRemoteUser(TypedKey remoteIdentityPublicKey, - ActiveConversationState activeConversationState) => - types.User( - id: remoteIdentityPublicKey.toString(), - firstName: activeConversationState.contact.displayName, - metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); + ActiveConversationState activeConversationState) { + // See if we have a contact for this remote user + final contacts = _contactListCubit.state.state.asData?.value; + if (contacts != null) { + final contactIdx = contacts.indexWhere((x) => + x.value.identityPublicKey.toVeilid() == remoteIdentityPublicKey); + if (contactIdx != -1) { + final contact = contacts[contactIdx].value; + return types.User( + id: remoteIdentityPublicKey.toString(), + firstName: contact.displayName, + metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); + } + } + + return types.User( + id: remoteIdentityPublicKey.toString(), + firstName: activeConversationState.remoteConversation.profile.name, + metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); + } types.User _convertUnknownUser(TypedKey remoteIdentityPublicKey) => types.User( @@ -376,6 +407,7 @@ class ChatComponentCubit extends Cubit { final _initWait = WaitSet(); final AccountInfo _accountInfo; final AccountRecordCubit _accountRecordCubit; + final ContactListCubit _contactListCubit; final List _conversationCubits; final SingleContactMessagesCubit _messagesCubit; diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 5035969..6d0fa73 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -9,6 +9,7 @@ import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../contacts/contacts.dart'; import '../../conversation/conversation.dart'; import '../../theme/theme.dart'; import '../chat.dart'; @@ -28,10 +29,13 @@ class ChatComponentWidget extends StatelessWidget { // Get the account record cubit final accountRecordCubit = context.read(); + // Get the contact list cubit + final contactListCubit = context.watch(); + // Get the active conversation cubit final activeConversationCubit = context .select( - (x) => x.tryOperate(localConversationRecordKey, + (x) => x.tryOperateSync(localConversationRecordKey, closure: (cubit) => cubit)); if (activeConversationCubit == null) { return waitingPage(); @@ -41,7 +45,7 @@ class ChatComponentWidget extends StatelessWidget { final messagesCubit = context.select< ActiveSingleContactChatBlocMapCubit, SingleContactMessagesCubit?>( - (x) => x.tryOperate(localConversationRecordKey, + (x) => x.tryOperateSync(localConversationRecordKey, closure: (cubit) => cubit)); if (messagesCubit == null) { return waitingPage(); @@ -49,9 +53,11 @@ class ChatComponentWidget extends StatelessWidget { // Make chat component state return BlocProvider( + key: key, create: (context) => ChatComponentCubit.singleContact( accountInfo: accountInfo, accountRecordCubit: accountRecordCubit, + contactListCubit: contactListCubit, activeConversationCubit: activeConversationCubit, messagesCubit: messagesCubit, ), diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 1b593c8..fa262f6 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -54,6 +54,7 @@ class ChatListCubit extends DHTShortArrayCubit // Make local copy so we don't share the buffer final localConversationRecordKey = contact.localConversationRecordKey.toVeilid(); + final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid(); final remoteConversationRecordKey = contact.remoteConversationRecordKey.toVeilid(); @@ -67,18 +68,38 @@ class ChatListCubit extends DHTShortArrayCubit throw Exception('Failed to get chat'); } final c = proto.Chat.fromBuffer(cbuf); - if (c.localConversationRecordKey == - contact.localConversationRecordKey) { - // Nothing to do here - return; + + switch (c.whichKind()) { + case proto.Chat_Kind.direct: + if (c.direct.localConversationRecordKey == + contact.localConversationRecordKey) { + // Nothing to do here + return; + } + break; + case proto.Chat_Kind.group: + if (c.group.localConversationRecordKey == + contact.localConversationRecordKey) { + throw StateError('direct conversation record key should' + ' not be used for group chats!'); + } + break; + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); } } // Create 1:1 conversation type Chat - final chat = proto.Chat() + final chatMember = proto.ChatMember() + ..remoteIdentityPublicKey = remoteIdentityPublicKey.toProto() + ..remoteConversationRecordKey = remoteConversationRecordKey.toProto(); + + final directChat = proto.DirectChat() ..settings = await getDefaultChatSettings(contact) ..localConversationRecordKey = localConversationRecordKey.toProto() - ..remoteConversationRecordKey = remoteConversationRecordKey.toProto(); + ..remoteMember = chatMember; + + final chat = proto.Chat()..direct = directChat; // Add chat await writer.add(chat.writeToBuffer()); @@ -88,9 +109,6 @@ class ChatListCubit extends DHTShortArrayCubit /// Delete a chat Future deleteChat( {required TypedKey localConversationRecordKey}) async { - final localConversationRecordKeyProto = - localConversationRecordKey.toProto(); - // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later final deletedItem = @@ -104,9 +122,9 @@ class ChatListCubit extends DHTShortArrayCubit if (c == null) { throw Exception('Failed to get chat'); } + if (c.localConversationRecordKey == - localConversationRecordKeyProto) { - // Found the right chat + localConversationRecordKey) { await writer.remove(i); return c; } @@ -133,7 +151,7 @@ class ChatListCubit extends DHTShortArrayCubit return IMap(); } return IMap.fromIterable(stateValue, - keyMapper: (e) => e.value.localConversationRecordKey.toVeilid(), + keyMapper: (e) => e.value.localConversationRecordKey, valueMapper: (e) => e.value); } diff --git a/lib/chat_list/views/chat_list_widget.dart b/lib/chat_list/views/chat_list_widget.dart new file mode 100644 index 0000000..e91cbba --- /dev/null +++ b/lib/chat_list/views/chat_list_widget.dart @@ -0,0 +1,96 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:searchable_listview/searchable_listview.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../contacts/contacts.dart'; +import '../../proto/proto.dart' as proto; +import '../../proto/proto.dart'; +import '../../theme/theme.dart'; +import '../chat_list.dart'; + +class ChatListWidget extends StatelessWidget { + const ChatListWidget({super.key}); + + Widget _itemBuilderDirect(proto.DirectChat direct, + IMap contactMap, bool busy) { + final contact = contactMap[direct.localConversationRecordKey]; + if (contact == null) { + return const Text('...'); + } + return ChatSingleContactItemWidget(contact: contact, disabled: busy) + .paddingLTRB(0, 4, 0, 0); + } + + List _itemFilter(IMap contactMap, + IList> chatList, String filter) { + final lowerValue = filter.toLowerCase(); + return chatList.map((x) => x.value).where((c) { + switch (c.whichKind()) { + case proto.Chat_Kind.direct: + final contact = contactMap[c.direct.localConversationRecordKey]; + if (contact == null) { + return false; + } + return contact.nickname.toLowerCase().contains(lowerValue) || + contact.profile.name.toLowerCase().contains(lowerValue) || + contact.profile.pronouns.toLowerCase().contains(lowerValue); + case proto.Chat_Kind.group: + // xxx: how to filter group chats + return true; + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); + } + }).toList(); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + final contactListV = context.watch().state; + + return contactListV.builder((context, contactList) { + final contactMap = IMap.fromIterable(contactList, + keyMapper: (c) => c.value.localConversationRecordKey, + valueMapper: (c) => c.value); + + final chatListV = context.watch().state; + return chatListV + .builder((context, chatList) => SizedBox.expand( + child: styledTitleContainer( + context: context, + title: translate('chat_list.chats'), + child: SizedBox.expand( + child: (chatList.isEmpty) + ? const EmptyChatListWidget() + : SearchableList( + initialList: chatList.map((x) => x.value).toList(), + itemBuilder: (c) { + switch (c.whichKind()) { + case proto.Chat_Kind.direct: + return _itemBuilderDirect( + c.direct, + contactMap, + contactListV.busy || chatListV.busy); + case proto.Chat_Kind.group: + return const Text( + 'group chats not yet supported!'); + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); + } + }, + filter: (value) => + _itemFilter(contactMap, chatList, value), + spaceBetweenSearchAndList: 4, + inputDecoration: InputDecoration( + labelText: translate('chat_list.search'), + ), + ), + ).paddingAll(8)))) + .paddingLTRB(8, 0, 8, 8); + }); + } +} diff --git a/lib/chat_list/views/chat_single_contact_list_widget.dart b/lib/chat_list/views/chat_single_contact_list_widget.dart deleted file mode 100644 index 605ade0..0000000 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:searchable_listview/searchable_listview.dart'; - -import '../../contacts/contacts.dart'; -import '../../proto/proto.dart' as proto; -import '../../theme/theme.dart'; -import '../chat_list.dart'; - -class ChatSingleContactListWidget extends StatelessWidget { - const ChatSingleContactListWidget({super.key}); - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - final contactListV = context.watch().state; - - return contactListV.builder((context, contactList) { - final contactMap = IMap.fromIterable(contactList, - keyMapper: (c) => c.value.localConversationRecordKey, - valueMapper: (c) => c.value); - - final chatListV = context.watch().state; - return chatListV - .builder((context, chatList) => SizedBox.expand( - child: styledTitleContainer( - context: context, - title: translate('chat_list.chats'), - child: SizedBox.expand( - child: (chatList.isEmpty) - ? const EmptyChatListWidget() - : SearchableList( - initialList: chatList.map((x) => x.value).toList(), - itemBuilder: (c) { - final contact = - contactMap[c.localConversationRecordKey]; - if (contact == null) { - return const Text('...'); - } - return ChatSingleContactItemWidget( - contact: contact, - disabled: contactListV.busy) - .paddingLTRB(0, 4, 0, 0); - }, - filter: (value) { - final lowerValue = value.toLowerCase(); - return chatList.map((x) => x.value).where((c) { - final contact = - contactMap[c.localConversationRecordKey]; - if (contact == null) { - return false; - } - return contact.nickname - .toLowerCase() - .contains(lowerValue) || - contact.profile.name - .toLowerCase() - .contains(lowerValue) || - contact.profile.pronouns - .toLowerCase() - .contains(lowerValue); - }).toList(); - }, - spaceBetweenSearchAndList: 4, - inputDecoration: InputDecoration( - labelText: translate('chat_list.search'), - ), - ), - ).paddingAll(8)))) - .paddingLTRB(8, 0, 8, 8); - }); - } -} diff --git a/lib/chat_list/views/views.dart b/lib/chat_list/views/views.dart index 311d02e..1420794 100644 --- a/lib/chat_list/views/views.dart +++ b/lib/chat_list/views/views.dart @@ -1,3 +1,3 @@ +export 'chat_list_widget.dart'; export 'chat_single_contact_item_widget.dart'; -export 'chat_single_contact_list_widget.dart'; export 'empty_chat_list_widget.dart'; diff --git a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart index a955978..47addc2 100644 --- a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -59,8 +59,11 @@ class WaitingInvitationCubit extends AsyncTransformerCubit close() async { + await _singleInvitationStatusProcessor.unfollow(); + await super.close(); + } + Future _addWaitingInvitation( {required proto.ContactInvitationRecord contactInvitationRecord}) async => @@ -40,16 +54,60 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit _invitationStatusListener( + WaitingInvitationsBlocMapState newState) async { + for (final entry in newState.entries) { + final contactRequestInboxRecordKey = entry.key; + final invStatus = entry.value.asData?.value; + // Skip invitations that have not yet been accepted or rejected + if (invStatus == null) { + continue; + } + + // Delete invitation and process the accepted or rejected contact + final acceptedContact = invStatus.acceptedContact; + if (acceptedContact != null) { + await _contactInvitationListCubit.deleteInvitation( + accepted: true, + contactRequestInboxRecordKey: contactRequestInboxRecordKey); + + // Accept + await _contactListCubit.createContact( + profile: acceptedContact.remoteProfile, + remoteSuperIdentity: acceptedContact.remoteIdentity, + remoteConversationRecordKey: + acceptedContact.remoteConversationRecordKey, + localConversationRecordKey: + acceptedContact.localConversationRecordKey, + ); + } else { + // Reject + await _contactInvitationListCubit.deleteInvitation( + accepted: false, + contactRequestInboxRecordKey: contactRequestInboxRecordKey); + } + } + } + /// StateFollower ///////////////////////// @override Future removeFromState(TypedKey key) => remove(key); @override - Future updateState(TypedKey key, proto.ContactInvitationRecord value) => - _addWaitingInvitation(contactInvitationRecord: value); + Future updateState( + TypedKey key, + proto.ContactInvitationRecord? oldValue, + proto.ContactInvitationRecord newValue) async { + await _addWaitingInvitation(contactInvitationRecord: newValue); + } //// final AccountInfo _accountInfo; final AccountRecordCubit _accountRecordCubit; + final ContactInvitationListCubit _contactInvitationListCubit; + final ContactListCubit _contactListCubit; + final _singleInvitationStatusProcessor = + SingleStateProcessor(); } diff --git a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart index ae527e4..b983265 100644 --- a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart @@ -13,17 +13,27 @@ import '../conversation.dart'; @immutable class ActiveConversationState extends Equatable { const ActiveConversationState({ - required this.contact, + required this.remoteIdentityPublicKey, + required this.localConversationRecordKey, + required this.remoteConversationRecordKey, required this.localConversation, required this.remoteConversation, }); - final proto.Contact contact; + final TypedKey remoteIdentityPublicKey; + final TypedKey localConversationRecordKey; + final TypedKey remoteConversationRecordKey; final proto.Conversation localConversation; final proto.Conversation remoteConversation; @override - List get props => [contact, localConversation, remoteConversation]; + List get props => [ + remoteIdentityPublicKey, + localConversationRecordKey, + remoteConversationRecordKey, + localConversation, + remoteConversation + ]; } typedef ActiveConversationCubit = TransformerCubit< @@ -37,9 +47,11 @@ typedef ActiveConversationsBlocMapState // Map of localConversationRecordKey to ActiveConversationCubit // Wraps a conversation cubit to only expose completely built conversations // Automatically follows the state of a ChatListCubit. -// Even though 'conversations' are per-contact and not per-chat // We currently only build the cubits for the chats that are active, not // archived chats or contacts that are not actively in a chat. +// +// TODO: Polling contacts for new inactive chats is yet to be done +// class ActiveConversationsBlocMapCubit extends BlocMapCubit, ActiveConversationCubit> with StateMapFollower { @@ -62,14 +74,11 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit _addConversation({required proto.Contact contact}) async => + Future _addDirectConversation( + {required TypedKey remoteIdentityPublicKey, + required TypedKey localConversationRecordKey, + required TypedKey remoteConversationRecordKey}) async => add(() { - final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid(); - final localConversationRecordKey = - contact.localConversationRecordKey.toVeilid(); - final remoteConversationRecordKey = - contact.remoteConversationRecordKey.toVeilid(); - // Conversation cubit the tracks the state between the local // and remote halves of a contact's relationship with this account final conversationCubit = ConversationCubit( @@ -105,14 +114,16 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit removeFromState(TypedKey key) => remove(key); @override - Future updateState(TypedKey key, proto.Chat value) async { - final contactList = _contactListCubit.state.state.asData?.value; - if (contactList == null) { - await addState(key, const AsyncValue.loading()); - return; + Future updateState( + TypedKey key, proto.Chat? oldValue, proto.Chat newValue) async { + switch (newValue.whichKind()) { + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); + case proto.Chat_Kind.direct: + final localConversationRecordKey = + newValue.direct.localConversationRecordKey.toVeilid(); + final remoteIdentityPublicKey = + newValue.direct.remoteMember.remoteIdentityPublicKey.toVeilid(); + final remoteConversationRecordKey = + newValue.direct.remoteMember.remoteConversationRecordKey.toVeilid(); + + if (oldValue != null) { + final oldLocalConversationRecordKey = + oldValue.direct.localConversationRecordKey.toVeilid(); + final oldRemoteIdentityPublicKey = + oldValue.direct.remoteMember.remoteIdentityPublicKey.toVeilid(); + final oldRemoteConversationRecordKey = oldValue + .direct.remoteMember.remoteConversationRecordKey + .toVeilid(); + + if (oldLocalConversationRecordKey == localConversationRecordKey && + oldRemoteIdentityPublicKey == remoteIdentityPublicKey && + oldRemoteConversationRecordKey == remoteConversationRecordKey) { + return; + } + } + + await _addDirectConversation( + remoteIdentityPublicKey: remoteIdentityPublicKey, + localConversationRecordKey: localConversationRecordKey, + remoteConversationRecordKey: remoteConversationRecordKey); + + break; + case proto.Chat_Kind.group: + break; } - final contactIndex = contactList.indexWhere( - (c) => c.value.localConversationRecordKey.toVeilid() == key); - if (contactIndex == -1) { - await addState(key, AsyncValue.error('Contact not found')); - return; - } - final contact = contactList[contactIndex]; - await _addConversation(contact: contact.value); } //// diff --git a/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart b/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart index c1d170d..6c6d4b8 100644 --- a/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart @@ -2,16 +2,42 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../chat/chat.dart'; -import '../../chat_list/cubits/chat_list_cubit.dart'; -import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; import '../conversation.dart'; import 'active_conversations_bloc_map_cubit.dart'; +@immutable +class _SingleContactChatState extends Equatable { + const _SingleContactChatState( + {required this.remoteIdentityPublicKey, + required this.localConversationRecordKey, + required this.remoteConversationRecordKey, + required this.localMessagesRecordKey, + required this.remoteMessagesRecordKey}); + + final TypedKey remoteIdentityPublicKey; + final TypedKey localConversationRecordKey; + final TypedKey remoteConversationRecordKey; + final TypedKey localMessagesRecordKey; + final TypedKey remoteMessagesRecordKey; + + @override + // TODO: implement props + List get props => [ + remoteIdentityPublicKey, + localConversationRecordKey, + remoteConversationRecordKey, + localMessagesRecordKey, + remoteMessagesRecordKey + ]; +} + // Map of localConversationRecordKey to MessagesCubit // Wraps a MessagesCubit to stream the latest messages to the state // Automatically follows the state of a ActiveConversationsBlocMapCubit. @@ -20,36 +46,42 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit> { - ActiveSingleContactChatBlocMapCubit( - {required AccountInfo accountInfo, - required ActiveConversationsBlocMapCubit activeConversationsBlocMapCubit, - required ContactListCubit contactListCubit, - required ChatListCubit chatListCubit}) - : _accountInfo = accountInfo, - _contactListCubit = contactListCubit, - _chatListCubit = chatListCubit { + ActiveSingleContactChatBlocMapCubit({ + required AccountInfo accountInfo, + required ActiveConversationsBlocMapCubit activeConversationsBlocMapCubit, + }) : _accountInfo = accountInfo { // Follow the active conversations bloc map cubit follow(activeConversationsBlocMapCubit); } - Future _addConversationMessages( - {required proto.Contact contact, - required proto.Chat chat, - required proto.Conversation localConversation, - required proto.Conversation remoteConversation}) async => + Future _addConversationMessages(_SingleContactChatState state) async => add(() => MapEntry( - contact.localConversationRecordKey.toVeilid(), + state.localConversationRecordKey, SingleContactMessagesCubit( accountInfo: _accountInfo, - remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), - localConversationRecordKey: - contact.localConversationRecordKey.toVeilid(), - remoteConversationRecordKey: - contact.remoteConversationRecordKey.toVeilid(), - localMessagesRecordKey: localConversation.messages.toVeilid(), - remoteMessagesRecordKey: remoteConversation.messages.toVeilid(), + remoteIdentityPublicKey: state.remoteIdentityPublicKey, + localConversationRecordKey: state.localConversationRecordKey, + remoteConversationRecordKey: state.remoteConversationRecordKey, + localMessagesRecordKey: state.localMessagesRecordKey, + remoteMessagesRecordKey: state.remoteMessagesRecordKey, ))); + _SingleContactChatState? _mapStateValue( + AsyncValue avInputState) { + final inputState = avInputState.asData?.value; + if (inputState == null) { + return null; + } + return _SingleContactChatState( + remoteIdentityPublicKey: inputState.remoteIdentityPublicKey, + localConversationRecordKey: inputState.localConversationRecordKey, + remoteConversationRecordKey: inputState.remoteConversationRecordKey, + localMessagesRecordKey: + inputState.localConversation.messages.toVeilid(), + remoteMessagesRecordKey: + inputState.remoteConversation.messages.toVeilid()); + } + /// StateFollower ///////////////////////// @override @@ -57,49 +89,27 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit updateState( - TypedKey key, AsyncValue value) async { - // Get the contact object for this single contact chat - final contactList = _contactListCubit.state.state.asData?.value; - if (contactList == null) { + TypedKey key, + AsyncValue? oldValue, + AsyncValue newValue) async { + final newState = _mapStateValue(newValue); + if (oldValue != null) { + final oldState = _mapStateValue(oldValue); + if (oldState == newState) { + return; + } + } + if (newState != null) { + await _addConversationMessages(newState); + } else if (newValue.isLoading) { await addState(key, const AsyncValue.loading()); - return; + } else { + final (error, stackTrace) = + (newValue.asError!.error, newValue.asError!.stackTrace); + await addState(key, AsyncValue.error(error, stackTrace)); } - final contactIndex = contactList.indexWhere( - (c) => c.value.localConversationRecordKey.toVeilid() == key); - if (contactIndex == -1) { - await addState( - key, AsyncValue.error('Contact not found for conversation')); - return; - } - final contact = contactList[contactIndex].value; - - // Get the chat object for this single contact chat - final chatList = _chatListCubit.state.state.asData?.value; - if (chatList == null) { - await addState(key, const AsyncValue.loading()); - return; - } - final chatIndex = chatList.indexWhere( - (c) => c.value.localConversationRecordKey.toVeilid() == key); - if (contactIndex == -1) { - await addState(key, AsyncValue.error('Chat not found for conversation')); - return; - } - final chat = chatList[chatIndex].value; - - await value.when( - data: (state) => _addConversationMessages( - contact: contact, - chat: chat, - localConversation: state.localConversation, - remoteConversation: state.remoteConversation), - loading: () => addState(key, const AsyncValue.loading()), - error: (error, stackTrace) => - addState(key, AsyncValue.error(error, stackTrace))); } //// final AccountInfo _accountInfo; - final ContactListCubit _contactListCubit; - final ChatListCubit _chatListCubit; } diff --git a/lib/layout/home/home_account_ready/main_pager/chats_page.dart b/lib/layout/home/home_account_ready/main_pager/chats_page.dart index bdea8e3..8811607 100644 --- a/lib/layout/home/home_account_ready/main_pager/chats_page.dart +++ b/lib/layout/home/home_account_ready/main_pager/chats_page.dart @@ -25,7 +25,7 @@ class ChatsPageState extends State { // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Column(children: [ - const ChatSingleContactListWidget().expanded(), + const ChatListWidget().expanded(), ]); } } diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 16d484d..8ade6f5 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -2,15 +2,12 @@ import 'dart:math'; import 'package:async_tools/async_tools.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../chat/chat.dart'; -import '../../contact_invitation/contact_invitation.dart'; -import '../../contacts/contacts.dart'; import '../../theme/theme.dart'; import '../../tools/tools.dart'; import 'active_account_page_controller_wrapper.dart'; @@ -39,48 +36,6 @@ class HomeScreenState extends State { super.dispose(); } - // Process all accepted or rejected invitations - void _invitationStatusListener( - BuildContext context, WaitingInvitationsBlocMapState state) { - _singleInvitationStatusProcessor.updateState(state, (newState) async { - final contactListCubit = context.read(); - final contactInvitationListCubit = - context.read(); - - for (final entry in newState.entries) { - final contactRequestInboxRecordKey = entry.key; - final invStatus = entry.value.asData?.value; - // Skip invitations that have not yet been accepted or rejected - if (invStatus == null) { - continue; - } - - // Delete invitation and process the accepted or rejected contact - final acceptedContact = invStatus.acceptedContact; - if (acceptedContact != null) { - await contactInvitationListCubit.deleteInvitation( - accepted: true, - contactRequestInboxRecordKey: contactRequestInboxRecordKey); - - // Accept - await contactListCubit.createContact( - profile: acceptedContact.remoteProfile, - remoteSuperIdentity: acceptedContact.remoteIdentity, - remoteConversationRecordKey: - acceptedContact.remoteConversationRecordKey, - localConversationRecordKey: - acceptedContact.localConversationRecordKey, - ); - } else { - // Reject - await contactInvitationListCubit.deleteInvitation( - accepted: false, - contactRequestInboxRecordKey: contactRequestInboxRecordKey); - } - } - }); - } - Widget _buildAccountReadyDeviceSpecific(BuildContext context) { final hasActiveChat = context.watch().state != null; if (responsiveVisibility( @@ -110,24 +65,18 @@ class HomeScreenState extends State { // Re-export all ready blocs to the account display subtree return perAccountCollectionState.provide( - child: MultiBlocListener(listeners: [ - BlocListener( - listener: _invitationStatusListener, - ) - ], child: Builder(builder: _buildAccountReadyDeviceSpecific))); + child: Builder(builder: _buildAccountReadyDeviceSpecific)); } } Widget _buildAccountPageView(BuildContext context) { final localAccounts = context.watch().state; - final activeLocalAccountCubit = - context.watch().state; + final activeLocalAccount = context.watch().state; final perAccountCollectionBlocMapState = context.watch().state; - final activeIndex = localAccounts.indexWhere( - (x) => x.superIdentity.recordKey == activeLocalAccountCubit); + final activeIndex = localAccounts + .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); if (activeIndex == -1) { return const HomeNoActive(); } @@ -208,6 +157,4 @@ class HomeScreenState extends State { } final _zoomDrawerController = ZoomDrawerController(); - final _singleInvitationStatusProcessor = - SingleStateProcessor(); } diff --git a/lib/proto/extensions.dart b/lib/proto/extensions.dart index a5e212e..4491f89 100644 --- a/lib/proto/extensions.dart +++ b/lib/proto/extensions.dart @@ -34,3 +34,16 @@ extension ContactExt on proto.Contact { String get displayName => nickname.isNotEmpty ? '$nickname (${profile.name})' : profile.name; } + +extension ChatExt on proto.Chat { + TypedKey get localConversationRecordKey { + switch (whichKind()) { + case proto.Chat_Kind.direct: + return direct.localConversationRecordKey.toVeilid(); + case proto.Chat_Kind.group: + return group.localConversationRecordKey.toVeilid(); + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); + } + } +} diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index f2b43d9..63bd910 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -1255,16 +1255,15 @@ class Conversation extends $pb.GeneratedMessage { $0.TypedKey ensureMessages() => $_ensure(2); } -class Chat extends $pb.GeneratedMessage { - factory Chat() => create(); - Chat._() : super(); - factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); +class ChatMember extends $pb.GeneratedMessage { + factory ChatMember() => create(); + ChatMember._() : super(); + factory ChatMember.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ChatMember.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) - ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) - ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatMember', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'remoteIdentityPublicKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) ..hasRequiredFields = false ; @@ -1272,22 +1271,79 @@ class Chat extends $pb.GeneratedMessage { 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Will be removed in next major version') - Chat clone() => Chat()..mergeFromMessage(this); + ChatMember clone() => ChatMember()..mergeFromMessage(this); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Will be removed in next major version') - Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat; + ChatMember copyWith(void Function(ChatMember) updates) => super.copyWith((message) => updates(message as ChatMember)) as ChatMember; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') - static Chat create() => Chat._(); - Chat createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static ChatMember create() => ChatMember._(); + ChatMember createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Chat? _defaultInstance; + static ChatMember getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ChatMember? _defaultInstance; + + @$pb.TagNumber(1) + $0.TypedKey get remoteIdentityPublicKey => $_getN(0); + @$pb.TagNumber(1) + set remoteIdentityPublicKey($0.TypedKey v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasRemoteIdentityPublicKey() => $_has(0); + @$pb.TagNumber(1) + void clearRemoteIdentityPublicKey() => clearField(1); + @$pb.TagNumber(1) + $0.TypedKey ensureRemoteIdentityPublicKey() => $_ensure(0); + + @$pb.TagNumber(2) + $0.TypedKey get remoteConversationRecordKey => $_getN(1); + @$pb.TagNumber(2) + set remoteConversationRecordKey($0.TypedKey v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasRemoteConversationRecordKey() => $_has(1); + @$pb.TagNumber(2) + void clearRemoteConversationRecordKey() => clearField(2); + @$pb.TagNumber(2) + $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(1); +} + +class DirectChat extends $pb.GeneratedMessage { + factory DirectChat() => create(); + DirectChat._() : super(); + factory DirectChat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DirectChat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DirectChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) + ..aOM(3, _omitFieldNames ? '' : 'remoteMember', subBuilder: ChatMember.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DirectChat clone() => DirectChat()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DirectChat copyWith(void Function(DirectChat) updates) => super.copyWith((message) => updates(message as DirectChat)) as DirectChat; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DirectChat create() => DirectChat._(); + DirectChat createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DirectChat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DirectChat? _defaultInstance; @$pb.TagNumber(1) ChatSettings get settings => $_getN(0); @@ -1312,15 +1368,15 @@ class Chat extends $pb.GeneratedMessage { $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1); @$pb.TagNumber(3) - $0.TypedKey get remoteConversationRecordKey => $_getN(2); + ChatMember get remoteMember => $_getN(2); @$pb.TagNumber(3) - set remoteConversationRecordKey($0.TypedKey v) { setField(3, v); } + set remoteMember(ChatMember v) { setField(3, v); } @$pb.TagNumber(3) - $core.bool hasRemoteConversationRecordKey() => $_has(2); + $core.bool hasRemoteMember() => $_has(2); @$pb.TagNumber(3) - void clearRemoteConversationRecordKey() => clearField(3); + void clearRemoteMember() => clearField(3); @$pb.TagNumber(3) - $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2); + ChatMember ensureRemoteMember() => $_ensure(2); } class GroupChat extends $pb.GeneratedMessage { @@ -1331,8 +1387,10 @@ class GroupChat extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GroupChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..aOM(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) - ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) - ..pc<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKeys', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..aOM(2, _omitFieldNames ? '' : 'membership', subBuilder: Membership.create) + ..aOM(3, _omitFieldNames ? '' : 'permissions', subBuilder: Permissions.create) + ..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) + ..pc(5, _omitFieldNames ? '' : 'remoteMembers', $pb.PbFieldType.PM, subBuilder: ChatMember.create) ..hasRequiredFields = false ; @@ -1369,18 +1427,111 @@ class GroupChat extends $pb.GeneratedMessage { ChatSettings ensureSettings() => $_ensure(0); @$pb.TagNumber(2) - $0.TypedKey get localConversationRecordKey => $_getN(1); + Membership get membership => $_getN(1); @$pb.TagNumber(2) - set localConversationRecordKey($0.TypedKey v) { setField(2, v); } + set membership(Membership v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasLocalConversationRecordKey() => $_has(1); + $core.bool hasMembership() => $_has(1); @$pb.TagNumber(2) - void clearLocalConversationRecordKey() => clearField(2); + void clearMembership() => clearField(2); @$pb.TagNumber(2) - $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1); + Membership ensureMembership() => $_ensure(1); @$pb.TagNumber(3) - $core.List<$0.TypedKey> get remoteConversationRecordKeys => $_getList(2); + Permissions get permissions => $_getN(2); + @$pb.TagNumber(3) + set permissions(Permissions v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasPermissions() => $_has(2); + @$pb.TagNumber(3) + void clearPermissions() => clearField(3); + @$pb.TagNumber(3) + Permissions ensurePermissions() => $_ensure(2); + + @$pb.TagNumber(4) + $0.TypedKey get localConversationRecordKey => $_getN(3); + @$pb.TagNumber(4) + set localConversationRecordKey($0.TypedKey v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasLocalConversationRecordKey() => $_has(3); + @$pb.TagNumber(4) + void clearLocalConversationRecordKey() => clearField(4); + @$pb.TagNumber(4) + $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(3); + + @$pb.TagNumber(5) + $core.List get remoteMembers => $_getList(4); +} + +enum Chat_Kind { + direct, + group, + notSet +} + +class Chat extends $pb.GeneratedMessage { + factory Chat() => create(); + Chat._() : super(); + factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, Chat_Kind> _Chat_KindByTag = { + 1 : Chat_Kind.direct, + 2 : Chat_Kind.group, + 0 : Chat_Kind.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..oo(0, [1, 2]) + ..aOM(1, _omitFieldNames ? '' : 'direct', subBuilder: DirectChat.create) + ..aOM(2, _omitFieldNames ? '' : 'group', subBuilder: GroupChat.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Chat clone() => Chat()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Chat create() => Chat._(); + Chat createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Chat? _defaultInstance; + + Chat_Kind whichKind() => _Chat_KindByTag[$_whichOneof(0)]!; + void clearKind() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + DirectChat get direct => $_getN(0); + @$pb.TagNumber(1) + set direct(DirectChat v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasDirect() => $_has(0); + @$pb.TagNumber(1) + void clearDirect() => clearField(1); + @$pb.TagNumber(1) + DirectChat ensureDirect() => $_ensure(0); + + @$pb.TagNumber(2) + GroupChat get group => $_getN(1); + @$pb.TagNumber(2) + set group(GroupChat v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasGroup() => $_has(1); + @$pb.TagNumber(2) + void clearGroup() => clearField(2); + @$pb.TagNumber(2) + GroupChat ensureGroup() => $_ensure(1); } class Profile extends $pb.GeneratedMessage { diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index d59b75c..fe6cac3 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -365,41 +365,76 @@ final $typed_data.Uint8List conversationDescriptor = $convert.base64Decode( 'JvZmlsZRIuChNzdXBlcl9pZGVudGl0eV9qc29uGAIgASgJUhFzdXBlcklkZW50aXR5SnNvbhIs' 'CghtZXNzYWdlcxgDIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIIbWVzc2FnZXM='); -@$core.Deprecated('Use chatDescriptor instead') -const Chat$json = { - '1': 'Chat', +@$core.Deprecated('Use chatMemberDescriptor instead') +const ChatMember$json = { + '1': 'ChatMember', '2': [ - {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, - {'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, - {'1': 'remote_conversation_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, + {'1': 'remote_identity_public_key', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteIdentityPublicKey'}, + {'1': 'remote_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, ], }; -/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List chatDescriptor = $convert.base64Decode( - 'CgRDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3NSCHNldH' - 'RpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVpbGlkLlR5' - 'cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRJVCh5yZW1vdGVfY29udmVyc2F0aW' - '9uX3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdGlv' - 'blJlY29yZEtleQ=='); +/// Descriptor for `ChatMember`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List chatMemberDescriptor = $convert.base64Decode( + 'CgpDaGF0TWVtYmVyEk0KGnJlbW90ZV9pZGVudGl0eV9wdWJsaWNfa2V5GAEgASgLMhAudmVpbG' + 'lkLlR5cGVkS2V5UhdyZW1vdGVJZGVudGl0eVB1YmxpY0tleRJVCh5yZW1vdGVfY29udmVyc2F0' + 'aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdG' + 'lvblJlY29yZEtleQ=='); + +@$core.Deprecated('Use directChatDescriptor instead') +const DirectChat$json = { + '1': 'DirectChat', + '2': [ + {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, + {'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, + {'1': 'remote_member', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.ChatMember', '10': 'remoteMember'}, + ], +}; + +/// Descriptor for `DirectChat`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List directChatDescriptor = $convert.base64Decode( + 'CgpEaXJlY3RDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3' + 'NSCHNldHRpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVp' + 'bGlkLlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRI7Cg1yZW1vdGVfbWVtYm' + 'VyGAMgASgLMhYudmVpbGlkY2hhdC5DaGF0TWVtYmVyUgxyZW1vdGVNZW1iZXI='); @$core.Deprecated('Use groupChatDescriptor instead') const GroupChat$json = { '1': 'GroupChat', '2': [ {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, - {'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, - {'1': 'remote_conversation_record_keys', '3': 3, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKeys'}, + {'1': 'membership', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Membership', '10': 'membership'}, + {'1': 'permissions', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.Permissions', '10': 'permissions'}, + {'1': 'local_conversation_record_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, + {'1': 'remote_members', '3': 5, '4': 3, '5': 11, '6': '.veilidchat.ChatMember', '10': 'remoteMembers'}, ], }; /// Descriptor for `GroupChat`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List groupChatDescriptor = $convert.base64Decode( 'CglHcm91cENoYXQSNAoIc2V0dGluZ3MYASABKAsyGC52ZWlsaWRjaGF0LkNoYXRTZXR0aW5nc1' - 'IIc2V0dGluZ3MSUwodbG9jYWxfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWls' - 'aWQuVHlwZWRLZXlSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5ElcKH3JlbW90ZV9jb252ZX' - 'JzYXRpb25fcmVjb3JkX2tleXMYAyADKAsyEC52ZWlsaWQuVHlwZWRLZXlSHHJlbW90ZUNvbnZl' - 'cnNhdGlvblJlY29yZEtleXM='); + 'IIc2V0dGluZ3MSNgoKbWVtYmVyc2hpcBgCIAEoCzIWLnZlaWxpZGNoYXQuTWVtYmVyc2hpcFIK' + 'bWVtYmVyc2hpcBI5CgtwZXJtaXNzaW9ucxgDIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbn' + 'NSC3Blcm1pc3Npb25zElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAQgASgLMhAu' + 'dmVpbGlkLlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRI9Cg5yZW1vdGVfbW' + 'VtYmVycxgFIAMoCzIWLnZlaWxpZGNoYXQuQ2hhdE1lbWJlclINcmVtb3RlTWVtYmVycw=='); + +@$core.Deprecated('Use chatDescriptor instead') +const Chat$json = { + '1': 'Chat', + '2': [ + {'1': 'direct', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.DirectChat', '9': 0, '10': 'direct'}, + {'1': 'group', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.GroupChat', '9': 0, '10': 'group'}, + ], + '8': [ + {'1': 'kind'}, + ], +}; + +/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List chatDescriptor = $convert.base64Decode( + 'CgRDaGF0EjAKBmRpcmVjdBgBIAEoCzIWLnZlaWxpZGNoYXQuRGlyZWN0Q2hhdEgAUgZkaXJlY3' + 'QSLQoFZ3JvdXAYAiABKAsyFS52ZWlsaWRjaGF0Lkdyb3VwQ2hhdEgAUgVncm91cEIGCgRraW5k'); @$core.Deprecated('Use profileDescriptor instead') const Profile$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 0c00db7..794cef8 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -267,15 +267,23 @@ message Conversation { veilid.TypedKey messages = 3; } -// Either a 1-1 conversation or a group chat +// A member of chat which may or may not be associated with a contact +message ChatMember { + // The identity public key most recently associated with the chat member + veilid.TypedKey remote_identity_public_key = 1; + // Conversation key for the other party + veilid.TypedKey remote_conversation_record_key = 2; +} + +// A 1-1 chat // Privately encrypted, this is the local user's copy of the chat -message Chat { +message DirectChat { // Settings ChatSettings settings = 1; // Conversation key for this user veilid.TypedKey local_conversation_record_key = 2; // Conversation key for the other party - veilid.TypedKey remote_conversation_record_key = 3; + ChatMember remote_member = 3; } // A group chat @@ -283,10 +291,22 @@ message Chat { message GroupChat { // Settings ChatSettings settings = 1; + // Membership + Membership membership = 2; + // Permissions + Permissions permissions = 3; // Conversation key for this user - veilid.TypedKey local_conversation_record_key = 2; + veilid.TypedKey local_conversation_record_key = 4; // Conversation keys for the other parties - repeated veilid.TypedKey remote_conversation_record_keys = 3; + repeated ChatMember remote_members = 5; +} + +// Some kind of chat +message Chat { + oneof kind { + DirectChat direct = 1; + GroupChat group = 2; + } } //////////////////////////////////////////////////////////////////////////////////// diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 5d59190..ca0074f 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -609,7 +609,7 @@ class _DHTLogSpine { // Don't watch for local changes because this class already handles // notifying listeners and knows when it makes local changes _subscription ??= - await _spineRecord.listen(localChanges: false, _onSpineChanged); + await _spineRecord.listen(localChanges: true, _onSpineChanged); } on Exception { // If anything fails, try to cancel the watches await cancelWatch(); diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart index a333160..5ea6761 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart @@ -12,14 +12,6 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { stateFunction: _makeStateFunction(decodeState), watchFunction: _makeWatchFunction()); - // DefaultDHTRecordCubit.value({ - // required super.record, - // required T Function(List data) decodeState, - // }) : super.value( - // initialStateFunction: _makeInitialStateFunction(decodeState), - // stateFunction: _makeStateFunction(decodeState), - // watchFunction: _makeWatchFunction()); - static InitialStateFunction _makeInitialStateFunction( T Function(List data) decodeState) => (record) async { diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_cubit.dart index 1cfcfcd..54d1dec 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_cubit.dart @@ -29,20 +29,6 @@ class DHTRecordCubit extends Cubit> { }); } - // DHTRecordCubit.value({ - // required DHTRecord record, - // required InitialStateFunction initialStateFunction, - // required StateFunction stateFunction, - // required WatchFunction watchFunction, - // }) : _record = record, - // _stateFunction = stateFunction, - // _wantsCloseRecord = false, - // super(const AsyncValue.loading()) { - // Future.delayed(Duration.zero, () async { - // await _init(initialStateFunction, stateFunction, watchFunction); - // }); - // } - Future _init( InitialStateFunction initialStateFunction, StateFunction stateFunction,