diff --git a/lib/app.dart b/lib/app.dart index a0976dc..ce24cc0 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -7,6 +7,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'router/router.dart'; +import 'tick.dart'; class VeilidChatApp extends ConsumerWidget { const VeilidChatApp({ @@ -23,24 +24,25 @@ class VeilidChatApp extends ConsumerWidget { final localizationDelegate = LocalizedApp.of(context).delegate; return ThemeProvider( - initTheme: theme, - builder: (_, theme) => LocalizationProvider( - state: LocalizationProvider.of(context).state, - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - routerConfig: router, - title: translate('app.title'), - theme: theme, - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - FormBuilderLocalizations.delegate, - localizationDelegate - ], - supportedLocales: localizationDelegate.supportedLocales, - locale: localizationDelegate.currentLocale, - )), - ); + initTheme: theme, + builder: (_, theme) => LocalizationProvider( + state: LocalizationProvider.of(context).state, + child: BackgroundTicker( + builder: (context) => MaterialApp.router( + debugShowCheckedModeBanner: false, + routerConfig: router, + title: translate('app.title'), + theme: theme, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FormBuilderLocalizations.delegate, + localizationDelegate + ], + supportedLocales: localizationDelegate.supportedLocales, + locale: localizationDelegate.currentLocale, + )), + )); } @override diff --git a/lib/components/chat_component.dart b/lib/components/chat_component.dart index fc2fa10..585d6cf 100644 --- a/lib/components/chat_component.dart +++ b/lib/components/chat_component.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; @@ -9,6 +11,7 @@ import 'package:uuid/uuid.dart'; import '../../entities/proto.dart' as proto; import '../entities/identity.dart'; import '../providers/account.dart'; +import '../providers/chat.dart'; import '../providers/conversation.dart'; import '../tools/theme_service.dart'; import '../veilid_support/veilid_support.dart'; @@ -59,6 +62,8 @@ class ChatComponentState extends ConsumerState { super.dispose(); } + externalize messages so they auto refresh and fix speed. + Future _loadMessages() async { final localConversationRecordKey = proto.TypedKeyProto.fromProto( widget.activeChatContact.localConversationRecordKey); @@ -88,8 +93,8 @@ class ChatComponentState extends ConsumerState { final textMessage = types.TextMessage( author: isLocal ? _localUser : _remoteUser, - createdAt: DateTime.now().millisecondsSinceEpoch, - id: const Uuid().v4(), + createdAt: (message.timestamp ~/ 1000).toInt(), + id: message.timestamp.toString(), text: message.text, ); return textMessage; @@ -157,16 +162,23 @@ class ChatComponentState extends ConsumerState { decoration: BoxDecoration( color: scale.primaryScale.subtleBackground, ), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Padding( - padding: - const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0), - child: Text(contactName, - textAlign: TextAlign.start, - style: textTheme.titleMedium), - ), - ), + child: Row(children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsetsDirectional.fromSTEB( + 16, 0, 16, 0), + child: Text(contactName, + textAlign: TextAlign.start, + style: textTheme.titleMedium), + )), + Spacer(), + IconButton( + icon: Icon(Icons.close), + onPressed: () async { + activeChatState.add(null); + }).paddingLTRB(16, 0, 16, 0) + ]), ), Expanded( child: Container( diff --git a/lib/components/chat_single_contact_list_widget.dart b/lib/components/chat_single_contact_list_widget.dart index 26ba08c..e63a0d7 100644 --- a/lib/components/chat_single_contact_list_widget.dart +++ b/lib/components/chat_single_contact_list_widget.dart @@ -42,6 +42,7 @@ class ChatSingleContactListWidget extends ConsumerWidget { child: (chatList.isEmpty) ? const EmptyChatListWidget() : SearchableList( + autoFocusOnSearch: false, initialList: chatList.toList(), builder: (c) { final contact = contactMap[c.remoteConversationKey]; diff --git a/lib/components/contact_list_widget.dart b/lib/components/contact_list_widget.dart index 3e76183..3e2e419 100644 --- a/lib/components/contact_list_widget.dart +++ b/lib/components/contact_list_widget.dart @@ -48,6 +48,7 @@ class ContactListWidget extends ConsumerWidget { child: (contactList.isEmpty) ? const EmptyContactListWidget().toCenter() : SearchableList( + autoFocusOnSearch: false, initialList: contactList.toList(), builder: (contact) => ContactItemWidget(contact: contact), filter: (value) { diff --git a/lib/entities/identity.dart b/lib/entities/identity.dart index 95d361c..5359897 100644 --- a/lib/entities/identity.dart +++ b/lib/entities/identity.dart @@ -137,17 +137,17 @@ extension IdentityMasterExtension on IdentityMaster { // Make empty contact list final contactList = await (await DHTShortArray.create(parent: accountRec.key)) - .scope((r) => r.record.ownedDHTRecordPointer); + .scope((r) async => r.record.ownedDHTRecordPointer); // Make empty contact invitation record list final contactInvitationRecords = await (await DHTShortArray.create(parent: accountRec.key)) - .scope((r) => r.record.ownedDHTRecordPointer); + .scope((r) async => r.record.ownedDHTRecordPointer); // Make empty chat record list final chatRecords = await (await DHTShortArray.create(parent: accountRec.key)) - .scope((r) => r.record.ownedDHTRecordPointer); + .scope((r) async => r.record.ownedDHTRecordPointer); // Make account object final account = proto.Account() diff --git a/lib/pages/chat_only_page.dart b/lib/pages/chat_only_page.dart new file mode 100644 index 0000000..ad3bee4 --- /dev/null +++ b/lib/pages/chat_only_page.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../providers/window_control.dart'; +import '../tools/tools.dart'; +import 'home.dart'; + +class ChatOnlyPage extends ConsumerStatefulWidget { + const ChatOnlyPage({super.key}); + static const path = '/chat'; + + @override + ChatOnlyPageState createState() => ChatOnlyPageState(); +} + +class ChatOnlyPageState extends ConsumerState + with TickerProviderStateMixin { + final _unfocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + setState(() {}); + await ref.read(windowControlProvider.notifier).changeWindowSetup( + TitleBarStyle.normal, OrientationCapability.normal); + }); + } + + @override + void dispose() { + _unfocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + ref.watch(windowControlProvider); + + return SafeArea( + child: GestureDetector( + onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), + child: HomePage.buildChatComponent(context, ref), + )); + } +} diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 3076288..cd04e3a 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -25,169 +25,8 @@ class HomePage extends ConsumerStatefulWidget { @override HomePageState createState() => HomePageState(); -} -// XXX Eliminate this when we have ValueChanged -const int ticksPerContactInvitationCheck = 5; -const int ticksPerNewMessageCheck = 5; - -class HomePageState extends ConsumerState - with TickerProviderStateMixin { - final _unfocusNode = FocusNode(); - - Timer? _homeTickTimer; - bool _inHomeTick = false; - int _contactInvitationCheckTick = 0; - int _newMessageCheckTick = 0; - - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - setState(() {}); - await ref.read(windowControlProvider.notifier).changeWindowSetup( - TitleBarStyle.normal, OrientationCapability.normal); - - _homeTickTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (!_inHomeTick) { - unawaited(_onHomeTick()); - } - }); - }); - } - - @override - void dispose() { - final homeTickTimer = _homeTickTimer; - if (homeTickTimer != null) { - homeTickTimer.cancel(); - } - _unfocusNode.dispose(); - super.dispose(); - } - - Future _onHomeTick() async { - _inHomeTick = true; - try { - final unord = >[]; - // Check extant contact invitations once every N seconds - _contactInvitationCheckTick += 1; - if (_contactInvitationCheckTick >= ticksPerContactInvitationCheck) { - _contactInvitationCheckTick = 0; - 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; - } - } - - Future _doContactInvitationCheck() async { - final contactInvitationRecords = - await ref.read(fetchContactInvitationRecordsProvider.future); - final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future); - if (contactInvitationRecords == null || activeAccountInfo == null) { - return; - } - - final allChecks = >[]; - for (final contactInvitationRecord in contactInvitationRecords) { - allChecks.add(() async { - final acceptReject = await checkAcceptRejectContact( - activeAccountInfo: activeAccountInfo, - contactInvitationRecord: contactInvitationRecord); - if (acceptReject != null) { - final acceptedContact = acceptReject.acceptedContact; - if (acceptedContact != null) { - // Accept - await createContact( - activeAccountInfo: activeAccountInfo, - profile: acceptedContact.profile, - remoteIdentity: acceptedContact.remoteIdentity, - remoteConversationRecordKey: - acceptedContact.remoteConversationRecordKey, - localConversationRecordKey: - acceptedContact.localConversationRecordKey, - ); - ref - ..invalidate(fetchContactInvitationRecordsProvider) - ..invalidate(fetchContactListProvider); - } else { - // Reject - ref.invalidate(fetchContactInvitationRecordsProvider); - } - } - }()); - } - await Future.wait(allChecks); - } - - Future _doNewMessageCheck() async { - final activeChat = activeChatState.currentState; - if (activeChat == null) { - return; - } - final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future); - if (activeAccountInfo == null) { - return; - } - - final contactList = ref.read(fetchContactListProvider).asData?.value ?? - const IListConst([]); - - final activeChatContactIdx = contactList.indexWhere( - (c) => - proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == - activeChat, - ); - if (activeChatContactIdx == -1) { - return; - } - final activeChatContact = contactList[activeChatContactIdx]; - final remoteIdentityPublicKey = - proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey); - final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( - activeChatContact.remoteConversationRecordKey); - - await getRemoteConversationMessages( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey, - remoteConversationRecordKey: remoteConversationRecordKey); - - // xxx add messages - } - - // ignore: prefer_expression_function_bodies - Widget buildPhone(BuildContext context) { - // - return Material( - color: Colors.transparent, elevation: 4, child: MainPager()); - } - - // ignore: prefer_expression_function_bodies - Widget buildTabletLeftPane(BuildContext context) { - // - return Material( - color: Colors.transparent, elevation: 4, child: MainPager()); - } - - // ignore: prefer_expression_function_bodies - Widget buildTabletRightPane(BuildContext context) { - // - return buildChatComponent(context); - } - - Widget buildChatComponent(BuildContext context) { + static Widget buildChatComponent(BuildContext context, WidgetRef ref) { final contactList = ref.watch(fetchContactListProvider).asData?.value ?? const IListConst([]); @@ -218,6 +57,47 @@ class HomePageState extends ConsumerState activeChat: activeChat, activeChatContact: activeChatContact); } +} + +class HomePageState extends ConsumerState + with TickerProviderStateMixin { + final _unfocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + setState(() {}); + await ref.read(windowControlProvider.notifier).changeWindowSetup( + TitleBarStyle.normal, OrientationCapability.normal); + }); + } + + @override + void dispose() { + _unfocusNode.dispose(); + super.dispose(); + } + + // ignore: prefer_expression_function_bodies + Widget buildPhone(BuildContext context) { + return Material( + color: Colors.transparent, elevation: 4, child: MainPager()); + } + + // ignore: prefer_expression_function_bodies + Widget buildTabletLeftPane(BuildContext context) { + // + return Material( + color: Colors.transparent, elevation: 4, child: MainPager()); + } + + // ignore: prefer_expression_function_bodies + Widget buildTabletRightPane(BuildContext context) { + // + return HomePage.buildChatComponent(context, ref); + } // ignore: prefer_expression_function_bodies Widget buildTablet(BuildContext context) { diff --git a/lib/pages/main_pager/main_pager.dart b/lib/pages/main_pager/main_pager.dart index 0130a44..18b49bd 100644 --- a/lib/pages/main_pager/main_pager.dart +++ b/lib/pages/main_pager/main_pager.dart @@ -34,8 +34,8 @@ class MainPagerState extends ConsumerState final _unfocusNode = FocusNode(); - final pageController = PageController(); var _currentPage = 0; + final pageController = PageController(); final _selectedIconList = [Icons.person, Icons.chat]; // final _unselectedIconList = [ @@ -235,6 +235,11 @@ class MainPagerState extends ConsumerState onNotification: onScrollNotification, child: PageView( controller: pageController, + onPageChanged: (index) { + setState(() { + _currentPage = index; + }); + }, //physics: const NeverScrollableScrollPhysics(), children: List.generate( _bottomBarPages.length, (index) => _bottomBarPages[index]), @@ -271,9 +276,6 @@ class MainPagerState extends ConsumerState onTap: (index) async { await pageController.animateToPage(index, duration: 250.ms, curve: Curves.easeInOut); - setState(() { - _currentPage = index; - }); }, ), diff --git a/lib/providers/conversation.dart b/lib/providers/conversation.dart index 2af9ca7..7caa63a 100644 --- a/lib/providers/conversation.dart +++ b/lib/providers/conversation.dart @@ -7,6 +7,7 @@ import '../entities/identity.dart'; import '../entities/proto.dart' as proto; import '../entities/proto.dart' show Conversation; +import '../log/loggy.dart'; import '../veilid_support/veilid_support.dart'; import 'account.dart'; @@ -194,6 +195,71 @@ Future addLocalConversationMessage( }); } +Future mergeLocalConversationMessages( + {required ActiveAccountInfo activeAccountInfo, + required TypedKey localConversationRecordKey, + required TypedKey remoteIdentityPublicKey, + required IList newMessages}) async { + final conversation = await readLocalConversation( + activeAccountInfo: activeAccountInfo, + localConversationRecordKey: localConversationRecordKey, + remoteIdentityPublicKey: remoteIdentityPublicKey); + if (conversation == null) { + return false; + } + bool changed = false; + final messagesRecordKey = + proto.TypedKeyProto.fromProto(conversation.messages); + final crypto = await getConversationCrypto( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: remoteIdentityPublicKey); + final writer = getConversationWriter(activeAccountInfo: activeAccountInfo); + + newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp) + .compareTo(Timestamp.fromInt64(b.timestamp))); + + await (await DHTShortArray.openWrite(messagesRecordKey, writer, + parent: localConversationRecordKey, crypto: crypto)) + .scope((messages) async { + // Ensure newMessages is sorted by timestamp + newMessages = + newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + + // Existing messages will always be sorted by timestamp so merging is easy + var pos = 0; + outer: + for (final newMessage in newMessages) { + var skip = false; + while (pos < messages.length) { + final m = await messages.getItemProtobuf(proto.Message.fromBuffer, pos); + if (m == null) { + log.error('unable to get message #$pos'); + break outer; + } + + // If timestamp to insert is less than + // the current position, insert it here + final newTs = Timestamp.fromInt64(newMessage.timestamp); + final curTs = Timestamp.fromInt64(m.timestamp); + final cmp = newTs.compareTo(curTs); + if (cmp < 0) { + break; + } else if (cmp == 0) { + skip = true; + break; + } + pos++; + } + // Insert at this position + if (!skip) { + await messages.tryInsertItem(pos, newMessage.writeToBuffer()); + changed = true; + } + } + }); + return changed; +} + Future?> getLocalConversationMessages({ required ActiveAccountInfo activeAccountInfo, required TypedKey localConversationRecordKey, diff --git a/lib/router/router_notifier.dart b/lib/router/router_notifier.dart index ef8d723..70ffb36 100644 --- a/lib/router/router_notifier.dart +++ b/lib/router/router_notifier.dart @@ -2,10 +2,13 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../pages/chat_only_page.dart'; import '../pages/home.dart'; import '../pages/index.dart'; import '../pages/new_account.dart'; +import '../providers/chat.dart'; import '../providers/local_accounts.dart'; +import '../tools/responsive.dart'; part 'router_notifier.g.dart'; @@ -16,6 +19,7 @@ class RouterNotifier extends _$RouterNotifier implements Listenable { /// Do we need to make or import an account immediately? bool hasAnyAccount = false; + bool hasActiveChat = false; /// AsyncNotifier build @override @@ -23,6 +27,7 @@ class RouterNotifier extends _$RouterNotifier implements Listenable { hasAnyAccount = await ref.watch( localAccountsProvider.selectAsync((data) => data.isNotEmpty), ); + hasActiveChat = ref.watch(activeChatStateProvider).asData?.value != null; // When this notifier's state changes, inform GoRouter ref.listenSelf((_, __) { @@ -46,6 +51,36 @@ class RouterNotifier extends _$RouterNotifier implements Listenable { return hasAnyAccount ? HomePage.path : NewAccountPage.path; case NewAccountPage.path: return hasAnyAccount ? HomePage.path : null; + case HomePage.path: + if (!hasAnyAccount) { + return NewAccountPage.path; + } + if (responsiveVisibility( + context: context, + tablet: false, + tabletLandscape: false, + desktop: false)) { + if (hasActiveChat) { + return ChatOnlyPage.path; + } + } + return null; + case ChatOnlyPage.path: + if (!hasAnyAccount) { + return NewAccountPage.path; + } + if (responsiveVisibility( + context: context, + tablet: false, + tabletLandscape: false, + desktop: false)) { + if (!hasActiveChat) { + return HomePage.path; + } + } else { + return HomePage.path; + } + return null; default: return hasAnyAccount ? null : NewAccountPage.path; } @@ -65,6 +100,10 @@ class RouterNotifier extends _$RouterNotifier implements Listenable { path: NewAccountPage.path, builder: (context, state) => const NewAccountPage(), ), + GoRoute( + path: ChatOnlyPage.path, + builder: (context, state) => const ChatOnlyPage(), + ), ]; /////////////////////////////////////////////////////////////////////////// diff --git a/lib/tick.dart b/lib/tick.dart new file mode 100644 index 0000000..0b5e9b4 --- /dev/null +++ b/lib/tick.dart @@ -0,0 +1,165 @@ +// XXX Eliminate this when we have ValueChanged +import 'dart:async'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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/conversation.dart'; + +const int ticksPerContactInvitationCheck = 5; +const int ticksPerNewMessageCheck = 5; + +class BackgroundTicker extends ConsumerStatefulWidget { + const BackgroundTicker({required this.builder, super.key}); + + final Widget Function(BuildContext) builder; + + @override + BackgroundTickerState createState() => BackgroundTickerState(); +} + +class BackgroundTickerState extends ConsumerState { + Timer? _tickTimer; + bool _inTick = false; + int _contactInvitationCheckTick = 0; + int _newMessageCheckTick = 0; + + @override + void initState() { + super.initState(); + _tickTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (!_inTick) { + unawaited(_onTick()); + } + }); + } + + @override + void dispose() { + final tickTimer = _tickTimer; + if (tickTimer != null) { + tickTimer.cancel(); + } + + super.dispose(); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + return widget.builder(context); + } + + Future _onTick() async { + _inTick = true; + try { + final unord = >[]; + // Check extant contact invitations once every N seconds + _contactInvitationCheckTick += 1; + if (_contactInvitationCheckTick >= ticksPerContactInvitationCheck) { + _contactInvitationCheckTick = 0; + 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 { + _inTick = false; + } + } + + Future _doContactInvitationCheck() async { + final contactInvitationRecords = + await ref.read(fetchContactInvitationRecordsProvider.future); + final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future); + if (contactInvitationRecords == null || activeAccountInfo == null) { + return; + } + + final allChecks = >[]; + for (final contactInvitationRecord in contactInvitationRecords) { + allChecks.add(() async { + final acceptReject = await checkAcceptRejectContact( + activeAccountInfo: activeAccountInfo, + contactInvitationRecord: contactInvitationRecord); + if (acceptReject != null) { + final acceptedContact = acceptReject.acceptedContact; + if (acceptedContact != null) { + // Accept + await createContact( + activeAccountInfo: activeAccountInfo, + profile: acceptedContact.profile, + remoteIdentity: acceptedContact.remoteIdentity, + remoteConversationRecordKey: + acceptedContact.remoteConversationRecordKey, + localConversationRecordKey: + acceptedContact.localConversationRecordKey, + ); + ref + ..invalidate(fetchContactInvitationRecordsProvider) + ..invalidate(fetchContactListProvider); + } else { + // Reject + ref.invalidate(fetchContactInvitationRecordsProvider); + } + } + }()); + } + await Future.wait(allChecks); + } + + Future _doNewMessageCheck() async { + final activeChat = activeChatState.currentState; + if (activeChat == null) { + return; + } + final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future); + if (activeAccountInfo == null) { + return; + } + + final contactList = ref.read(fetchContactListProvider).asData?.value ?? + const IListConst([]); + + final activeChatContactIdx = contactList.indexWhere( + (c) => + proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == + activeChat, + ); + if (activeChatContactIdx == -1) { + return; + } + final activeChatContact = contactList[activeChatContactIdx]; + final remoteIdentityPublicKey = + proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey); + final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( + activeChatContact.remoteConversationRecordKey); + final localConversationRecordKey = proto.TypedKeyProto.fromProto( + activeChatContact.localConversationRecordKey); + + final newMessages = await getRemoteConversationMessages( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: remoteIdentityPublicKey, + remoteConversationRecordKey: remoteConversationRecordKey); + if (newMessages != null) { + await mergeLocalConversationMessages( + activeAccountInfo: activeAccountInfo, + localConversationRecordKey: localConversationRecordKey, + remoteIdentityPublicKey: remoteIdentityPublicKey, + newMessages: newMessages); + } + } +} diff --git a/lib/veilid_support/dht_support/dht_record.dart b/lib/veilid_support/dht_support/dht_record.dart index 648ce49..72c3f41 100644 --- a/lib/veilid_support/dht_support/dht_record.dart +++ b/lib/veilid_support/dht_support/dht_record.dart @@ -66,7 +66,7 @@ class DHTRecord { _valid = false; } - Future scope(FutureOr Function(DHTRecord) scopeFunction) async { + Future scope(Future Function(DHTRecord) scopeFunction) async { try { return await scopeFunction(this); } finally { @@ -76,8 +76,7 @@ class DHTRecord { } } - Future deleteScope( - FutureOr Function(DHTRecord) scopeFunction) async { + Future deleteScope(Future Function(DHTRecord) scopeFunction) async { try { final out = await scopeFunction(this); if (_valid && _open) { diff --git a/lib/veilid_support/dht_support/dht_short_array.dart b/lib/veilid_support/dht_support/dht_short_array.dart index 2f3d175..435aba5 100644 --- a/lib/veilid_support/dht_support/dht_short_array.dart +++ b/lib/veilid_support/dht_support/dht_short_array.dart @@ -42,10 +42,10 @@ class DHTShortArray { } stride = oCnt - 1; case DHTSchemaSMPL(oCnt: final oCnt, members: final members): - if (oCnt != 0 || members.length != 1 || members[1].mCnt <= 1) { + if (oCnt != 0 || members.length != 1 || members[0].mCnt <= 1) { throw StateError('Invalid SMPL schema in DHTShortArray'); } - stride = members[1].mCnt - 1; + stride = members[0].mCnt - 1; } assert(stride <= maxElements, 'stride too long'); _stride = stride; @@ -117,7 +117,7 @@ class DHTShortArray { parent: parent, routingContext: routingContext, crypto: crypto); try { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); - await dhtShortArray._refreshHead(); + await dhtShortArray._refreshHead(forceRefresh: true); return dhtShortArray; } on Exception catch (_) { await dhtRecord.close(); @@ -137,7 +137,7 @@ class DHTShortArray { parent: parent, routingContext: routingContext, crypto: crypto); try { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); - await dhtShortArray._refreshHead(); + await dhtShortArray._refreshHead(forceRefresh: true); return dhtShortArray; } on Exception catch (_) { await dhtRecord.close(); @@ -315,7 +315,7 @@ class DHTShortArray { await Future.wait(futures); } - Future scope(FutureOr Function(DHTShortArray) scopeFunction) async { + Future scope(Future Function(DHTShortArray) scopeFunction) async { try { return await scopeFunction(this); } finally { @@ -324,7 +324,7 @@ class DHTShortArray { } Future deleteScope( - FutureOr Function(DHTShortArray) scopeFunction) async { + Future Function(DHTShortArray) scopeFunction) async { try { final out = await scopeFunction(this); await close();