checkpoint

This commit is contained in:
Christien Rioux 2024-06-15 00:01:08 -04:00
parent 56d65442f4
commit 751022e743
26 changed files with 482 additions and 303 deletions

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:protobuf/protobuf.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
@ -7,6 +8,11 @@ import '../account_manager.dart';
typedef AccountRecordState = proto.Account; typedef AccountRecordState = proto.Account;
/// The saved state of a VeilidChat Account on the DHT
/// Used to synchronize status, profile, and options for a specific account
/// across multiple clients. This DHT record is the 'source of truth' for an
/// account and is privately encrypted with an owned recrod from the 'userLogin'
/// tabledb-local storage, encrypted by the unlock code for the account.
class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> { class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
AccountRecordCubit( AccountRecordCubit(
{required AccountRepository accountRepository, {required AccountRepository accountRepository,
@ -35,4 +41,16 @@ class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
Future<void> close() async { Future<void> close() async {
await super.close(); await super.close();
} }
////////////////////////////////////////////////////////////////////////////
// Public Interface
Future<void> updateProfile(proto.Profile profile) async {
await record.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async {
if (old == null || old.profile == profile) {
return null;
}
return old.deepCopy()..profile = profile;
});
}
} }

View File

@ -7,7 +7,8 @@ import '../../account_manager/account_manager.dart';
typedef AccountRecordsBlocMapState typedef AccountRecordsBlocMapState
= BlocMapState<TypedKey, AsyncValue<AccountRecordState>>; = BlocMapState<TypedKey, AsyncValue<AccountRecordState>>;
// Map of the logged in user accounts to their account information /// Map of the logged in user accounts to their AccountRecordCubit
/// Ensures there is an single account record cubit for each logged in account
class AccountRecordsBlocMapCubit extends BlocMapCubit<TypedKey, class AccountRecordsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<AccountRecordState>, AccountRecordCubit> AsyncValue<AccountRecordState>, AccountRecordCubit>
with StateMapFollower<UserLoginsState, TypedKey, UserLogin> { with StateMapFollower<UserLoginsState, TypedKey, UserLogin> {

View File

@ -8,8 +8,6 @@ import 'package:go_router/go_router.dart';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../contacts/cubits/contact_list_cubit.dart';
import '../../conversation/conversation.dart';
import '../../layout/default_app_bar.dart'; import '../../layout/default_app_bar.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart'; import '../../theme/theme.dart';
@ -41,7 +39,6 @@ class EditAccountPage extends StatefulWidget {
} }
class _EditAccountPageState extends State<EditAccountPage> { class _EditAccountPageState extends State<EditAccountPage> {
final _formKey = GlobalKey<FormBuilderState>();
bool _isInAsyncCall = false; bool _isInAsyncCall = false;
@override @override
@ -58,24 +55,37 @@ class _EditAccountPageState extends State<EditAccountPage> {
{required Future<void> Function(GlobalKey<FormBuilderState>) {required Future<void> Function(GlobalKey<FormBuilderState>)
onSubmit}) => onSubmit}) =>
EditProfileForm( EditProfileForm(
header: translate('edit_account_page.header'), header: translate('edit_account_page.header'),
instructions: translate('edit_account_page.instructions'), instructions: translate('edit_account_page.instructions'),
submitText: translate('edit_account_page.update'), submitText: translate('edit_account_page.update'),
submitDisabledText: translate('button.waiting_for_network'), submitDisabledText: translate('button.waiting_for_network'),
onSubmit: onSubmit); onSubmit: onSubmit,
initialValueCallback: (key) => switch (key) {
EditProfileForm.formFieldName => widget.existingProfile.name,
EditProfileForm.formFieldPronouns => widget.existingProfile.pronouns,
String() => throw UnimplementedError(),
},
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final displayModalHUD = _isInAsyncCall; final displayModalHUD = _isInAsyncCall;
final accountRecordCubit = context.read<AccountRecordCubit>(); final accountRecordsCubit = context.watch<AccountRecordsBlocMapCubit>();
final activeConversationsBlocMapCubit = final accountRecordCubit = accountRecordsCubit
context.read<ActiveConversationsBlocMapCubit>(); .operate(widget.superIdentityRecordKey, closure: (c) => c);
final contactListCubit = context.read<ContactListCubit>();
return Scaffold( return Scaffold(
// resizeToAvoidBottomInset: false, // resizeToAvoidBottomInset: false,
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: Text(translate('edit_account_page.titlebar')), title: Text(translate('edit_account_page.titlebar')),
leading: Navigator.canPop(context)
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
)
: null,
actions: [ actions: [
const SignalStrengthMeterWidget(), const SignalStrengthMeterWidget(),
IconButton( IconButton(
@ -92,57 +102,35 @@ class _EditAccountPageState extends State<EditAccountPage> {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
try { try {
final name = _formKey.currentState! final name = formKey.currentState!
.fields[EditProfileForm.formFieldName]!.value as String; .fields[EditProfileForm.formFieldName]!.value as String;
final pronouns = _formKey final pronouns = formKey
.currentState! .currentState!
.fields[EditProfileForm.formFieldPronouns]! .fields[EditProfileForm.formFieldPronouns]!
.value as String? ?? .value as String? ??
''; '';
final newProfile = widget.existingProfile.deepCopy() final newProfile = widget.existingProfile.deepCopy()
..name = name ..name = name
..pronouns = pronouns; ..pronouns = pronouns
..timestamp = Veilid.instance.now().toInt64();
setState(() { setState(() {
_isInAsyncCall = true; _isInAsyncCall = true;
}); });
try { try {
// Update account profile DHT record // Update account profile DHT record
final newValue = await accountRecordCubit.record // This triggers ConversationCubits to update
.tryWriteProtobuf(proto.Account.fromBuffer, newProfile); await accountRecordCubit.updateProfile(newProfile);
if (newValue != null) {
if (context.mounted) {
await showErrorModal(
context,
translate('edit_account_page.error'),
'Failed to update profile online');
return;
}
}
// Update local account profile // Update local account profile
await AccountRepository.instance.editAccountProfile( await AccountRepository.instance.editAccountProfile(
widget.superIdentityRecordKey, newProfile); widget.superIdentityRecordKey, newProfile);
// Update all conversations with new profile if (context.mounted) {
final updates = <Future<void>>[]; Navigator.canPop(context)
for (final key in activeConversationsBlocMapCubit.state.keys) { ? GoRouterHelper(context).pop()
await activeConversationsBlocMapCubit.operateAsync(key, : GoRouterHelper(context).go('/');
closure: (cubit) async {
final newLocalConversation =
cubit.state.asData?.value.localConversation.deepCopy();
if (newLocalConversation != null) {
newLocalConversation.profile = newProfile;
updates.add(cubit.input.writeLocalConversation(
conversation: newLocalConversation));
}
});
} }
// Wait for updates
await updates.wait;
// XXX: how to do this for non-chat contacts?
} finally { } finally {
if (mounted) { if (mounted) {
setState(() { setState(() {

View File

@ -21,7 +21,6 @@ class NewAccountPage extends StatefulWidget {
} }
class _NewAccountPageState extends State<NewAccountPage> { class _NewAccountPageState extends State<NewAccountPage> {
final _formKey = GlobalKey<FormBuilderState>();
bool _isInAsyncCall = false; bool _isInAsyncCall = false;
@override @override
@ -61,6 +60,14 @@ class _NewAccountPageState extends State<NewAccountPage> {
// resizeToAvoidBottomInset: false, // resizeToAvoidBottomInset: false,
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: Text(translate('new_account_page.titlebar')), title: Text(translate('new_account_page.titlebar')),
leading: Navigator.canPop(context)
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
)
: null,
actions: [ actions: [
const SignalStrengthMeterWidget(), const SignalStrengthMeterWidget(),
IconButton( IconButton(
@ -77,9 +84,9 @@ class _NewAccountPageState extends State<NewAccountPage> {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
try { try {
final name = _formKey.currentState! final name = formKey.currentState!
.fields[EditProfileForm.formFieldName]!.value as String; .fields[EditProfileForm.formFieldName]!.value as String;
final pronouns = _formKey final pronouns = formKey
.currentState! .currentState!
.fields[EditProfileForm.formFieldPronouns]! .fields[EditProfileForm.formFieldPronouns]!
.value as String? ?? .value as String? ??

View File

@ -13,6 +13,7 @@ class EditProfileForm extends StatefulWidget {
required this.submitDisabledText, required this.submitDisabledText,
super.key, super.key,
this.onSubmit, this.onSubmit,
this.initialValueCallback,
}); });
@override @override
@ -23,6 +24,7 @@ class EditProfileForm extends StatefulWidget {
final Future<void> Function(GlobalKey<FormBuilderState>)? onSubmit; final Future<void> Function(GlobalKey<FormBuilderState>)? onSubmit;
final String submitText; final String submitText;
final String submitDisabledText; final String submitDisabledText;
final Object? Function(String key)? initialValueCallback;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@ -34,7 +36,9 @@ class EditProfileForm extends StatefulWidget {
Future<void> Function( Future<void> Function(
GlobalKey<FormBuilderState> p1)?>.has('onSubmit', onSubmit)) GlobalKey<FormBuilderState> p1)?>.has('onSubmit', onSubmit))
..add(StringProperty('submitText', submitText)) ..add(StringProperty('submitText', submitText))
..add(StringProperty('submitDisabledText', submitDisabledText)); ..add(StringProperty('submitDisabledText', submitDisabledText))
..add(ObjectFlagProperty<Object? Function(String key)?>.has(
'initialValueCallback', initialValueCallback));
} }
static const String formFieldName = 'name'; static const String formFieldName = 'name';
@ -62,6 +66,8 @@ class _EditProfileFormState extends State<EditProfileForm> {
FormBuilderTextField( FormBuilderTextField(
autofocus: true, autofocus: true,
name: EditProfileForm.formFieldName, name: EditProfileForm.formFieldName,
initialValue: widget.initialValueCallback
?.call(EditProfileForm.formFieldName) as String?,
decoration: decoration:
InputDecoration(labelText: translate('account.form_name')), InputDecoration(labelText: translate('account.form_name')),
maxLength: 64, maxLength: 64,
@ -73,6 +79,8 @@ class _EditProfileFormState extends State<EditProfileForm> {
), ),
FormBuilderTextField( FormBuilderTextField(
name: EditProfileForm.formFieldPronouns, name: EditProfileForm.formFieldPronouns,
initialValue: widget.initialValueCallback
?.call(EditProfileForm.formFieldPronouns) as String?,
maxLength: 64, maxLength: 64,
decoration: InputDecoration( decoration: InputDecoration(
labelText: translate('account.form_pronouns')), labelText: translate('account.form_pronouns')),

View File

@ -1,18 +1,12 @@
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../layout/default_app_bar.dart'; import '../../layout/default_app_bar.dart';
import '../../theme/theme.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../../veilid_processor/veilid_processor.dart'; import '../../veilid_processor/veilid_processor.dart';
import '../account_manager.dart';
class ShowRecoveryKeyPage extends StatefulWidget { class ShowRecoveryKeyPage extends StatefulWidget {
const ShowRecoveryKeyPage({required SecretKey secretKey, super.key}) const ShowRecoveryKeyPage({required SecretKey secretKey, super.key})
@ -57,7 +51,11 @@ class ShowRecoveryKeyPageState extends State<ShowRecoveryKeyPage> {
Text('ASS: $secretKey'), Text('ASS: $secretKey'),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
GoRouterHelper(context).go('/'); if (context.mounted) {
Navigator.canPop(context)
? GoRouterHelper(context).pop()
: GoRouterHelper(context).go('/');
}
}, },
child: Text(translate('button.finish'))) child: Text(translate('button.finish')))
]).paddingSymmetric(horizontal: 24, vertical: 8)); ]).paddingSymmetric(horizontal: 24, vertical: 8));

View File

@ -1,3 +1,4 @@
export 'edit_account_page.dart';
export 'new_account_page.dart'; export 'new_account_page.dart';
export 'profile_widget.dart'; export 'profile_widget.dart';
export 'show_recovery_key_page.dart'; export 'show_recovery_key_page.dart';

View File

@ -1,12 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:async_tools/async_tools.dart';
import 'package:protobuf/protobuf.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import 'conversation_cubit.dart'; import '../../conversation/cubits/conversation_cubit.dart';
////////////////////////////////////////////////// //////////////////////////////////////////////////
// Mutable state for per-account contacts // Mutable state for per-account contacts
@ -34,6 +36,54 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
return dhtRecord; return dhtRecord;
} }
@override
Future<void> close() async {
await _contactProfileUpdateMap.close();
await super.close();
}
////////////////////////////////////////////////////////////////////////////
// Public Interface
void followContactProfileChanges(TypedKey localConversationRecordKey,
Stream<proto.Profile?> profileStream, proto.Profile? profileState) {
_contactProfileUpdateMap
.follow(localConversationRecordKey, profileStream, profileState,
(remoteProfile) async {
if (remoteProfile == null) {
return;
}
return updateContactRemoteProfile(
localConversationRecordKey: localConversationRecordKey,
remoteProfile: remoteProfile);
});
}
Future<void> updateContactRemoteProfile({
required TypedKey localConversationRecordKey,
required proto.Profile remoteProfile,
}) async {
// Update contact's remoteProfile
await operateWriteEventual((writer) async {
for (var pos = 0; pos < writer.length; pos++) {
final c = await writer.getProtobuf(proto.Contact.fromBuffer, pos);
if (c != null &&
c.localConversationRecordKey.toVeilid() ==
localConversationRecordKey) {
if (c.remoteProfile == remoteProfile) {
// Unchanged
break;
}
final newContact = c.deepCopy()..remoteProfile = remoteProfile;
final updated = await writer.tryWriteItemProtobuf(
proto.Contact.fromBuffer, pos, newContact);
if (!updated) {
throw DHTExceptionTryAgain();
}
}
}
});
}
Future<void> createContact({ Future<void> createContact({
required proto.Profile remoteProfile, required proto.Profile remoteProfile,
required SuperIdentity remoteSuperIdentity, required SuperIdentity remoteSuperIdentity,
@ -100,4 +150,6 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
} }
final UnlockedAccountInfo _activeAccountInfo; final UnlockedAccountInfo _activeAccountInfo;
final _contactProfileUpdateMap =
SingleStateProcessorMap<TypedKey, proto.Profile?>();
} }

View File

@ -5,10 +5,10 @@ import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../chat_list/cubits/cubits.dart';
import '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
import '../../conversation/conversation.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import 'cubits.dart'; import '../conversation.dart';
@immutable @immutable
class ActiveConversationState extends Equatable { class ActiveConversationState extends Equatable {
@ -43,11 +43,13 @@ typedef ActiveConversationsBlocMapState
class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey, class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<ActiveConversationState>, ActiveConversationCubit> AsyncValue<ActiveConversationState>, ActiveConversationCubit>
with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> { with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> {
ActiveConversationsBlocMapCubit( ActiveConversationsBlocMapCubit({
{required UnlockedAccountInfo unlockedAccountInfo, required UnlockedAccountInfo unlockedAccountInfo,
required ContactListCubit contactListCubit}) required ContactListCubit contactListCubit,
: _activeAccountInfo = unlockedAccountInfo, required AccountRecordCubit accountRecordCubit,
_contactListCubit = contactListCubit; }) : _activeAccountInfo = unlockedAccountInfo,
_contactListCubit = contactListCubit,
_accountRecordCubit = accountRecordCubit;
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Public Interface // Public Interface
@ -57,30 +59,51 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
// Add an active conversation to be tracked for changes // Add an active conversation to be tracked for changes
Future<void> _addConversation({required proto.Contact contact}) async => Future<void> _addConversation({required proto.Contact contact}) async =>
add(() => MapEntry( add(() {
contact.localConversationRecordKey.toVeilid(), final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
TransformerCubit( final localConversationRecordKey =
ConversationCubit( contact.localConversationRecordKey.toVeilid();
activeAccountInfo: _activeAccountInfo, final remoteConversationRecordKey =
remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), contact.remoteConversationRecordKey.toVeilid();
localConversationRecordKey:
contact.localConversationRecordKey.toVeilid(), // Conversation cubit the tracks the state between the local
remoteConversationRecordKey: // and remote halves of a contact's relationship with this account
contact.remoteConversationRecordKey.toVeilid(), final conversationCubit = ConversationCubit(
), activeAccountInfo: _activeAccountInfo,
// Transformer that only passes through completed conversations remoteIdentityPublicKey: remoteIdentityPublicKey,
// along with the contact that corresponds to the completed localConversationRecordKey: localConversationRecordKey,
// conversation remoteConversationRecordKey: remoteConversationRecordKey,
transform: (avstate) => avstate.when( )..watchAccountChanges(
data: (data) => (data.localConversation == null || _accountRecordCubit.stream, _accountRecordCubit.state);
data.remoteConversation == null) _contactListCubit.followContactProfileChanges(
? const AsyncValue.loading() localConversationRecordKey,
: AsyncValue.data(ActiveConversationState( conversationCubit.stream.map((x) => x.map(
contact: contact, data: (d) => d.value.remoteConversation?.profile,
localConversation: data.localConversation!, loading: (_) => null,
remoteConversation: data.remoteConversation!)), error: (_) => null)),
loading: AsyncValue.loading, conversationCubit.state.asData?.value.remoteConversation?.profile);
error: AsyncValue.error))));
// Transformer that only passes through completed/active conversations
// along with the contact that corresponds to the completed
// conversation
final transformedCubit = TransformerCubit<
AsyncValue<ActiveConversationState>,
AsyncValue<ConversationState>,
ConversationCubit>(conversationCubit,
transform: (avstate) => avstate.when(
data: (data) => (data.localConversation == null ||
data.remoteConversation == null)
? const AsyncValue.loading()
: AsyncValue.data(ActiveConversationState(
contact: contact,
localConversation: data.localConversation!,
remoteConversation: data.remoteConversation!)),
loading: AsyncValue.loading,
error: AsyncValue.error));
return MapEntry(
contact.localConversationRecordKey.toVeilid(), transformedCubit);
});
/// StateFollower ///////////////////////// /// StateFollower /////////////////////////
@ -108,4 +131,5 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
final UnlockedAccountInfo _activeAccountInfo; final UnlockedAccountInfo _activeAccountInfo;
final ContactListCubit _contactListCubit; final ContactListCubit _contactListCubit;
final AccountRecordCubit _accountRecordCubit;
} }

View File

@ -9,7 +9,7 @@ import '../../chat/chat.dart';
import '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import 'active_conversations_bloc_map_cubit.dart'; import 'active_conversations_bloc_map_cubit.dart';
import 'chat_list_cubit.dart'; import '../../chat_list/cubits/chat_list_cubit.dart';
// Map of localConversationRecordKey to MessagesCubit // Map of localConversationRecordKey to MessagesCubit
// Wraps a MessagesCubit to stream the latest messages to the state // Wraps a MessagesCubit to stream the latest messages to the state

View File

@ -9,12 +9,15 @@ import 'package:async_tools/async_tools.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:protobuf/protobuf.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
const _sfUpdateAccountChange = 'updateAccountChange';
@immutable @immutable
class ConversationState extends Equatable { class ConversationState extends Equatable {
const ConversationState( const ConversationState(
@ -27,6 +30,9 @@ class ConversationState extends Equatable {
List<Object?> get props => [localConversation, remoteConversation]; List<Object?> get props => [localConversation, remoteConversation];
} }
/// Represents the control channel between two contacts
/// Used to pass profile, identity and status changes, and the messages key for
/// 1-1 chats
class ConversationCubit extends Cubit<AsyncValue<ConversationState>> { class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
ConversationCubit( ConversationCubit(
{required UnlockedAccountInfo activeAccountInfo, {required UnlockedAccountInfo activeAccountInfo,
@ -53,6 +59,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
debugName: 'ConversationCubit::LocalConversation', debugName: 'ConversationCubit::LocalConversation',
parent: accountRecordKey, parent: accountRecordKey,
crypto: crypto); crypto: crypto);
return record; return record;
}); });
}); });
@ -80,6 +87,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
@override @override
Future<void> close() async { Future<void> close() async {
await _initWait(); await _initWait();
await _accountSubscription?.cancel();
await _localSubscription?.cancel(); await _localSubscription?.cancel();
await _remoteSubscription?.cancel(); await _remoteSubscription?.cancel();
await _localConversationCubit?.close(); await _localConversationCubit?.close();
@ -88,127 +96,16 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
await super.close(); await super.close();
} }
void _updateLocalConversationState(AsyncValue<proto.Conversation> avconv) { ////////////////////////////////////////////////////////////////////////////
final newState = avconv.when( // Public Interface
data: (conv) {
_incrementalState = ConversationState(
localConversation: conv,
remoteConversation: _incrementalState.remoteConversation);
// return loading still if state isn't complete
if ((_localConversationRecordKey != null &&
_incrementalState.localConversation == null) ||
(_remoteConversationRecordKey != null &&
_incrementalState.remoteConversation == null)) {
return const AsyncValue<ConversationState>.loading();
}
// state is complete, all required keys are open
return AsyncValue.data(_incrementalState);
},
loading: AsyncValue<ConversationState>.loading,
error: AsyncValue<ConversationState>.error,
);
emit(newState);
}
void _updateRemoteConversationState(AsyncValue<proto.Conversation> avconv) { /// Initialize a local conversation
final newState = avconv.when( /// If we were the initiator of the conversation there may be an
data: (conv) { /// incomplete 'existingConversationRecord' that we need to fill
_incrementalState = ConversationState( /// in now that we have the remote identity key
localConversation: _incrementalState.localConversation, /// The ConversationCubit must not already have a local conversation
remoteConversation: conv); /// The callback allows for more initialization to occur and for
// return loading still if state isn't complete /// cleanup to delete records upon failure of the callback
if ((_localConversationRecordKey != null &&
_incrementalState.localConversation == null) ||
(_remoteConversationRecordKey != null &&
_incrementalState.remoteConversation == null)) {
return const AsyncValue<ConversationState>.loading();
}
// state is complete, all required keys are open
return AsyncValue.data(_incrementalState);
},
loading: AsyncValue<ConversationState>.loading,
error: AsyncValue<ConversationState>.error,
);
emit(newState);
}
// Open local converation key
Future<void> _setLocalConversation(Future<DHTRecord> Function() open) async {
assert(_localConversationCubit == null,
'shoud not set local conversation twice');
_localConversationCubit = DefaultDHTRecordCubit(
open: open, decodeState: proto.Conversation.fromBuffer);
_localSubscription =
_localConversationCubit!.stream.listen(_updateLocalConversationState);
}
// Open remote converation key
Future<void> _setRemoteConversation(Future<DHTRecord> Function() open) async {
assert(_remoteConversationCubit == null,
'shoud not set remote conversation twice');
_remoteConversationCubit = DefaultDHTRecordCubit(
open: open, decodeState: proto.Conversation.fromBuffer);
_remoteSubscription =
_remoteConversationCubit!.stream.listen(_updateRemoteConversationState);
}
Future<bool> delete() async {
final pool = DHTRecordPool.instance;
await _initWait();
final localConversationCubit = _localConversationCubit;
final remoteConversationCubit = _remoteConversationCubit;
final deleteSet = DelayedWaitSet<void>();
if (localConversationCubit != null) {
final data = localConversationCubit.state.asData;
if (data == null) {
log.warning('could not delete local conversation');
return false;
}
deleteSet.add(() async {
_localConversationCubit = null;
await localConversationCubit.close();
final conversation = data.value;
final messagesKey = conversation.messages.toVeilid();
await pool.deleteRecord(messagesKey);
await pool.deleteRecord(_localConversationRecordKey!);
_localConversationRecordKey = null;
});
}
if (remoteConversationCubit != null) {
final data = remoteConversationCubit.state.asData;
if (data == null) {
log.warning('could not delete remote conversation');
return false;
}
deleteSet.add(() async {
_remoteConversationCubit = null;
await remoteConversationCubit.close();
final conversation = data.value;
final messagesKey = conversation.messages.toVeilid();
await pool.deleteRecord(messagesKey);
await pool.deleteRecord(_remoteConversationRecordKey!);
});
}
// Commit the delete futures
await deleteSet();
return true;
}
// Initialize a local conversation
// If we were the initiator of the conversation there may be an
// incomplete 'existingConversationRecord' that we need to fill
// in now that we have the remote identity key
// The ConversationCubit must not already have a local conversation
// The callback allows for more initialization to occur and for
// cleanup to delete records upon failure of the callback
Future<T> initLocalConversation<T>( Future<T> initLocalConversation<T>(
{required proto.Profile profile, {required proto.Profile profile,
required FutureOr<T> Function(DHTRecord) callback, required FutureOr<T> Function(DHTRecord) callback,
@ -280,6 +177,167 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
return out; return out;
} }
/// Delete the conversation keys associated with this conversation
Future<bool> delete() async {
final pool = DHTRecordPool.instance;
await _initWait();
final localConversationCubit = _localConversationCubit;
final remoteConversationCubit = _remoteConversationCubit;
final deleteSet = DelayedWaitSet<void>();
if (localConversationCubit != null) {
final data = localConversationCubit.state.asData;
if (data == null) {
log.warning('could not delete local conversation');
return false;
}
deleteSet.add(() async {
_localConversationCubit = null;
await localConversationCubit.close();
final conversation = data.value;
final messagesKey = conversation.messages.toVeilid();
await pool.deleteRecord(messagesKey);
await pool.deleteRecord(_localConversationRecordKey!);
_localConversationRecordKey = null;
});
}
if (remoteConversationCubit != null) {
final data = remoteConversationCubit.state.asData;
if (data == null) {
log.warning('could not delete remote conversation');
return false;
}
deleteSet.add(() async {
_remoteConversationCubit = null;
await remoteConversationCubit.close();
final conversation = data.value;
final messagesKey = conversation.messages.toVeilid();
await pool.deleteRecord(messagesKey);
await pool.deleteRecord(_remoteConversationRecordKey!);
});
}
// Commit the delete futures
await deleteSet();
return true;
}
/// Force refresh of conversation keys
Future<void> refresh() async {
await _initWait();
final lcc = _localConversationCubit;
final rcc = _remoteConversationCubit;
if (lcc != null) {
await lcc.refreshDefault();
}
if (rcc != null) {
await rcc.refreshDefault();
}
}
/// Watch for account record changes and update the conversation
void watchAccountChanges(Stream<AsyncValue<proto.Account>> accountStream,
AsyncValue<proto.Account> currentState) {
assert(_accountSubscription == null, 'only watch account once');
_accountSubscription = accountStream.listen(_updateAccountChange);
_updateAccountChange(currentState);
}
////////////////////////////////////////////////////////////////////////////
// Private Implementation
void _updateAccountChange(AsyncValue<proto.Account> avaccount) {
final account = avaccount.asData?.value;
if (account == null) {
return;
}
final cubit = _localConversationCubit;
if (cubit == null) {
return;
}
serialFuture((this, _sfUpdateAccountChange), () async {
await cubit.record.eventualUpdateProtobuf(proto.Conversation.fromBuffer,
(old) async {
if (old == null || old.profile == account.profile) {
return null;
}
return old.deepCopy()..profile = account.profile;
});
});
}
void _updateLocalConversationState(AsyncValue<proto.Conversation> avconv) {
final newState = avconv.when(
data: (conv) {
_incrementalState = ConversationState(
localConversation: conv,
remoteConversation: _incrementalState.remoteConversation);
// return loading still if state isn't complete
if ((_localConversationRecordKey != null &&
_incrementalState.localConversation == null) ||
(_remoteConversationRecordKey != null &&
_incrementalState.remoteConversation == null)) {
return const AsyncValue<ConversationState>.loading();
}
// state is complete, all required keys are open
return AsyncValue.data(_incrementalState);
},
loading: AsyncValue<ConversationState>.loading,
error: AsyncValue<ConversationState>.error,
);
emit(newState);
}
void _updateRemoteConversationState(AsyncValue<proto.Conversation> avconv) {
final newState = avconv.when(
data: (conv) {
_incrementalState = ConversationState(
localConversation: _incrementalState.localConversation,
remoteConversation: conv);
// return loading still if state isn't complete
if ((_localConversationRecordKey != null &&
_incrementalState.localConversation == null) ||
(_remoteConversationRecordKey != null &&
_incrementalState.remoteConversation == null)) {
return const AsyncValue<ConversationState>.loading();
}
// state is complete, all required keys are open
return AsyncValue.data(_incrementalState);
},
loading: AsyncValue<ConversationState>.loading,
error: AsyncValue<ConversationState>.error,
);
emit(newState);
}
// Open local converation key
Future<void> _setLocalConversation(Future<DHTRecord> Function() open) async {
assert(_localConversationCubit == null,
'shoud not set local conversation twice');
_localConversationCubit = DefaultDHTRecordCubit(
open: open, decodeState: proto.Conversation.fromBuffer);
_localSubscription =
_localConversationCubit!.stream.listen(_updateLocalConversationState);
}
// Open remote converation key
Future<void> _setRemoteConversation(Future<DHTRecord> Function() open) async {
assert(_remoteConversationCubit == null,
'shoud not set remote conversation twice');
_remoteConversationCubit = DefaultDHTRecordCubit(
open: open, decodeState: proto.Conversation.fromBuffer);
_remoteSubscription =
_remoteConversationCubit!.stream.listen(_updateRemoteConversationState);
}
// Initialize local messages // Initialize local messages
Future<T> _initLocalMessages<T>({ Future<T> _initLocalMessages<T>({
required UnlockedAccountInfo activeAccountInfo, required UnlockedAccountInfo activeAccountInfo,
@ -299,34 +357,6 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
.deleteScope((messages) async => await callback(messages)); .deleteScope((messages) async => await callback(messages));
} }
// Force refresh of conversation keys
Future<void> refresh() async {
await _initWait();
final lcc = _localConversationCubit;
final rcc = _remoteConversationCubit;
if (lcc != null) {
await lcc.refreshDefault();
}
if (rcc != null) {
await rcc.refreshDefault();
}
}
Future<proto.Conversation?> writeLocalConversation({
required proto.Conversation conversation,
}) async {
final update = await _localConversationCubit!.record
.tryWriteProtobuf(proto.Conversation.fromBuffer, conversation);
if (update != null) {
_updateLocalConversationState(AsyncValue.data(conversation));
}
return update;
}
Future<VeilidCrypto> _cachedConversationCrypto() async { Future<VeilidCrypto> _cachedConversationCrypto() async {
var conversationCrypto = _conversationCrypto; var conversationCrypto = _conversationCrypto;
if (conversationCrypto != null) { if (conversationCrypto != null) {
@ -339,6 +369,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
return conversationCrypto; return conversationCrypto;
} }
////////////////////////////////////////////////////////////////////////////
// Fields
final UnlockedAccountInfo _unlockedAccountInfo; final UnlockedAccountInfo _unlockedAccountInfo;
final TypedKey _remoteIdentityPublicKey; final TypedKey _remoteIdentityPublicKey;
TypedKey? _localConversationRecordKey; TypedKey? _localConversationRecordKey;
@ -347,9 +380,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit; DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit;
StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription; StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription;
StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription; StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription;
StreamSubscription<AsyncValue<proto.Account>>? _accountSubscription;
ConversationState _incrementalState = const ConversationState( ConversationState _incrementalState = const ConversationState(
localConversation: null, remoteConversation: null); localConversation: null, remoteConversation: null);
//
VeilidCrypto? _conversationCrypto; VeilidCrypto? _conversationCrypto;
final WaitSet<void> _initWait = WaitSet(); final WaitSet<void> _initWait = WaitSet();
} }

View File

@ -8,6 +8,7 @@ import 'package:go_router/go_router.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart'; import '../../../account_manager/account_manager.dart';
import '../../../proto/proto.dart' as proto;
import '../../../theme/theme.dart'; import '../../../theme/theme.dart';
import '../../../tools/tools.dart'; import '../../../tools/tools.dart';
import '../../../veilid_processor/veilid_processor.dart'; import '../../../veilid_processor/veilid_processor.dart';
@ -37,8 +38,12 @@ class _DrawerMenuState extends State<DrawerMenu> {
}); });
} }
void _doEditClick(TypedKey superIdentityRecordKey) { void _doEditClick(
// TypedKey superIdentityRecordKey, proto.Profile existingProfile) {
singleFuture(this, () async {
await GoRouterHelper(context).push('/edit_account',
extra: [superIdentityRecordKey, existingProfile]);
});
} }
Widget _wrapInBox({required Widget child, required Color color}) => Widget _wrapInBox({required Widget child, required Color color}) =>
@ -127,7 +132,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
_doSwitchClick(superIdentityRecordKey); _doSwitchClick(superIdentityRecordKey);
}, },
footerCallback: () { footerCallback: () {
_doEditClick(superIdentityRecordKey); _doEditClick(superIdentityRecordKey, value.profile);
}), }),
loading: () => _wrapInBox( loading: () => _wrapInBox(
child: buildProgressIndicator(), child: buildProgressIndicator(),

View File

@ -102,6 +102,9 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// XXX: Should probably eliminate this in favor
// of streaming changes into other cubits. Too much rebuilding!
// should not need to 'watch' all these cubits
final account = context.watch<AccountRecordCubit>().state.asData?.value; final account = context.watch<AccountRecordCubit>().state.asData?.value;
if (account == null) { if (account == null) {
return waitingPage(); return waitingPage();
@ -121,28 +124,29 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
create: (context) => WaitingInvitationsBlocMapCubit( create: (context) => WaitingInvitationsBlocMapCubit(
unlockedAccountInfo: widget.unlockedAccountInfo, unlockedAccountInfo: widget.unlockedAccountInfo,
account: account) account: account)
..follow(context.watch<ContactInvitationListCubit>())), ..follow(context.read<ContactInvitationListCubit>())),
// Chat Cubits // Chat Cubits
BlocProvider( BlocProvider(
create: (context) => ActiveChatCubit(null, create: (context) => ActiveChatCubit(null,
routerCubit: context.watch<RouterCubit>())), routerCubit: context.read<RouterCubit>())),
BlocProvider( BlocProvider(
create: (context) => ChatListCubit( create: (context) => ChatListCubit(
unlockedAccountInfo: widget.unlockedAccountInfo, unlockedAccountInfo: widget.unlockedAccountInfo,
activeChatCubit: context.watch<ActiveChatCubit>(), activeChatCubit: context.read<ActiveChatCubit>(),
account: account)), account: account)),
// Conversation Cubits // Conversation Cubits
BlocProvider( BlocProvider(
create: (context) => ActiveConversationsBlocMapCubit( create: (context) => ActiveConversationsBlocMapCubit(
unlockedAccountInfo: widget.unlockedAccountInfo, unlockedAccountInfo: widget.unlockedAccountInfo,
contactListCubit: context.watch<ContactListCubit>()) contactListCubit: context.read<ContactListCubit>(),
..follow(context.watch<ChatListCubit>())), accountRecordCubit: context.read<AccountRecordCubit>())
..follow(context.read<ChatListCubit>())),
BlocProvider( BlocProvider(
create: (context) => ActiveSingleContactChatBlocMapCubit( create: (context) => ActiveSingleContactChatBlocMapCubit(
unlockedAccountInfo: widget.unlockedAccountInfo, unlockedAccountInfo: widget.unlockedAccountInfo,
contactListCubit: context.watch<ContactListCubit>(), contactListCubit: context.read<ContactListCubit>(),
chatListCubit: context.watch<ChatListCubit>()) chatListCubit: context.read<ChatListCubit>())
..follow(context.watch<ActiveConversationsBlocMapCubit>())), ..follow(context.read<ActiveConversationsBlocMapCubit>())),
], ],
child: MultiBlocListener(listeners: [ child: MultiBlocListener(listeners: [
BlocListener<WaitingInvitationsBlocMapCubit, BlocListener<WaitingInvitationsBlocMapCubit,

View File

@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -13,12 +14,12 @@ import 'home_account_missing.dart';
import 'home_no_active.dart'; import 'home_no_active.dart';
class HomeShell extends StatefulWidget { class HomeShell extends StatefulWidget {
const HomeShell({required this.accountReadyBuilder, super.key}); const HomeShell({required this.child, super.key});
@override @override
HomeShellState createState() => HomeShellState(); HomeShellState createState() => HomeShellState();
final Builder accountReadyBuilder; final Widget child;
} }
class HomeShellState extends State<HomeShell> { class HomeShellState extends State<HomeShell> {
@ -58,13 +59,17 @@ class HomeShellState extends State<HomeShell> {
case AccountInfoStatus.accountLocked: case AccountInfoStatus.accountLocked:
return const HomeAccountLocked(); return const HomeAccountLocked();
case AccountInfoStatus.accountReady: case AccountInfoStatus.accountReady:
return MultiProvider(providers: [ return MultiBlocProvider(
Provider<UnlockedAccountInfo>.value( providers: [
value: accountInfo.unlockedAccountInfo!, BlocProvider<AccountRecordCubit>.value(value: activeCubit),
), ],
Provider<AccountRecordCubit>.value(value: activeCubit), child: MultiProvider(providers: [
Provider<ZoomDrawerController>.value(value: _zoomDrawerController), Provider<UnlockedAccountInfo>.value(
], child: widget.accountReadyBuilder); value: accountInfo.unlockedAccountInfo!,
),
Provider<ZoomDrawerController>.value(
value: _zoomDrawerController),
], child: widget.child));
} }
} }

View File

@ -1396,6 +1396,7 @@ class Profile extends $pb.GeneratedMessage {
..aOS(4, _omitFieldNames ? '' : 'status') ..aOS(4, _omitFieldNames ? '' : 'status')
..e<Availability>(5, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values) ..e<Availability>(5, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values)
..aOM<DataReference>(6, _omitFieldNames ? '' : 'avatar', subBuilder: DataReference.create) ..aOM<DataReference>(6, _omitFieldNames ? '' : 'avatar', subBuilder: DataReference.create)
..a<$fixnum.Int64>(7, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -1475,6 +1476,15 @@ class Profile extends $pb.GeneratedMessage {
void clearAvatar() => clearField(6); void clearAvatar() => clearField(6);
@$pb.TagNumber(6) @$pb.TagNumber(6)
DataReference ensureAvatar() => $_ensure(5); DataReference ensureAvatar() => $_ensure(5);
@$pb.TagNumber(7)
$fixnum.Int64 get timestamp => $_getI64(6);
@$pb.TagNumber(7)
set timestamp($fixnum.Int64 v) { $_setInt64(6, v); }
@$pb.TagNumber(7)
$core.bool hasTimestamp() => $_has(6);
@$pb.TagNumber(7)
void clearTimestamp() => clearField(7);
} }
class Account extends $pb.GeneratedMessage { class Account extends $pb.GeneratedMessage {

View File

@ -411,6 +411,7 @@ const Profile$json = {
{'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'}, {'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'},
{'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'}, {'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'},
{'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '9': 0, '10': 'avatar', '17': true}, {'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '9': 0, '10': 'avatar', '17': true},
{'1': 'timestamp', '3': 7, '4': 1, '5': 4, '10': 'timestamp'},
], ],
'8': [ '8': [
{'1': '_avatar'}, {'1': '_avatar'},
@ -422,8 +423,8 @@ final $typed_data.Uint8List profileDescriptor = $convert.base64Decode(
'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW' 'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW'
'5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp' '5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp'
'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ej' 'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ej'
'YKBmF2YXRhchgGIAEoCzIZLnZlaWxpZGNoYXQuRGF0YVJlZmVyZW5jZUgAUgZhdmF0YXKIAQFC' 'YKBmF2YXRhchgGIAEoCzIZLnZlaWxpZGNoYXQuRGF0YVJlZmVyZW5jZUgAUgZhdmF0YXKIAQES'
'CQoHX2F2YXRhcg=='); 'HAoJdGltZXN0YW1wGAcgASgEUgl0aW1lc3RhbXBCCQoHX2F2YXRhcg==');
@$core.Deprecated('Use accountDescriptor instead') @$core.Deprecated('Use accountDescriptor instead')
const Account$json = { const Account$json = {

View File

@ -311,6 +311,8 @@ message Profile {
Availability availability = 5; Availability availability = 5;
// Avatar // Avatar
optional DataReference avatar = 6; optional DataReference avatar = 6;
// Timestamp of last change
uint64 timestamp = 7;
} }
// A record of an individual account // A record of an individual account

View File

@ -11,6 +11,7 @@ import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart'; import '../../../account_manager/account_manager.dart';
import '../../layout/layout.dart'; import '../../layout/layout.dart';
import '../../proto/proto.dart' as proto;
import '../../settings/settings.dart'; import '../../settings/settings.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../../veilid_processor/views/developer.dart'; import '../../veilid_processor/views/developer.dart';
@ -20,6 +21,7 @@ part 'router_cubit.g.dart';
final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'rootNavKey'); final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'rootNavKey');
final _homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'homeNavKey'); final _homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'homeNavKey');
final _activeNavKey = GlobalKey<NavigatorState>(debugLabel: 'activeNavKey');
@freezed @freezed
class RouterState with _$RouterState { class RouterState with _$RouterState {
@ -65,21 +67,34 @@ class RouterCubit extends Cubit<RouterState> {
List<RouteBase> get routes => [ List<RouteBase> get routes => [
ShellRoute( ShellRoute(
navigatorKey: _homeNavKey, navigatorKey: _homeNavKey,
builder: (context, state, child) => HomeShell( builder: (context, state, child) => HomeShell(child: child),
accountReadyBuilder: Builder(
builder: (context) =>
HomeAccountReadyShell(context: context, child: child))),
routes: [ routes: [
GoRoute( ShellRoute(
path: '/', navigatorKey: _activeNavKey,
builder: (context, state) => const HomeAccountReadyMain(), builder: (context, state, child) =>
), HomeAccountReadyShell(context: context, child: child),
GoRoute( routes: [
path: '/chat', GoRoute(
builder: (context, state) => const HomeAccountReadyChat(), path: '/',
), builder: (context, state) => const HomeAccountReadyMain(),
),
GoRoute(
path: '/chat',
builder: (context, state) => const HomeAccountReadyChat(),
),
]),
], ],
), ),
GoRoute(
path: '/edit_account',
builder: (context, state) {
final extra = state.extra! as List<Object?>;
return EditAccountPage(
superIdentityRecordKey: extra[0]! as TypedKey,
existingProfile: extra[1]! as proto.Profile,
);
},
),
GoRoute( GoRoute(
path: '/new_account', path: '/new_account',
builder: (context, state) => const NewAccountPage(), builder: (context, state) => const NewAccountPage(),

View File

@ -14,7 +14,7 @@ dependencies:
path: ../ path: ../
dev_dependencies: dev_dependencies:
async_tools: ^0.1.2 async_tools: ^0.1.3
integration_test: integration_test:
sdk: flutter sdk: flutter
lint_hard: ^4.0.0 lint_hard: ^4.0.0

View File

@ -308,7 +308,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
/// Each attempt to write the value calls an update function with the /// Each attempt to write the value calls an update function with the
/// old value to determine what new value should be attempted for that write. /// old value to determine what new value should be attempted for that write.
Future<void> eventualUpdateBytes( Future<void> eventualUpdateBytes(
Future<Uint8List> Function(Uint8List? oldValue) update, Future<Uint8List?> Function(Uint8List? oldValue) update,
{int subkey = -1, {int subkey = -1,
VeilidCrypto? crypto, VeilidCrypto? crypto,
KeyPair? writer, KeyPair? writer,
@ -323,7 +323,10 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
do { do {
// Update the data // Update the data
final updatedValue = await update(oldValue); final updatedValue = await update(oldValue);
if (updatedValue == null) {
// If null is returned from the update, stop trying to do the update
break;
}
// Try to write it back to the network // Try to write it back to the network
oldValue = await tryWriteBytes(updatedValue, oldValue = await tryWriteBytes(updatedValue,
subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum);
@ -389,7 +392,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
/// Like 'eventualUpdateBytes' but with JSON marshal/unmarshal of the value /// Like 'eventualUpdateBytes' but with JSON marshal/unmarshal of the value
Future<void> eventualUpdateJson<T>( Future<void> eventualUpdateJson<T>(
T Function(dynamic) fromJson, Future<T> Function(T?) update, T Function(dynamic) fromJson, Future<T?> Function(T?) update,
{int subkey = -1, {int subkey = -1,
VeilidCrypto? crypto, VeilidCrypto? crypto,
KeyPair? writer, KeyPair? writer,
@ -399,7 +402,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
/// Like 'eventualUpdateBytes' but with protobuf marshal/unmarshal of the value /// Like 'eventualUpdateBytes' but with protobuf marshal/unmarshal of the value
Future<void> eventualUpdateProtobuf<T extends GeneratedMessage>( Future<void> eventualUpdateProtobuf<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, Future<T> Function(T?) update, T Function(List<int>) fromBuffer, Future<T?> Function(T?) update,
{int subkey = -1, {int subkey = -1,
VeilidCrypto? crypto, VeilidCrypto? crypto,
KeyPair? writer, KeyPair? writer,

View File

@ -12,16 +12,19 @@ Uint8List jsonEncodeBytes(Object? object,
Uint8List.fromList( Uint8List.fromList(
utf8.encode(jsonEncode(object, toEncodable: toEncodable))); utf8.encode(jsonEncode(object, toEncodable: toEncodable)));
Future<Uint8List> jsonUpdateBytes<T>(T Function(dynamic) fromJson, Future<Uint8List?> jsonUpdateBytes<T>(T Function(dynamic) fromJson,
Uint8List? oldBytes, Future<T> Function(T?) update) async { Uint8List? oldBytes, Future<T?> Function(T?) update) async {
final oldObj = final oldObj =
oldBytes == null ? null : fromJson(jsonDecode(utf8.decode(oldBytes))); oldBytes == null ? null : fromJson(jsonDecode(utf8.decode(oldBytes)));
final newObj = await update(oldObj); final newObj = await update(oldObj);
if (newObj == null) {
return null;
}
return jsonEncodeBytes(newObj); return jsonEncodeBytes(newObj);
} }
Future<Uint8List> Function(Uint8List?) jsonUpdate<T>( Future<Uint8List?> Function(Uint8List?) jsonUpdate<T>(
T Function(dynamic) fromJson, Future<T> Function(T?) update) => T Function(dynamic) fromJson, Future<T?> Function(T?) update) =>
(oldBytes) => jsonUpdateBytes(fromJson, oldBytes, update); (oldBytes) => jsonUpdateBytes(fromJson, oldBytes, update);
T Function(Object?) genericFromJson<T>( T Function(Object?) genericFromJson<T>(

View File

@ -2,16 +2,19 @@ import 'dart:typed_data';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart';
Future<Uint8List> protobufUpdateBytes<T extends GeneratedMessage>( Future<Uint8List?> protobufUpdateBytes<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, T Function(List<int>) fromBuffer,
Uint8List? oldBytes, Uint8List? oldBytes,
Future<T> Function(T?) update) async { Future<T?> Function(T?) update) async {
final oldObj = oldBytes == null ? null : fromBuffer(oldBytes); final oldObj = oldBytes == null ? null : fromBuffer(oldBytes);
final newObj = await update(oldObj); final newObj = await update(oldObj);
if (newObj == null) {
return null;
}
return Uint8List.fromList(newObj.writeToBuffer()); return Uint8List.fromList(newObj.writeToBuffer());
} }
Future<Uint8List> Function(Uint8List?) Future<Uint8List?> Function(Uint8List?)
protobufUpdate<T extends GeneratedMessage>( protobufUpdate<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, Future<T> Function(T?) update) => T Function(List<int>) fromBuffer, Future<T?> Function(T?) update) =>
(oldBytes) => protobufUpdateBytes(fromBuffer, oldBytes, update); (oldBytes) => protobufUpdateBytes(fromBuffer, oldBytes, update);

View File

@ -36,11 +36,10 @@ packages:
async_tools: async_tools:
dependency: "direct main" dependency: "direct main"
description: description:
name: async_tools path: "../../../dart_async_tools"
sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684" relative: true
url: "https://pub.dev" source: path
source: hosted version: "0.1.3"
version: "0.1.2"
bloc: bloc:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -7,7 +7,7 @@ environment:
sdk: '>=3.2.0 <4.0.0' sdk: '>=3.2.0 <4.0.0'
dependencies: dependencies:
async_tools: ^0.1.2 async_tools: ^0.1.3
bloc: ^8.1.4 bloc: ^8.1.4
bloc_advanced_tools: ^0.1.3 bloc_advanced_tools: ^0.1.3
charcode: ^1.3.1 charcode: ^1.3.1
@ -25,10 +25,10 @@ dependencies:
path: ../../../veilid/veilid-flutter path: ../../../veilid/veilid-flutter
dependency_overrides: dependency_overrides:
# async_tools: async_tools:
# path: ../../../dart_async_tools path: ../../../dart_async_tools
bloc_advanced_tools: bloc_advanced_tools:
path: ../../../bloc_advanced_tools path: ../../../bloc_advanced_tools
dev_dependencies: dev_dependencies:
build_runner: ^2.4.10 build_runner: ^2.4.10

View File

@ -60,11 +60,10 @@ packages:
async_tools: async_tools:
dependency: "direct main" dependency: "direct main"
description: description:
name: async_tools path: "../dart_async_tools"
sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684" relative: true
url: "https://pub.dev" source: path
source: hosted version: "0.1.3"
version: "0.1.2"
awesome_extensions: awesome_extensions:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -11,7 +11,7 @@ dependencies:
animated_theme_switcher: ^2.0.10 animated_theme_switcher: ^2.0.10
ansicolor: ^2.0.2 ansicolor: ^2.0.2
archive: ^3.6.1 archive: ^3.6.1
async_tools: ^0.1.2 async_tools: ^0.1.3
awesome_extensions: ^2.0.16 awesome_extensions: ^2.0.16
badges: ^3.1.2 badges: ^3.1.2
basic_utils: ^5.7.0 basic_utils: ^5.7.0
@ -94,8 +94,8 @@ dependencies:
zxing2: ^0.2.3 zxing2: ^0.2.3
dependency_overrides: dependency_overrides:
# async_tools: async_tools:
# path: ../dart_async_tools path: ../dart_async_tools
bloc_advanced_tools: bloc_advanced_tools:
path: ../bloc_advanced_tools path: ../bloc_advanced_tools
# flutter_chat_ui: # flutter_chat_ui: