mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-08-13 08:25:29 -04:00
checkpoint
This commit is contained in:
parent
56d65442f4
commit
751022e743
26 changed files with 482 additions and 303 deletions
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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(() {
|
||||
|
|
|
@ -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? ??
|
||||
|
|
|
@ -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')),
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export 'edit_account_page.dart';
|
||||
export 'new_account_page.dart';
|
||||
export 'profile_widget.dart';
|
||||
export 'show_recovery_key_page.dart';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue