This commit is contained in:
Christien Rioux 2024-07-03 20:59:54 -04:00
parent 8c89ce91cf
commit 9dfb8c3f71
16 changed files with 305 additions and 162 deletions

View File

@ -37,11 +37,17 @@
"pronouns": "Pronouns",
"remove_account": "Remove Account",
"delete_identity": "Delete Identity",
"remove_account_confirm": "Confirm Account Removal?",
"remove_account_confirm": "Confirm Account Removal",
"remove_account_description": "Remove account from this device only",
"delete_identity_description": "Delete identity and all messages completely",
"delete_identity_confirm_message": "This action is PERMANENT, and your identity will no longer be recoverable with the recovery key. This will not remove your messages you have sent from other people's devices.",
"confirm_are_you_sure": "Are you sure you want to do this?"
"remove_account_confirm_message": " • Your account will be removed from this device ONLY\n • Your identity will remain recoverable with the recovery key\n • Your messages and contacts will remain available on other devices\n",
"delete_identity_description": "Delete identity from all devices everywhere",
"delete_identity_confirm_message": "This action is PERMANENT, and your identity will no longer be recoverable with the recovery key. Restoring from backups will not recover your account!",
"delete_identity_confirm_message_details": "You will lose access to:\n • Your entire message history\n • Your contacts\n • This will not remove your messages you have sent from other people's devices\n",
"confirm_are_you_sure": "Are you sure you want to do this?",
"failed_to_remove": "Failed to remove account.\n\nTry again when you have a more stable network connection.",
"failed_to_delete": "Failed to delete identity.\n\nTry again when you have a more stable network connection.",
"account_removed": "Account removed successfully",
"identity_deleted": "Identity deleted successfully"
},
"show_recovery_key_page": {
"titlebar": "Save Recovery Key",
@ -58,6 +64,8 @@
"accept": "Accept",
"reject": "Reject",
"finish": "Finish",
"yes_proceed": "Yes, proceed",
"no_cancel": "No, cancel",
"waiting_for_network": "Waiting For Network"
},
"toast": {

View File

@ -168,7 +168,15 @@ class AccountRepository {
}
/// Remove an account and wipe the messages for this account from this device
Future<bool> deleteLocalAccount(TypedKey superIdentityRecordKey) async {
Future<bool> deleteLocalAccount(TypedKey superIdentityRecordKey,
OwnedDHTRecordPointer? accountRecord) async {
// Delete the account record locally which causes a deep delete
// of all the contacts, invites, chats, and messages in the dht record
// pool
if (accountRecord != null) {
await DHTRecordPool.instance.deleteRecord(accountRecord.recordKey);
}
await logout(superIdentityRecordKey);
final localAccounts = await _localAccounts.get();
@ -178,8 +186,6 @@ class AccountRepository {
await _localAccounts.set(newLocalAccounts);
_streamController.add(AccountRepositoryChange.localAccounts);
// TO DO: wipe messages
return true;
}
@ -367,6 +373,11 @@ class AccountRepository {
return;
}
if (logoutUser == activeLocalAccount) {
await switchToAccount(
_localAccounts.value.firstOrNull?.superIdentity.recordKey);
}
final logoutUserLogin = fetchUserLogin(logoutUser);
if (logoutUserLogin == null) {
// Already logged out

View File

@ -20,6 +20,7 @@ class EditAccountPage extends StatefulWidget {
const EditAccountPage(
{required this.superIdentityRecordKey,
required this.existingProfile,
required this.accountRecord,
super.key});
@override
@ -27,6 +28,7 @@ class EditAccountPage extends StatefulWidget {
final TypedKey superIdentityRecordKey;
final proto.Profile existingProfile;
final OwnedDHTRecordPointer accountRecord;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
@ -34,7 +36,9 @@ class EditAccountPage extends StatefulWidget {
..add(DiagnosticsProperty<TypedKey>(
'superIdentityRecordKey', superIdentityRecordKey))
..add(DiagnosticsProperty<proto.Profile>(
'existingProfile', existingProfile));
'existingProfile', existingProfile))
..add(DiagnosticsProperty<OwnedDHTRecordPointer>(
'accountRecord', accountRecord));
}
}
@ -67,92 +71,179 @@ class _EditAccountPageState extends State<EditAccountPage> {
},
);
Future<void> _onRemoveAccount() async {
final confirmed = await StyledDialog.show<bool>(
context: context,
title: translate('edit_account_page.remove_account_confirm'),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(translate('edit_account_page.remove_account_confirm_message'))
.paddingLTRB(24, 24, 24, 0),
Text(translate('edit_account_page.confirm_are_you_sure'))
.paddingAll(8),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.cancel, size: 16).paddingLTRB(0, 0, 4, 0),
Text(translate('button.no_cancel')).paddingLTRB(0, 0, 4, 0)
])),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
Text(translate('button.yes_proceed')).paddingLTRB(0, 0, 4, 0)
]))
]).paddingAll(24)
]));
if (confirmed != null && confirmed && mounted) {
// dismiss the keyboard by unfocusing the textfield
FocusScope.of(context).unfocus();
try {
setState(() {
_isInAsyncCall = true;
});
try {
final success = await AccountRepository.instance.deleteLocalAccount(
widget.superIdentityRecordKey, widget.accountRecord);
if (success && mounted) {
showInfoToast(
context, translate('edit_account_page.account_removed'));
GoRouterHelper(context).pop();
} else if (mounted) {
showErrorToast(
context, translate('edit_account_page.failed_to_remove'));
}
} finally {
if (mounted) {
setState(() {
_isInAsyncCall = false;
});
}
}
} on Exception catch (e) {
if (mounted) {
await showErrorModal(
context, translate('new_account_page.error'), 'Exception: $e');
}
}
}
}
Future<void> _onDeleteIdentity() async {
//
}
Future<void> _onSubmit(GlobalKey<FormBuilderState> 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
..timestamp = Veilid.instance.now().toInt64();
setState(() {
_isInAsyncCall = true;
});
try {
// Look up account cubit for this specific account
final perAccountCollectionBlocMapCubit =
context.read<PerAccountCollectionBlocMapCubit>();
final accountRecordCubit = await perAccountCollectionBlocMapCubit
.operate(widget.superIdentityRecordKey,
closure: (c) async => c.accountRecordCubit);
if (accountRecordCubit == null) {
return;
}
// Update account profile DHT record
// This triggers ConversationCubits to update
await accountRecordCubit.updateProfile(newProfile);
// Update local account profile
await AccountRepository.instance
.editAccountProfile(widget.superIdentityRecordKey, newProfile);
if (mounted) {
Navigator.canPop(context)
? GoRouterHelper(context).pop()
: GoRouterHelper(context).go('/');
}
} finally {
if (mounted) {
setState(() {
_isInAsyncCall = false;
});
}
}
} on Exception catch (e) {
if (mounted) {
await showErrorModal(
context, translate('edit_account_page.error'), 'Exception: $e');
}
}
}
@override
Widget build(BuildContext context) {
final displayModalHUD = _isInAsyncCall;
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(
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
..timestamp = Veilid.instance.now().toInt64();
setState(() {
_isInAsyncCall = true;
});
try {
// Look up account cubit for this specific account
final perAccountCollectionBlocMapCubit =
context.read<PerAccountCollectionBlocMapCubit>();
final accountRecordCubit = await perAccountCollectionBlocMapCubit
.operate(widget.superIdentityRecordKey,
closure: (c) async => c.accountRecordCubit);
if (accountRecordCubit == null) {
return;
}
// Update account profile DHT record
// This triggers ConversationCubits to update
await accountRecordCubit.updateProfile(newProfile);
// Update local account profile
await AccountRepository.instance.editAccountProfile(
widget.superIdentityRecordKey, newProfile);
if (context.mounted) {
Navigator.canPop(context)
? GoRouterHelper(context).pop()
: GoRouterHelper(context).go('/');
}
} 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);
// 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(
icon: const Icon(Icons.settings),
tooltip: translate('menu.settings_tooltip'),
onPressed: () async {
await GoRouterHelper(context).push('/settings');
})
]),
body: Column(children: [
_editAccountForm(
context,
onSubmit: _onSubmit,
).expanded(),
Text(translate('edit_account_page.remove_account_description')),
ElevatedButton(
onPressed: _onRemoveAccount,
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.person_remove_alt_1, size: 16)
.paddingLTRB(0, 0, 4, 0),
Text(translate('edit_account_page.remove_account'))
.paddingLTRB(0, 0, 4, 0)
])).paddingLTRB(0, 8, 0, 24),
Text(translate('edit_account_page.delete_identity_description')),
ElevatedButton(
onPressed: _onDeleteIdentity,
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.person_off, size: 16)
.paddingLTRB(0, 0, 4, 0),
Text(translate('edit_account_page.delete_identity'))
.paddingLTRB(0, 0, 4, 0)
])).paddingLTRB(0, 8, 0, 24)
]).paddingSymmetric(horizontal: 24, vertical: 8))
.withModalHUD(context, displayModalHUD);
}
}

View File

@ -52,6 +52,43 @@ class _NewAccountPageState extends State<NewAccountPage> {
onSubmit: !canSubmit ? null : onSubmit);
}
Future<void> _onSubmit(GlobalKey<FormBuilderState> 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 = proto.Profile()
..name = name
..pronouns = pronouns;
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 (mounted) {
await showErrorModal(
context, translate('new_account_page.error'), 'Exception: $e');
}
}
}
@override
Widget build(BuildContext context) {
final displayModalHUD = _isInAsyncCall;
@ -79,45 +116,7 @@ class _NewAccountPageState extends State<NewAccountPage> {
]),
body: _newAccountForm(
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 = proto.Profile()
..name = name
..pronouns = pronouns;
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'),
'Exception: $e');
}
}
},
onSubmit: _onSubmit,
).paddingSymmetric(horizontal: 24, vertical: 8),
).withModalHUD(context, displayModalHUD);
}

View File

@ -99,9 +99,13 @@ class _EditProfileFormState extends State<EditProfileForm> {
await widget.onSubmit!(_formKey);
}
},
child: Text((widget.onSubmit == null)
? widget.submitDisabledText
: widget.submitText),
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
Text((widget.onSubmit == null)
? widget.submitDisabledText
: widget.submitText)
.paddingLTRB(0, 0, 4, 0)
]),
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
],
),

View File

@ -15,7 +15,7 @@ class VeilidChatGlobalInit {
Future<void> _initializeVeilid() async {
// Init Veilid
Veilid.instance.initializeVeilidCore(
getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name));
await getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name));
// Veilid logging
initVeilidLog(kDebugMode);

View File

@ -39,11 +39,11 @@ class _DrawerMenuState extends State<DrawerMenu> {
});
}
void _doEditClick(
TypedKey superIdentityRecordKey, proto.Profile existingProfile) {
void _doEditClick(TypedKey superIdentityRecordKey,
proto.Profile existingProfile, OwnedDHTRecordPointer accountRecord) {
singleFuture(this, () async {
await GoRouterHelper(context).push('/edit_account',
extra: [superIdentityRecordKey, existingProfile]);
extra: [superIdentityRecordKey, existingProfile, accountRecord]);
});
}
@ -128,10 +128,10 @@ class _DrawerMenuState extends State<DrawerMenu> {
final superIdentityRecordKey = la.superIdentity.recordKey;
// See if this account is logged in
final avAccountRecordState = perAccountCollectionBlocMapState
.get(superIdentityRecordKey)
?.avAccountRecordState;
if (avAccountRecordState != null) {
final perAccountState =
perAccountCollectionBlocMapState.get(superIdentityRecordKey);
final avAccountRecordState = perAccountState?.avAccountRecordState;
if (perAccountState != null && avAccountRecordState != null) {
// Account is logged in
final scale = theme.extension<ScaleScheme>()!.tertiaryScale;
final loggedInAccount = avAccountRecordState.when(
@ -144,7 +144,11 @@ class _DrawerMenuState extends State<DrawerMenu> {
_doSwitchClick(superIdentityRecordKey);
},
footerCallback: () {
_doEditClick(superIdentityRecordKey, value.profile);
_doEditClick(
superIdentityRecordKey,
value.profile,
perAccountState.accountInfo.userLogin!.accountRecordInfo
.accountRecord);
}),
loading: () => _wrapInBox(
child: buildProgressIndicator(),

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../chat/chat.dart';
import '../../../tools/tools.dart';
class HomeAccountReadyChat extends StatefulWidget {
const HomeAccountReadyChat({super.key});
@ -15,11 +14,6 @@ class HomeAccountReadyChatState extends State<HomeAccountReadyChat> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
});
}
@override

View File

@ -23,11 +23,6 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
});
}
Widget buildUserPanel() => Builder(builder: (context) {

View File

@ -45,8 +45,20 @@ class HomeScreenState extends State<HomeScreen>
curve: Curves.easeInOut,
));
// Account animation setup
WidgetsBinding.instance.addPostFrameCallback((_) async {
final localAccounts = context.read<LocalAccountsCubit>().state;
final activeLocalAccount = context.read<ActiveLocalAccountCubit>().state;
final activeIndex = localAccounts
.indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount);
final canClose = activeIndex != -1;
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
if (!canClose) {
await _zoomDrawerController.open!();
}
});
super.initState();
}
@ -152,6 +164,12 @@ class HomeScreenState extends State<HomeScreen>
scale.tertiaryScale.appBackground,
]);
final localAccounts = context.watch<LocalAccountsCubit>().state;
final activeLocalAccount = context.watch<ActiveLocalAccountCubit>().state;
final activeIndex = localAccounts
.indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount);
final canClose = activeIndex != -1;
return SafeArea(
child: DecoratedBox(
decoration: BoxDecoration(gradient: gradient),
@ -178,9 +196,9 @@ class HomeScreenState extends State<HomeScreen>
openCurve: Curves.fastEaseInToSlowEaseOut,
// duration: const Duration(milliseconds: 250),
// reverseDuration: const Duration(milliseconds: 250),
menuScreenTapClose: true,
mainScreenTapClose: true,
//disableDragGesture: false,
menuScreenTapClose: canClose,
mainScreenTapClose: canClose,
disableDragGesture: !canClose,
mainScreenScale: .25,
slideWidth: min(360, MediaQuery.of(context).size.width * 0.9),
)));

View File

@ -69,6 +69,7 @@ class RouterCubit extends Cubit<RouterState> {
return EditAccountPage(
superIdentityRecordKey: extra[0]! as TypedKey,
existingProfile: extra[1]! as proto.Profile,
accountRecord: extra[2]! as OwnedDHTRecordPointer,
);
},
),

View File

@ -5,7 +5,6 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import '../src/veilid_log.dart';
import '../veilid_support.dart';
import 'exceptions.dart';
part 'identity_instance.freezed.dart';
part 'identity_instance.g.dart';

View File

@ -1,5 +1,7 @@
import 'dart:io' show Platform;
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:veilid/veilid.dart';
// ignore: do_not_use_environment
@ -8,8 +10,8 @@ const bool _kReleaseMode = bool.fromEnvironment('dart.vm.product');
const bool _kProfileMode = bool.fromEnvironment('dart.vm.profile');
const bool _kDebugMode = !_kReleaseMode && !_kProfileMode;
Map<String, dynamic> getDefaultVeilidPlatformConfig(
bool isWeb, String appName) {
Future<Map<String, dynamic>> getDefaultVeilidPlatformConfig(
bool isWeb, String appName) async {
final ignoreLogTargetsStr =
// ignore: do_not_use_environment
const String.fromEnvironment('IGNORE_LOG_TARGETS').trim();
@ -17,6 +19,16 @@ Map<String, dynamic> getDefaultVeilidPlatformConfig(
? <String>[]
: ignoreLogTargetsStr.split(',').map((e) => e.trim()).toList();
// ignore: do_not_use_environment
var flamePathStr = const String.fromEnvironment('FLAME').trim();
if (flamePathStr == '1') {
flamePathStr = p.join(
(await getApplicationSupportDirectory()).absolute.path,
'$appName.folded');
// ignore: avoid_print
print('Flame data logged to $flamePathStr');
}
if (isWeb) {
return VeilidWASMConfig(
logging: VeilidWASMConfigLogging(
@ -52,7 +64,9 @@ Map<String, dynamic> getDefaultVeilidPlatformConfig(
api: VeilidFFIConfigLoggingApi(
enabled: true,
level: VeilidConfigLogLevel.info,
ignoreLogTargets: ignoreLogTargets)))
ignoreLogTargets: ignoreLogTargets),
flame: VeilidFFIConfigLoggingFlame(
enabled: flamePathStr.isNotEmpty, path: flamePathStr)))
.toJson();
}

View File

@ -428,7 +428,7 @@ packages:
source: hosted
version: "2.1.0"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
@ -436,7 +436,7 @@ packages:
source: hosted
version: "1.9.0"
path_provider:
dependency: transitive
dependency: "direct main"
description:
name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161

View File

@ -19,6 +19,8 @@ dependencies:
loggy: ^2.0.3
meta: ^1.12.0
path: ^1.9.0
path_provider: ^2.1.3
protobuf: ^3.1.0
veilid:
# veilid: ^0.0.1

3
process_flame.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
cat "/Users/$USER/Library/Containers/com.veilid.veilidchat/Data/Library/Application Support/com.veilid.veilidchat/VeilidChat.folded" | inferno-flamegraph -c purple --fontsize 8 --height 24 --title "VeilidChat" --factor 0.000000001 --countname secs > /tmp/veilidchat.svg
cat "/Users/$USER/Library/Containers/com.veilid.veilidchat/Data/Library/Application Support/com.veilid.veilidchat/VeilidChat.folded" | inferno-flamegraph --reverse -c aqua --fontsize 8 --height 24 --title "VeilidChat Reverse" --factor 0.000000001 --countname secs > /tmp/veilidchat-reverse.svg