From 13ddb4f22c7f4414210b8d290fe5c7039c35aa63 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 6 Aug 2023 19:46:40 -0400 Subject: [PATCH] xfer --- assets/i18n/en.json | 9 +- lib/components/chat_component.dart | 32 ++++- lib/components/chat_index.dart | 27 ---- lib/components/chat_list.dart | 25 ---- lib/components/chat_list_widget.dart | 92 ++++++++++++++ .../chat_single_contact_item_widget.dart | 99 +++++++++++++++ lib/components/contact_item_widget.dart | 5 +- lib/components/contact_list_widget.dart | 7 ++ lib/components/empty_chat_list_widget.dart | 35 ++++++ lib/components/empty_contact_list_widget.dart | 2 +- lib/entities/identity.dart | 8 +- lib/pages/main_pager/chats_page.dart | 77 +++++++++++- lib/pages/main_pager/main_pager.dart | 11 +- lib/providers/chat.dart | 116 ++++++++++++++++++ lib/providers/chat.g.dart | 26 ++++ lib/providers/contact.dart | 12 +- lib/providers/contact.g.dart | 2 +- lib/providers/contact_invite.g.dart | 2 +- 18 files changed, 514 insertions(+), 73 deletions(-) delete mode 100644 lib/components/chat_index.dart delete mode 100644 lib/components/chat_list.dart create mode 100644 lib/components/chat_list_widget.dart create mode 100644 lib/components/chat_single_contact_item_widget.dart create mode 100644 lib/components/empty_chat_list_widget.dart create mode 100644 lib/providers/chat.dart create mode 100644 lib/providers/chat.g.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 90e40a9..4fe6ee6 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -54,9 +54,6 @@ "invalid_account_text": "Account is invalid, removing from list", "contact_invitations": "Contact Invitations" }, - "empty_contact_list": { - "invite_people": "Invite people to VeilidChat" - }, "accounts_menu": { "invite_contact": "Invite Contact", "create_invite": "Create Invite", @@ -97,9 +94,15 @@ "reenter_pin": "Re-Enter PIN To Confirm" }, "contact_list": { + "invite_people": "Invite people to VeilidChat", "search": "Search contacts", "invitation": "Invitation" }, + "chat_list": { + "start_a_conversation": "Start a conversation", + "conversations": "Conversations", + "groups": "Groups" + }, "themes": { "vapor": "Vapor" } diff --git a/lib/components/chat_component.dart b/lib/components/chat_component.dart index 8e7ef29..d250cd8 100644 --- a/lib/components/chat_component.dart +++ b/lib/components/chat_component.dart @@ -1,11 +1,15 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; -import 'package:intl/date_symbol_data_local.dart'; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +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 '../tools/theme_service.dart'; +import 'empty_chat_widget.dart'; class ChatComponent extends ConsumerStatefulWidget { const ChatComponent({super.key}); @@ -77,7 +81,25 @@ class ChatComponentState extends ConsumerState { 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]; + return DefaultTextStyle( style: textTheme.bodySmall!, child: Align( @@ -96,7 +118,7 @@ class ChatComponentState extends ConsumerState { child: Padding( padding: const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0), - child: Text("current contact", + child: Text(activeChatContact.editedProfile.name, textAlign: TextAlign.start, style: textTheme.titleMedium), ), diff --git a/lib/components/chat_index.dart b/lib/components/chat_index.dart deleted file mode 100644 index 0f2b9f2..0000000 --- a/lib/components/chat_index.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class ChatIndex extends ConsumerWidget { - const ChatIndex({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) => const Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Contacts Page'), - // ElevatedButton( - // onPressed: () async { - // ref.watch(authNotifierProvider.notifier).login( - // "myEmail", - // "myPassword", - // ); - // }, - // child: const Text("Login"), - // ), - ], - ), - ), - ); -} diff --git a/lib/components/chat_list.dart b/lib/components/chat_list.dart deleted file mode 100644 index 9ddf746..0000000 --- a/lib/components/chat_list.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class ChatList extends ConsumerWidget { - const ChatList({super.key}); - //final LocalAccount account; - - @override - Widget build(BuildContext context, WidgetRef ref) { - //final logins = ref.watch(loginsProvider); - - return ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 300), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [const Expanded(child: Text('Chat List'))])); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - //properties.add(DiagnosticsProperty('account', account)); - } -} diff --git a/lib/components/chat_list_widget.dart b/lib/components/chat_list_widget.dart new file mode 100644 index 0000000..5246737 --- /dev/null +++ b/lib/components/chat_list_widget.dart @@ -0,0 +1,92 @@ +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 new file mode 100644 index 0000000..18931f3 --- /dev/null +++ b/lib/components/chat_single_contact_item_widget.dart @@ -0,0 +1,99 @@ +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 '../tools/theme_service.dart'; + +class ChatSingleContactItemWidget extends ConsumerWidget { + const ChatSingleContactItemWidget({required this.contact, super.key}); + + final proto.Contact contact; + + @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()!; + + final activeChat = ref.watch(activeChatStateProvider).asData?.value; + final selected = activeChat == + proto.TypedKeyProto.fromProto(contact.remoteConversationKey); + + return Container( + margin: const EdgeInsets.fromLTRB(4, 4, 4, 0), + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: scale.tertiaryScale.subtleBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + )), + child: Slidable( + key: ObjectKey(contact), + endActionPane: ActionPane( + motion: const DrawerMotion(), + children: [ + SlidableAction( + onPressed: (context) async { + final activeAccountInfo = + await ref.read(fetchActiveAccountProvider.future); + if (activeAccountInfo != null) { + await deleteChat( + activeAccountInfo: activeAccountInfo, + remoteConversationRecordKey: + proto.TypedKeyProto.fromProto( + contact.remoteConversationKey)); + ref.invalidate(fetchChatListProvider); + } + }, + backgroundColor: scale.tertiaryScale.background, + foregroundColor: scale.tertiaryScale.text, + icon: Icons.delete, + label: translate('button.delete'), + padding: const EdgeInsets.all(2)), + // SlidableAction( + // onPressed: (context) => (), + // backgroundColor: scale.secondaryScale.background, + // foregroundColor: scale.secondaryScale.text, + // icon: Icons.edit, + // label: 'Edit', + // ), + ], + ), + + // The child of the Slidable is what the user sees when the + // component is not dragged. + child: ListTile( + onTap: () async { + 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), + + /// xxx show last message here + subtitle: (contact.editedProfile.title.isNotEmpty) + ? Text(contact.editedProfile.title) + : null, + iconColor: scale.tertiaryScale.background, + textColor: scale.tertiaryScale.text, + selected: selected, + //Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ), + leading: const Icon(Icons.chat)))); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('contact', contact)); + } +} diff --git a/lib/components/contact_item_widget.dart b/lib/components/contact_item_widget.dart index 363c4e4..374642b 100644 --- a/lib/components/contact_item_widget.dart +++ b/lib/components/contact_item_widget.dart @@ -5,6 +5,7 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../entities/proto.dart' as proto; import '../providers/account.dart'; +import '../providers/chat.dart'; import '../providers/contact.dart'; import '../tools/theme_service.dart'; @@ -41,7 +42,9 @@ class ContactItemWidget extends ConsumerWidget { await deleteContact( activeAccountInfo: activeAccountInfo, contact: contact); - ref.invalidate(fetchContactListProvider); + ref + ..invalidate(fetchContactListProvider) + ..invalidate(fetchChatListProvider); } }, backgroundColor: scale.tertiaryScale.background, diff --git a/lib/components/contact_list_widget.dart b/lib/components/contact_list_widget.dart index 11ef2a5..3e76183 100644 --- a/lib/components/contact_list_widget.dart +++ b/lib/components/contact_list_widget.dart @@ -1,5 +1,6 @@ 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'; @@ -14,6 +15,12 @@ class ContactListWidget extends ConsumerWidget { const ContactListWidget({required this.contactList, super.key}); final IList contactList; + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IterableProperty('contactList', contactList)); + } + @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/components/empty_chat_list_widget.dart b/lib/components/empty_chat_list_widget.dart new file mode 100644 index 0000000..9698b9e --- /dev/null +++ b/lib/components/empty_chat_list_widget.dart @@ -0,0 +1,35 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../tools/tools.dart'; + +class EmptyChatListWidget extends ConsumerWidget { + const EmptyChatListWidget({super.key}); + + @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 Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.chat, + color: scale.primaryScale.border, + size: 48, + ), + Text( + translate('chat_list.start_a_conversation'), + style: textTheme.bodyMedium?.copyWith( + color: scale.primaryScale.border, + ), + ), + ], + ).expanded(); + } +} diff --git a/lib/components/empty_contact_list_widget.dart b/lib/components/empty_contact_list_widget.dart index ddc3039..06b3b10 100644 --- a/lib/components/empty_contact_list_widget.dart +++ b/lib/components/empty_contact_list_widget.dart @@ -23,7 +23,7 @@ class EmptyContactListWidget extends ConsumerWidget { size: 48, ), Text( - translate('empty_contact_list.invite_people'), + translate('contact_list.invite_people'), style: textTheme.bodyMedium?.copyWith( color: scale.primaryScale.border, ), diff --git a/lib/entities/identity.dart b/lib/entities/identity.dart index ecaafda..95d361c 100644 --- a/lib/entities/identity.dart +++ b/lib/entities/identity.dart @@ -144,13 +144,19 @@ extension IdentityMasterExtension on IdentityMaster { await (await DHTShortArray.create(parent: accountRec.key)) .scope((r) => r.record.ownedDHTRecordPointer); + // Make empty chat record list + final chatRecords = + await (await DHTShortArray.create(parent: accountRec.key)) + .scope((r) => r.record.ownedDHTRecordPointer); + // Make account object final account = proto.Account() ..profile = (proto.Profile() ..name = name ..title = title) ..contactList = contactList.toProto() - ..contactInvitationRecords = contactInvitationRecords.toProto(); + ..contactInvitationRecords = contactInvitationRecords.toProto() + ..chatList = chatRecords.toProto(); // Write account key await accountRec.eventualWriteProtobuf(account); diff --git a/lib/pages/main_pager/chats_page.dart b/lib/pages/main_pager/chats_page.dart index 386fa3e..fbabc58 100644 --- a/lib/pages/main_pager/chats_page.dart +++ b/lib/pages/main_pager/chats_page.dart @@ -1,5 +1,20 @@ +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/empty_chat_list_widget.dart'; +import '../../entities/local_account.dart'; +import '../../entities/proto.dart' as proto; +import '../../providers/account.dart'; +import '../../providers/chat.dart'; +import '../../providers/contact.dart'; +import '../../providers/contact_invite.dart'; +import '../../providers/local_accounts.dart'; +import '../../providers/logins.dart'; +import '../../tools/tools.dart'; +import '../../veilid_support/veilid_support.dart'; class ChatsPage extends ConsumerStatefulWidget { const ChatsPage({super.key}); @@ -22,9 +37,69 @@ class ChatsPageState extends ConsumerState { super.dispose(); } + /// We have an active, unlocked, user login + Widget buildChatList( + BuildContext context, + IList localAccounts, + TypedKey activeUserLogin, + proto.Account account, + // ignore: prefer_expression_function_bodies + ) { + final contactList = ref.watch(fetchContactListProvider).asData?.value ?? + const IListConst([]); + final chatList = + ref.watch(fetchChatListProvider).asData?.value ?? const IListConst([]); + + 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(), + ]); + } + @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - return Center(child: Text("Conversations Page")); + final localAccountsV = ref.watch(localAccountsProvider); + final loginsV = ref.watch(loginsProvider); + + if (!localAccountsV.hasValue || !loginsV.hasValue) { + return waitingPage(context); + } + final localAccounts = localAccountsV.requireValue; + final logins = loginsV.requireValue; + + final activeUserLogin = logins.activeUserLogin; + if (activeUserLogin == null) { + // If no logged in user is active show a placeholder + return waitingPage(context); + } + final accountV = ref + .watch(fetchAccountProvider(accountMasterRecordKey: activeUserLogin)); + if (!accountV.hasValue) { + return waitingPage(context); + } + final account = accountV.requireValue; + switch (account.status) { + case AccountInfoStatus.noAccount: + return waitingPage(context); + case AccountInfoStatus.accountInvalid: + return waitingPage(context); + case AccountInfoStatus.accountLocked: + return waitingPage(context); + case AccountInfoStatus.accountReady: + return buildChatList( + context, + localAccounts, + activeUserLogin, + account.account!, + ); + } } } diff --git a/lib/pages/main_pager/main_pager.dart b/lib/pages/main_pager/main_pager.dart index 27a7b98..0130a44 100644 --- a/lib/pages/main_pager/main_pager.dart +++ b/lib/pages/main_pager/main_pager.dart @@ -23,6 +23,9 @@ class MainPager extends ConsumerStatefulWidget { @override MainPagerState createState() => MainPagerState(); + + static MainPagerState? of(BuildContext context) => + context.findAncestorStateOfType(); } class MainPagerState extends ConsumerState @@ -31,7 +34,7 @@ class MainPagerState extends ConsumerState final _unfocusNode = FocusNode(); - final _pageController = PageController(); + final pageController = PageController(); var _currentPage = 0; final _selectedIconList = [Icons.person, Icons.chat]; @@ -62,7 +65,7 @@ class MainPagerState extends ConsumerState @override void dispose() { _unfocusNode.dispose(); - _pageController.dispose(); + pageController.dispose(); super.dispose(); } @@ -231,7 +234,7 @@ class MainPagerState extends ConsumerState body: NotificationListener( onNotification: onScrollNotification, child: PageView( - controller: _pageController, + controller: pageController, //physics: const NeverScrollableScrollPhysics(), children: List.generate( _bottomBarPages.length, (index) => _bottomBarPages[index]), @@ -266,7 +269,7 @@ class MainPagerState extends ConsumerState fabLocation: StylishBarFabLocation.end, currentIndex: _currentPage, onTap: (index) async { - await _pageController.animateToPage(index, + await pageController.animateToPage(index, duration: 250.ms, curve: Curves.easeInOut); setState(() { _currentPage = index; diff --git a/lib/providers/chat.dart b/lib/providers/chat.dart new file mode 100644 index 0000000..29479c1 --- /dev/null +++ b/lib/providers/chat.dart @@ -0,0 +1,116 @@ +import 'dart:convert'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../entities/proto.dart' as proto; +import '../entities/proto.dart' show Chat, ChatType; + +import '../tools/tools.dart'; +import '../veilid_support/veilid_support.dart'; +import 'account.dart'; + +part 'chat.g.dart'; + +/// Create a new chat (singleton for single contact chats) +Future getOrCreateChatSingleContact({ + required ActiveAccountInfo activeAccountInfo, + required TypedKey remoteConversationRecordKey, +}) async { + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + // Create conversation type Chat + final chat = Chat() + ..type = ChatType.SINGLE_CONTACT + ..remoteConversationKey = remoteConversationRecordKey.toProto(); + + // Add Chat to account's list + // if this fails, don't keep retrying, user can try again later + await (await DHTShortArray.openOwned( + proto.OwnedDHTRecordPointerProto.fromProto( + activeAccountInfo.account.chatList), + parent: accountRecordKey)) + .scope((chatList) async { + for (var i = 0; i < chatList.length; i++) { + final cbuf = await chatList.getItem(i); + if (cbuf == null) { + throw Exception('Failed to get chat'); + } + final c = Chat.fromBuffer(cbuf); + if (c == chat) { + return; + } + } + if (await chatList.tryAddItem(chat.writeToBuffer()) == false) { + throw Exception('Failed to add chat'); + } + }); +} + +/// Delete a chat +Future deleteChat( + {required ActiveAccountInfo activeAccountInfo, + required TypedKey remoteConversationRecordKey}) async { + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + // Create conversation type Chat + final remoteConversationKey = remoteConversationRecordKey.toProto(); + + // Add Chat to account's list + // if this fails, don't keep retrying, user can try again later + await (await DHTShortArray.openOwned( + proto.OwnedDHTRecordPointerProto.fromProto( + activeAccountInfo.account.chatList), + parent: accountRecordKey)) + .scope((chatList) async { + for (var i = 0; i < chatList.length; i++) { + final cbuf = await chatList.getItem(i); + if (cbuf == null) { + throw Exception('Failed to get chat'); + } + final c = Chat.fromBuffer(cbuf); + if (c.remoteConversationKey == remoteConversationKey) { + await chatList.tryRemoveItem(i); + return; + } + } + }); +} + +/// Get the active account contact list +@riverpod +Future?> fetchChatList(FetchChatListRef 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; + + // Decode the chat list from the DHT + IList out = const IListConst([]); + await (await DHTShortArray.openOwned( + proto.OwnedDHTRecordPointerProto.fromProto( + activeAccountInfo.account.chatList), + 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 chat'); + } + out = out.add(Chat.fromBuffer(cir)); + } + }); + + return out; +} + +// The selected chat +ExternalStreamState activeChatState = + ExternalStreamState(null); +AutoDisposeStreamProvider activeChatStateProvider = + activeChatState.provider(); diff --git a/lib/providers/chat.g.dart b/lib/providers/chat.g.dart new file mode 100644 index 0000000..e0b30b0 --- /dev/null +++ b/lib/providers/chat.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$fetchChatListHash() => r'407692f9d6794a5a2b356d7a34240624b211daa8'; + +/// Get the active account contact list +/// +/// Copied from [fetchChatList]. +@ProviderFor(fetchChatList) +final fetchChatListProvider = AutoDisposeFutureProvider?>.internal( + fetchChatList, + name: r'fetchChatListProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$fetchChatListHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef FetchChatListRef = AutoDisposeFutureProviderRef?>; +// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions diff --git a/lib/providers/contact.dart b/lib/providers/contact.dart index f44a4c9..62e1c36 100644 --- a/lib/providers/contact.dart +++ b/lib/providers/contact.dart @@ -9,6 +9,7 @@ import '../entities/proto.dart' show Contact; import '../veilid_support/veilid_support.dart'; import 'account.dart'; +import 'chat.dart'; part 'contact.g.dart'; @@ -54,6 +55,13 @@ Future deleteContact( final pool = await DHTRecordPool.instance(); final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final remoteConversationKey = + proto.TypedKeyProto.fromProto(contact.remoteConversationKey); + + // Remove any chats for this contact + await deleteChat( + activeAccountInfo: activeAccountInfo, + remoteConversationRecordKey: remoteConversationKey); // Remove Contact from account's list await (await DHTShortArray.openOwned( @@ -77,9 +85,7 @@ Future deleteContact( contact.localConversation), parent: accountRecordKey)) .delete(); - await (await pool.openRead( - proto.TypedKeyProto.fromProto(contact.remoteConversationKey), - parent: accountRecordKey)) + await (await pool.openRead(remoteConversationKey, parent: accountRecordKey)) .delete(); }); } diff --git a/lib/providers/contact.g.dart b/lib/providers/contact.g.dart index aa98b33..56383a3 100644 --- a/lib/providers/contact.g.dart +++ b/lib/providers/contact.g.dart @@ -6,7 +6,7 @@ part of 'contact.dart'; // RiverpodGenerator // ************************************************************************** -String _$fetchContactListHash() => r'60ae4f117fc51c0870449563aedca7baf51cc254'; +String _$fetchContactListHash() => r'f75cb33fbc664404bba122f1e128e437e0f0b2da'; /// Get the active account contact list /// diff --git a/lib/providers/contact_invite.g.dart b/lib/providers/contact_invite.g.dart index 49b1beb..c4822c5 100644 --- a/lib/providers/contact_invite.g.dart +++ b/lib/providers/contact_invite.g.dart @@ -7,7 +7,7 @@ part of 'contact_invite.dart'; // ************************************************************************** String _$fetchContactInvitationRecordsHash() => - r'fcedc1807c6cb25ac6c2c42b372ec04abd4b911f'; + r'2fe40d7aaf5fa856f00c6d2b4d9e28f4a08bed1b'; /// Get the active account contact invitation list ///