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

View file

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

View file

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

View file

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

View file

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