mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-12-15 16:29:10 -05:00
refactor and cleanup in prep for profile changing
This commit is contained in:
parent
87bb1657c7
commit
56d65442f4
49 changed files with 967 additions and 655 deletions
|
|
@ -3,13 +3,33 @@ import 'dart:async';
|
|||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../account_manager.dart';
|
||||
|
||||
typedef AccountRecordState = proto.Account;
|
||||
|
||||
class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
|
||||
AccountRecordCubit({
|
||||
required super.open,
|
||||
}) : super(decodeState: proto.Account.fromBuffer);
|
||||
AccountRecordCubit(
|
||||
{required AccountRepository accountRepository,
|
||||
required TypedKey superIdentityRecordKey})
|
||||
: super(
|
||||
decodeState: proto.Account.fromBuffer,
|
||||
open: () => _open(accountRepository, superIdentityRecordKey));
|
||||
|
||||
static Future<DHTRecord> _open(AccountRepository accountRepository,
|
||||
TypedKey superIdentityRecordKey) async {
|
||||
final localAccount =
|
||||
accountRepository.fetchLocalAccount(superIdentityRecordKey)!;
|
||||
final userLogin = accountRepository.fetchUserLogin(superIdentityRecordKey)!;
|
||||
|
||||
// Record not yet open, do it
|
||||
final pool = DHTRecordPool.instance;
|
||||
final record = await pool.openRecordOwned(
|
||||
userLogin.accountRecordInfo.accountRecord,
|
||||
debugName: 'AccountRecordCubit::_open::AccountRecord',
|
||||
parent: localAccount.superIdentity.currentInstance.recordKey);
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
|
|
|
|||
|
|
@ -15,11 +15,13 @@ class AccountRecordsBlocMapCubit extends BlocMapCubit<TypedKey,
|
|||
: _accountRepository = accountRepository;
|
||||
|
||||
// Add account record cubit
|
||||
Future<void> _addAccountRecordCubit({required UserLogin userLogin}) async =>
|
||||
Future<void> _addAccountRecordCubit(
|
||||
{required TypedKey superIdentityRecordKey}) async =>
|
||||
add(() => MapEntry(
|
||||
userLogin.superIdentityRecordKey,
|
||||
superIdentityRecordKey,
|
||||
AccountRecordCubit(
|
||||
open: () => _accountRepository.openAccountRecord(userLogin))));
|
||||
accountRepository: _accountRepository,
|
||||
superIdentityRecordKey: superIdentityRecordKey)));
|
||||
|
||||
/// StateFollower /////////////////////////
|
||||
|
||||
|
|
@ -28,7 +30,8 @@ class AccountRecordsBlocMapCubit extends BlocMapCubit<TypedKey,
|
|||
|
||||
@override
|
||||
Future<void> updateState(TypedKey key, UserLogin value) async {
|
||||
await _addAccountRecordCubit(userLogin: value);
|
||||
await _addAccountRecordCubit(
|
||||
superIdentityRecordKey: value.superIdentityRecordKey);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../models/models.dart';
|
||||
import '../repository/account_repository.dart';
|
||||
|
||||
class ActiveLocalAccountCubit extends Cubit<TypedKey?> {
|
||||
ActiveLocalAccountCubit(AccountRepository accountRepository)
|
||||
class ActiveAccountInfoCubit extends Cubit<AccountInfo> {
|
||||
ActiveAccountInfoCubit(AccountRepository accountRepository)
|
||||
: _accountRepository = accountRepository,
|
||||
super(accountRepository.getActiveLocalAccount()) {
|
||||
super(accountRepository
|
||||
.getAccountInfo(accountRepository.getActiveLocalAccount())) {
|
||||
// Subscribe to streams
|
||||
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
|
||||
switch (change) {
|
||||
case AccountRepositoryChange.activeLocalAccount:
|
||||
emit(_accountRepository.getActiveLocalAccount());
|
||||
break;
|
||||
// Ignore these
|
||||
case AccountRepositoryChange.localAccounts:
|
||||
case AccountRepositoryChange.userLogins:
|
||||
emit(accountRepository
|
||||
.getAccountInfo(accountRepository.getActiveLocalAccount()));
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
export 'account_record_cubit.dart';
|
||||
export 'account_records_bloc_map_cubit.dart';
|
||||
export 'active_local_account_cubit.dart';
|
||||
export 'active_account_info_cubit.dart';
|
||||
export 'local_accounts_cubit.dart';
|
||||
export 'user_logins_cubit.dart';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'active_account_info.dart';
|
||||
import 'unlocked_account_info.dart';
|
||||
|
||||
enum AccountInfoStatus {
|
||||
noAccount,
|
||||
|
|
@ -14,10 +14,10 @@ class AccountInfo {
|
|||
const AccountInfo({
|
||||
required this.status,
|
||||
required this.active,
|
||||
required this.activeAccountInfo,
|
||||
required this.unlockedAccountInfo,
|
||||
});
|
||||
|
||||
final AccountInfoStatus status;
|
||||
final bool active;
|
||||
final ActiveAccountInfo? activeAccountInfo;
|
||||
final UnlockedAccountInfo? unlockedAccountInfo;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export 'account_info.dart';
|
||||
export 'active_account_info.dart';
|
||||
export 'unlocked_account_info.dart';
|
||||
export 'encryption_key_type.dart';
|
||||
export 'local_account/local_account.dart';
|
||||
export 'new_profile_spec.dart';
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import 'local_account/local_account.dart';
|
|||
import 'user_login/user_login.dart';
|
||||
|
||||
@immutable
|
||||
class ActiveAccountInfo {
|
||||
const ActiveAccountInfo({
|
||||
class UnlockedAccountInfo {
|
||||
const UnlockedAccountInfo({
|
||||
required this.localAccount,
|
||||
required this.userLogin,
|
||||
});
|
||||
|
|
@ -45,19 +45,6 @@ class AccountRepository {
|
|||
valueToJson: (val) => val?.toJson(),
|
||||
makeInitialValue: () => null);
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// Fields
|
||||
|
||||
final TableDBValue<IList<LocalAccount>> _localAccounts;
|
||||
final TableDBValue<IList<UserLogin>> _userLogins;
|
||||
final TableDBValue<TypedKey?> _activeLocalAccount;
|
||||
final StreamController<AccountRepositoryChange> _streamController;
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// Singleton initialization
|
||||
|
||||
static AccountRepository instance = AccountRepository._();
|
||||
|
||||
Future<void> init() async {
|
||||
await _localAccounts.get();
|
||||
await _userLogins.get();
|
||||
|
|
@ -71,12 +58,10 @@ class AccountRepository {
|
|||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// Streams
|
||||
|
||||
/// Public Interface
|
||||
///
|
||||
Stream<AccountRepositoryChange> get stream => _streamController.stream;
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// Selectors
|
||||
IList<LocalAccount> getLocalAccounts() => _localAccounts.value;
|
||||
TypedKey? getActiveLocalAccount() => _activeLocalAccount.value;
|
||||
IList<UserLogin> getUserLogins() => _userLogins.value;
|
||||
|
|
@ -116,7 +101,7 @@ class AccountRepository {
|
|||
return const AccountInfo(
|
||||
status: AccountInfoStatus.noAccount,
|
||||
active: false,
|
||||
activeAccountInfo: null);
|
||||
unlockedAccountInfo: null);
|
||||
}
|
||||
superIdentityRecordKey = activeLocalAccount;
|
||||
}
|
||||
|
|
@ -129,7 +114,7 @@ class AccountRepository {
|
|||
return AccountInfo(
|
||||
status: AccountInfoStatus.noAccount,
|
||||
active: active,
|
||||
activeAccountInfo: null);
|
||||
unlockedAccountInfo: null);
|
||||
}
|
||||
|
||||
// See if we've logged into this account or if it is locked
|
||||
|
|
@ -139,21 +124,18 @@ class AccountRepository {
|
|||
return AccountInfo(
|
||||
status: AccountInfoStatus.accountLocked,
|
||||
active: active,
|
||||
activeAccountInfo: null);
|
||||
unlockedAccountInfo: null);
|
||||
}
|
||||
|
||||
// Got account, decrypted and decoded
|
||||
return AccountInfo(
|
||||
status: AccountInfoStatus.accountReady,
|
||||
active: active,
|
||||
activeAccountInfo:
|
||||
ActiveAccountInfo(localAccount: localAccount, userLogin: userLogin),
|
||||
unlockedAccountInfo:
|
||||
UnlockedAccountInfo(localAccount: localAccount, userLogin: userLogin),
|
||||
);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// Mutators
|
||||
|
||||
/// Reorder accounts
|
||||
Future<void> reorderAccount(int oldIndex, int newIndex) async {
|
||||
final localAccounts = await _localAccounts.get();
|
||||
|
|
@ -168,15 +150,14 @@ class AccountRepository {
|
|||
/// Creates a new super identity, an identity instance, an account associated
|
||||
/// with the identity instance, stores the account in the identity key and
|
||||
/// then logs into that account with no password set at this time
|
||||
Future<SecretKey> createWithNewSuperIdentity(
|
||||
NewProfileSpec newProfileSpec) async {
|
||||
Future<SecretKey> createWithNewSuperIdentity(proto.Profile newProfile) async {
|
||||
log.debug('Creating super identity');
|
||||
final wsi = await WritableSuperIdentity.create();
|
||||
try {
|
||||
final localAccount = await _newLocalAccount(
|
||||
superIdentity: wsi.superIdentity,
|
||||
identitySecret: wsi.identitySecret,
|
||||
newProfileSpec: newProfileSpec);
|
||||
newProfile: newProfile);
|
||||
|
||||
// Log in the new account by default with no pin
|
||||
final ok = await login(
|
||||
|
|
@ -190,85 +171,18 @@ class AccountRepository {
|
|||
}
|
||||
}
|
||||
|
||||
/// Creates a new Account associated with the current instance of the identity
|
||||
/// Adds a logged-out LocalAccount to track its existence on this device
|
||||
Future<LocalAccount> _newLocalAccount(
|
||||
{required SuperIdentity superIdentity,
|
||||
required SecretKey identitySecret,
|
||||
required NewProfileSpec newProfileSpec,
|
||||
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
|
||||
String encryptionKey = ''}) async {
|
||||
log.debug('Creating new local account');
|
||||
Future<void> editAccountProfile(
|
||||
TypedKey superIdentityRecordKey, proto.Profile newProfile) async {
|
||||
log.debug('Editing profile for $superIdentityRecordKey');
|
||||
|
||||
final localAccounts = await _localAccounts.get();
|
||||
|
||||
// Add account with profile to DHT
|
||||
await superIdentity.currentInstance.addAccount(
|
||||
superRecordKey: superIdentity.recordKey,
|
||||
secretKey: identitySecret,
|
||||
accountKey: veilidChatAccountKey,
|
||||
createAccountCallback: (parent) async {
|
||||
// Make empty contact list
|
||||
log.debug('Creating contacts list');
|
||||
final contactList = await (await DHTShortArray.create(
|
||||
debugName: 'AccountRepository::_newLocalAccount::Contacts',
|
||||
parent: parent))
|
||||
.scope((r) async => r.recordPointer);
|
||||
|
||||
// Make empty contact invitation record list
|
||||
log.debug('Creating contact invitation records list');
|
||||
final contactInvitationRecords = await (await DHTShortArray.create(
|
||||
debugName:
|
||||
'AccountRepository::_newLocalAccount::ContactInvitations',
|
||||
parent: parent))
|
||||
.scope((r) async => r.recordPointer);
|
||||
|
||||
// Make empty chat record list
|
||||
log.debug('Creating chat records list');
|
||||
final chatRecords = await (await DHTShortArray.create(
|
||||
debugName: 'AccountRepository::_newLocalAccount::Chats',
|
||||
parent: parent))
|
||||
.scope((r) async => r.recordPointer);
|
||||
|
||||
// Make account object
|
||||
final account = proto.Account()
|
||||
..profile = (proto.Profile()
|
||||
..name = newProfileSpec.name
|
||||
..pronouns = newProfileSpec.pronouns)
|
||||
..contactList = contactList.toProto()
|
||||
..contactInvitationRecords = contactInvitationRecords.toProto()
|
||||
..chatList = chatRecords.toProto();
|
||||
return account.writeToBuffer();
|
||||
});
|
||||
|
||||
// Encrypt identitySecret with key
|
||||
final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes(
|
||||
secret: identitySecret,
|
||||
cryptoKind: superIdentity.currentInstance.recordKey.kind,
|
||||
encryptionKey: encryptionKey,
|
||||
);
|
||||
|
||||
// Create local account object
|
||||
// Does not contain the account key or its secret
|
||||
// as that is not to be persisted, and only pulled from the identity key
|
||||
// and optionally decrypted with the unlock password
|
||||
final localAccount = LocalAccount(
|
||||
superIdentity: superIdentity,
|
||||
identitySecretBytes: identitySecretBytes,
|
||||
encryptionKeyType: encryptionKeyType,
|
||||
biometricsEnabled: false,
|
||||
hiddenAccount: false,
|
||||
name: newProfileSpec.name,
|
||||
);
|
||||
|
||||
// Add local account object to internal store
|
||||
final newLocalAccounts = localAccounts.add(localAccount);
|
||||
final newLocalAccounts = localAccounts.replaceFirstWhere(
|
||||
(x) => x.superIdentity.recordKey == superIdentityRecordKey,
|
||||
(localAccount) => localAccount!.copyWith(name: newProfile.name));
|
||||
|
||||
await _localAccounts.set(newLocalAccounts);
|
||||
_streamController.add(AccountRepositoryChange.localAccounts);
|
||||
|
||||
// Return local account object
|
||||
return localAccount;
|
||||
}
|
||||
|
||||
/// Remove an account and wipe the messages for this account from this device
|
||||
|
|
@ -310,6 +224,88 @@ class AccountRepository {
|
|||
_streamController.add(AccountRepositoryChange.activeLocalAccount);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// Internal Implementation
|
||||
|
||||
/// Creates a new Account associated with the current instance of the identity
|
||||
/// Adds a logged-out LocalAccount to track its existence on this device
|
||||
Future<LocalAccount> _newLocalAccount(
|
||||
{required SuperIdentity superIdentity,
|
||||
required SecretKey identitySecret,
|
||||
required proto.Profile newProfile,
|
||||
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
|
||||
String encryptionKey = ''}) async {
|
||||
log.debug('Creating new local account');
|
||||
|
||||
final localAccounts = await _localAccounts.get();
|
||||
|
||||
// Add account with profile to DHT
|
||||
await superIdentity.currentInstance.addAccount(
|
||||
superRecordKey: superIdentity.recordKey,
|
||||
secretKey: identitySecret,
|
||||
accountKey: veilidChatAccountKey,
|
||||
createAccountCallback: (parent) async {
|
||||
// Make empty contact list
|
||||
log.debug('Creating contacts list');
|
||||
final contactList = await (await DHTShortArray.create(
|
||||
debugName: 'AccountRepository::_newLocalAccount::Contacts',
|
||||
parent: parent))
|
||||
.scope((r) async => r.recordPointer);
|
||||
|
||||
// Make empty contact invitation record list
|
||||
log.debug('Creating contact invitation records list');
|
||||
final contactInvitationRecords = await (await DHTShortArray.create(
|
||||
debugName:
|
||||
'AccountRepository::_newLocalAccount::ContactInvitations',
|
||||
parent: parent))
|
||||
.scope((r) async => r.recordPointer);
|
||||
|
||||
// Make empty chat record list
|
||||
log.debug('Creating chat records list');
|
||||
final chatRecords = await (await DHTShortArray.create(
|
||||
debugName: 'AccountRepository::_newLocalAccount::Chats',
|
||||
parent: parent))
|
||||
.scope((r) async => r.recordPointer);
|
||||
|
||||
// Make account object
|
||||
final account = proto.Account()
|
||||
..profile = newProfile
|
||||
..contactList = contactList.toProto()
|
||||
..contactInvitationRecords = contactInvitationRecords.toProto()
|
||||
..chatList = chatRecords.toProto();
|
||||
return account.writeToBuffer();
|
||||
});
|
||||
|
||||
// Encrypt identitySecret with key
|
||||
final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes(
|
||||
secret: identitySecret,
|
||||
cryptoKind: superIdentity.currentInstance.recordKey.kind,
|
||||
encryptionKey: encryptionKey,
|
||||
);
|
||||
|
||||
// Create local account object
|
||||
// Does not contain the account key or its secret
|
||||
// as that is not to be persisted, and only pulled from the identity key
|
||||
// and optionally decrypted with the unlock password
|
||||
final localAccount = LocalAccount(
|
||||
superIdentity: superIdentity,
|
||||
identitySecretBytes: identitySecretBytes,
|
||||
encryptionKeyType: encryptionKeyType,
|
||||
biometricsEnabled: false,
|
||||
hiddenAccount: false,
|
||||
name: newProfile.name,
|
||||
);
|
||||
|
||||
// Add local account object to internal store
|
||||
final newLocalAccounts = localAccounts.add(localAccount);
|
||||
|
||||
await _localAccounts.set(newLocalAccounts);
|
||||
_streamController.add(AccountRepositoryChange.localAccounts);
|
||||
|
||||
// Return local account object
|
||||
return localAccount;
|
||||
}
|
||||
|
||||
Future<bool> _decryptedLogin(
|
||||
SuperIdentity superIdentity, SecretKey identitySecret) async {
|
||||
// Verify identity secret works and return the valid cryptosystem
|
||||
|
|
@ -402,16 +398,13 @@ class AccountRepository {
|
|||
_streamController.add(AccountRepositoryChange.userLogins);
|
||||
}
|
||||
|
||||
Future<DHTRecord> openAccountRecord(UserLogin userLogin) async {
|
||||
final localAccount = fetchLocalAccount(userLogin.superIdentityRecordKey)!;
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// Fields
|
||||
|
||||
// Record not yet open, do it
|
||||
final pool = DHTRecordPool.instance;
|
||||
final record = await pool.openRecordOwned(
|
||||
userLogin.accountRecordInfo.accountRecord,
|
||||
debugName: 'AccountRepository::openAccountRecord::AccountRecord',
|
||||
parent: localAccount.superIdentity.currentInstance.recordKey);
|
||||
static AccountRepository instance = AccountRepository._();
|
||||
|
||||
return record;
|
||||
}
|
||||
final TableDBValue<IList<LocalAccount>> _localAccounts;
|
||||
final TableDBValue<IList<UserLogin>> _userLogins;
|
||||
final TableDBValue<TypedKey?> _activeLocalAccount;
|
||||
final StreamController<AccountRepositoryChange> _streamController;
|
||||
}
|
||||
|
|
|
|||
163
lib/account_manager/views/edit_account_page.dart
Normal file
163
lib/account_manager/views/edit_account_page.dart
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
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: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';
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_processor/veilid_processor.dart';
|
||||
import '../account_manager.dart';
|
||||
import 'profile_edit_form.dart';
|
||||
|
||||
class EditAccountPage extends StatefulWidget {
|
||||
const EditAccountPage(
|
||||
{required this.superIdentityRecordKey,
|
||||
required this.existingProfile,
|
||||
super.key});
|
||||
|
||||
@override
|
||||
State createState() => _EditAccountPageState();
|
||||
|
||||
final TypedKey superIdentityRecordKey;
|
||||
final proto.Profile existingProfile;
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<TypedKey>(
|
||||
'superIdentityRecordKey', superIdentityRecordKey))
|
||||
..add(DiagnosticsProperty<proto.Profile>(
|
||||
'existingProfile', existingProfile));
|
||||
}
|
||||
}
|
||||
|
||||
class _EditAccountPageState extends State<EditAccountPage> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
bool _isInAsyncCall = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await changeWindowSetup(
|
||||
TitleBarStyle.normal, OrientationCapability.portraitOnly);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _editAccountForm(BuildContext context,
|
||||
{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);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayModalHUD = _isInAsyncCall;
|
||||
final accountRecordCubit = context.read<AccountRecordCubit>();
|
||||
final activeConversationsBlocMapCubit =
|
||||
context.read<ActiveConversationsBlocMapCubit>();
|
||||
final contactListCubit = context.read<ContactListCubit>();
|
||||
|
||||
return Scaffold(
|
||||
// resizeToAvoidBottomInset: false,
|
||||
appBar: DefaultAppBar(
|
||||
title: Text(translate('edit_account_page.titlebar')),
|
||||
actions: [
|
||||
const SignalStrengthMeterWidget(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: translate('menu.settings_tooltip'),
|
||||
onPressed: () async {
|
||||
await GoRouterHelper(context).push('/settings');
|
||||
})
|
||||
]),
|
||||
body: _editAccountForm(
|
||||
context,
|
||||
onSubmit: (formKey) async {
|
||||
// dismiss the keyboard by unfocusing the textfield
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
try {
|
||||
final name = _formKey.currentState!
|
||||
.fields[EditProfileForm.formFieldName]!.value as String;
|
||||
final pronouns = _formKey
|
||||
.currentState!
|
||||
.fields[EditProfileForm.formFieldPronouns]!
|
||||
.value as String? ??
|
||||
'';
|
||||
final newProfile = widget.existingProfile.deepCopy()
|
||||
..name = name
|
||||
..pronouns = pronouns;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for updates
|
||||
await updates.wait;
|
||||
|
||||
// XXX: how to do this for non-chat contacts?
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isInAsyncCall = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (context.mounted) {
|
||||
await showErrorModal(context,
|
||||
translate('edit_account_page.error'), 'Exception: $e');
|
||||
}
|
||||
}
|
||||
},
|
||||
).paddingSymmetric(horizontal: 24, vertical: 8),
|
||||
).withModalHUD(context, displayModalHUD);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +1,28 @@
|
|||
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 '../../layout/default_app_bar.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_processor/veilid_processor.dart';
|
||||
import '../account_manager.dart';
|
||||
import 'profile_edit_form.dart';
|
||||
|
||||
class NewAccountPage extends StatefulWidget {
|
||||
const NewAccountPage({super.key});
|
||||
|
||||
@override
|
||||
NewAccountPageState createState() => NewAccountPageState();
|
||||
State createState() => _NewAccountPageState();
|
||||
}
|
||||
|
||||
class NewAccountPageState extends State<NewAccountPage> {
|
||||
class _NewAccountPageState extends State<NewAccountPage> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
late bool isInAsyncCall = false;
|
||||
static const String formFieldName = 'name';
|
||||
static const String formFieldPronouns = 'pronouns';
|
||||
bool _isInAsyncCall = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -47,70 +45,17 @@ class NewAccountPageState extends State<NewAccountPage> {
|
|||
false;
|
||||
final canSubmit = networkReady;
|
||||
|
||||
return FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
Text(translate('new_account_page.header'))
|
||||
.textStyle(context.headlineSmall)
|
||||
.paddingSymmetric(vertical: 16),
|
||||
FormBuilderTextField(
|
||||
autofocus: true,
|
||||
name: formFieldName,
|
||||
decoration:
|
||||
InputDecoration(labelText: translate('account.form_name')),
|
||||
maxLength: 64,
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(),
|
||||
]),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: formFieldPronouns,
|
||||
maxLength: 64,
|
||||
decoration:
|
||||
InputDecoration(labelText: translate('account.form_pronouns')),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
Row(children: [
|
||||
const Spacer(),
|
||||
Text(translate('new_account_page.instructions'))
|
||||
.toCenter()
|
||||
.flexible(flex: 6),
|
||||
const Spacer(),
|
||||
]).paddingSymmetric(vertical: 4),
|
||||
ElevatedButton(
|
||||
onPressed: !canSubmit
|
||||
? null
|
||||
: () async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
setState(() {
|
||||
isInAsyncCall = true;
|
||||
});
|
||||
try {
|
||||
await onSubmit(_formKey);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isInAsyncCall = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(translate(!networkReady
|
||||
? 'button.waiting_for_network'
|
||||
: 'new_account_page.create')),
|
||||
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
|
||||
],
|
||||
),
|
||||
);
|
||||
return EditProfileForm(
|
||||
header: translate('new_account_page.header'),
|
||||
instructions: translate('new_account_page.instructions'),
|
||||
submitText: translate('new_account_page.create'),
|
||||
submitDisabledText: translate('button.waiting_for_network'),
|
||||
onSubmit: !canSubmit ? null : onSubmit);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayModalHUD = isInAsyncCall;
|
||||
final displayModalHUD = _isInAsyncCall;
|
||||
|
||||
return Scaffold(
|
||||
// resizeToAvoidBottomInset: false,
|
||||
|
|
@ -120,7 +65,7 @@ class NewAccountPageState extends State<NewAccountPage> {
|
|||
const SignalStrengthMeterWidget(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: translate('app_bar.settings_tooltip'),
|
||||
tooltip: translate('menu.settings_tooltip'),
|
||||
onPressed: () async {
|
||||
await GoRouterHelper(context).push('/settings');
|
||||
})
|
||||
|
|
@ -132,19 +77,33 @@ class NewAccountPageState extends State<NewAccountPage> {
|
|||
FocusScope.of(context).unfocus();
|
||||
|
||||
try {
|
||||
final name =
|
||||
_formKey.currentState!.fields[formFieldName]!.value as String;
|
||||
final pronouns = _formKey.currentState!.fields[formFieldPronouns]!
|
||||
final name = _formKey.currentState!
|
||||
.fields[EditProfileForm.formFieldName]!.value as String;
|
||||
final pronouns = _formKey
|
||||
.currentState!
|
||||
.fields[EditProfileForm.formFieldPronouns]!
|
||||
.value as String? ??
|
||||
'';
|
||||
final newProfileSpec =
|
||||
NewProfileSpec(name: name, pronouns: pronouns);
|
||||
final newProfile = proto.Profile()
|
||||
..name = name
|
||||
..pronouns = pronouns;
|
||||
|
||||
final superSecret = await AccountRepository.instance
|
||||
.createWithNewSuperIdentity(newProfileSpec);
|
||||
|
||||
GoRouterHelper(context).pushReplacement('/new_account/recovery_key',
|
||||
extra: superSecret);
|
||||
setState(() {
|
||||
_isInAsyncCall = true;
|
||||
});
|
||||
try {
|
||||
final superSecret = await AccountRepository.instance
|
||||
.createWithNewSuperIdentity(newProfile);
|
||||
GoRouterHelper(context).pushReplacement(
|
||||
'/new_account/recovery_key',
|
||||
extra: superSecret);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isInAsyncCall = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (context.mounted) {
|
||||
await showErrorModal(context, translate('new_account_page.error'),
|
||||
|
|
@ -155,10 +114,4 @@ class NewAccountPageState extends State<NewAccountPage> {
|
|||
).paddingSymmetric(horizontal: 24, vertical: 8),
|
||||
).withModalHUD(context, displayModalHUD);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<bool>('isInAsyncCall', isInAsyncCall));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
106
lib/account_manager/views/profile_edit_form.dart
Normal file
106
lib/account_manager/views/profile_edit_form.dart
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.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';
|
||||
|
||||
class EditProfileForm extends StatefulWidget {
|
||||
const EditProfileForm({
|
||||
required this.header,
|
||||
required this.instructions,
|
||||
required this.submitText,
|
||||
required this.submitDisabledText,
|
||||
super.key,
|
||||
this.onSubmit,
|
||||
});
|
||||
|
||||
@override
|
||||
State createState() => _EditProfileFormState();
|
||||
|
||||
final String header;
|
||||
final String instructions;
|
||||
final Future<void> Function(GlobalKey<FormBuilderState>)? onSubmit;
|
||||
final String submitText;
|
||||
final String submitDisabledText;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('header', header))
|
||||
..add(StringProperty('instructions', instructions))
|
||||
..add(ObjectFlagProperty<
|
||||
Future<void> Function(
|
||||
GlobalKey<FormBuilderState> p1)?>.has('onSubmit', onSubmit))
|
||||
..add(StringProperty('submitText', submitText))
|
||||
..add(StringProperty('submitDisabledText', submitDisabledText));
|
||||
}
|
||||
|
||||
static const String formFieldName = 'name';
|
||||
static const String formFieldPronouns = 'pronouns';
|
||||
}
|
||||
|
||||
class _EditProfileFormState extends State<EditProfileForm> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Widget _editProfileForm(
|
||||
BuildContext context,
|
||||
) =>
|
||||
FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
Text(widget.header)
|
||||
.textStyle(context.headlineSmall)
|
||||
.paddingSymmetric(vertical: 16),
|
||||
FormBuilderTextField(
|
||||
autofocus: true,
|
||||
name: EditProfileForm.formFieldName,
|
||||
decoration:
|
||||
InputDecoration(labelText: translate('account.form_name')),
|
||||
maxLength: 64,
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(),
|
||||
]),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldPronouns,
|
||||
maxLength: 64,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('account.form_pronouns')),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
Row(children: [
|
||||
const Spacer(),
|
||||
Text(widget.instructions).toCenter().flexible(flex: 6),
|
||||
const Spacer(),
|
||||
]).paddingSymmetric(vertical: 4),
|
||||
ElevatedButton(
|
||||
onPressed: widget.onSubmit == null
|
||||
? null
|
||||
: () async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
await widget.onSubmit!(_formKey);
|
||||
}
|
||||
},
|
||||
child: Text((widget.onSubmit == null)
|
||||
? widget.submitDisabledText
|
||||
: widget.submitText),
|
||||
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _editProfileForm(
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ class ShowRecoveryKeyPageState extends State<ShowRecoveryKeyPage> {
|
|||
const SignalStrengthMeterWidget(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: translate('app_bar.settings_tooltip'),
|
||||
tooltip: translate('menu.settings_tooltip'),
|
||||
onPressed: () async {
|
||||
await GoRouterHelper(context).push('/settings');
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue