contact invitation accept notifications

This commit is contained in:
Christien Rioux 2024-07-25 14:37:51 -04:00
parent 6080c2f0c6
commit 1455aabe6c
27 changed files with 718 additions and 220 deletions

View File

@ -153,6 +153,10 @@
"invalid_pin": "Invalid PIN", "invalid_pin": "Invalid PIN",
"invalid_password": "Invalid password" "invalid_password": "Invalid password"
}, },
"waiting_invitation": {
"accepted": "Contact invitation accepted from {name}",
"reject": "Contact invitation was rejected"
},
"paste_invitation_dialog": { "paste_invitation_dialog": {
"title": "Paste Contact Invite", "title": "Paste Contact Invite",
"paste_invite_here": "Paste your contact invite here:", "paste_invite_here": "Paste your contact invite here:",

View File

@ -11,6 +11,7 @@ import '../../chat_list/chat_list.dart';
import '../../contact_invitation/contact_invitation.dart'; import '../../contact_invitation/contact_invitation.dart';
import '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
import '../../conversation/conversation.dart'; import '../../conversation/conversation.dart';
import '../../notifications/notifications.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../account_manager.dart'; import '../account_manager.dart';
@ -146,6 +147,7 @@ class PerAccountCollectionCubit extends Cubit<PerAccountCollectionState> {
accountRecordCubit!, accountRecordCubit!,
contactInvitationListCubit, contactInvitationListCubit,
contactListCubit, contactListCubit,
_locator<NotificationsCubit>(),
)); ));
// ActiveChatCubit // ActiveChatCubit
@ -262,13 +264,15 @@ class PerAccountCollectionCubit extends Cubit<PerAccountCollectionState> {
AccountInfo, AccountInfo,
AccountRecordCubit, AccountRecordCubit,
ContactInvitationListCubit, ContactInvitationListCubit,
ContactListCubit ContactListCubit,
NotificationsCubit,
)>( )>(
create: (params) => WaitingInvitationsBlocMapCubit( create: (params) => WaitingInvitationsBlocMapCubit(
accountInfo: params.$1, accountInfo: params.$1,
accountRecordCubit: params.$2, accountRecordCubit: params.$2,
contactInvitationListCubit: params.$3, contactInvitationListCubit: params.$3,
contactListCubit: params.$4, contactListCubit: params.$4,
notificationsCubit: params.$5,
)); ));
final activeChatCubitUpdater = final activeChatCubitUpdater =
BlocUpdater<ActiveChatCubit, bool>(create: (_) => ActiveChatCubit(null)); BlocUpdater<ActiveChatCubit, bool>(create: (_) => ActiveChatCubit(null));

View File

@ -11,6 +11,7 @@ import 'package:protobuf/protobuf.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 '../../notifications/notifications.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
@ -106,12 +107,14 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
final success = await AccountRepository.instance.deleteLocalAccount( final success = await AccountRepository.instance.deleteLocalAccount(
widget.superIdentityRecordKey, widget.accountRecord); widget.superIdentityRecordKey, widget.accountRecord);
if (success && mounted) { if (success && mounted) {
showInfoToast( context
context, translate('edit_account_page.account_removed')); .read<NotificationsCubit>()
.info(text: translate('edit_account_page.account_removed'));
GoRouterHelper(context).pop(); GoRouterHelper(context).pop();
} else if (mounted) { } else if (mounted) {
showErrorToast( context
context, translate('edit_account_page.failed_to_remove')); .read<NotificationsCubit>()
.error(text: translate('edit_account_page.failed_to_remove'));
} }
} finally { } finally {
if (mounted) { if (mounted) {
@ -172,12 +175,14 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
final success = await AccountRepository.instance.destroyAccount( final success = await AccountRepository.instance.destroyAccount(
widget.superIdentityRecordKey, widget.accountRecord); widget.superIdentityRecordKey, widget.accountRecord);
if (success && mounted) { if (success && mounted) {
showInfoToast( context
context, translate('edit_account_page.account_destroyed')); .read<NotificationsCubit>()
.info(text: translate('edit_account_page.account_destroyed'));
GoRouterHelper(context).pop(); GoRouterHelper(context).pop();
} else if (mounted) { } else if (mounted) {
showErrorToast( context
context, translate('edit_account_page.failed_to_destroy')); .read<NotificationsCubit>()
.error(text: translate('edit_account_page.failed_to_destroy'));
} }
} finally { } finally {
if (mounted) { if (mounted) {

View File

@ -1,5 +1,6 @@
import 'package:animated_theme_switcher/animated_theme_switcher.dart'; import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -13,6 +14,7 @@ import 'package:veilid_support/veilid_support.dart';
import 'account_manager/account_manager.dart'; import 'account_manager/account_manager.dart';
import 'init.dart'; import 'init.dart';
import 'layout/splash.dart'; import 'layout/splash.dart';
import 'notifications/notifications.dart';
import 'router/router.dart'; import 'router/router.dart';
import 'settings/settings.dart'; import 'settings/settings.dart';
import 'theme/theme.dart'; import 'theme/theme.dart';
@ -24,8 +26,8 @@ class ReloadThemeIntent extends Intent {
const ReloadThemeIntent(); const ReloadThemeIntent();
} }
class AttachDetachThemeIntent extends Intent { class AttachDetachIntent extends Intent {
const AttachDetachThemeIntent(); const AttachDetachIntent();
} }
class VeilidChatApp extends StatelessWidget { class VeilidChatApp extends StatelessWidget {
@ -55,7 +57,7 @@ class VeilidChatApp extends StatelessWidget {
}); });
} }
void _attachDetachTheme(BuildContext context) { void _attachDetach(BuildContext context) {
singleFuture(this, () async { singleFuture(this, () async {
if (ProcessorRepository.instance.processorConnectionState.isAttached) { if (ProcessorRepository.instance.processorConnectionState.isAttached) {
log.info('Detaching'); log.info('Detaching');
@ -77,14 +79,13 @@ class VeilidChatApp extends StatelessWidget {
const ReloadThemeIntent(), const ReloadThemeIntent(),
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.alt, LogicalKeyboardKey.keyD): LogicalKeyboardKey.alt, LogicalKeyboardKey.keyD):
const AttachDetachThemeIntent(), const AttachDetachIntent(),
}, },
child: Actions(actions: <Type, Action<Intent>>{ child: Actions(actions: <Type, Action<Intent>>{
ReloadThemeIntent: CallbackAction<ReloadThemeIntent>( ReloadThemeIntent: CallbackAction<ReloadThemeIntent>(
onInvoke: (intent) => _reloadTheme(context)), onInvoke: (intent) => _reloadTheme(context)),
AttachDetachThemeIntent: AttachDetachIntent: CallbackAction<AttachDetachIntent>(
CallbackAction<AttachDetachThemeIntent>( onInvoke: (intent) => _attachDetach(context)),
onInvoke: (intent) => _attachDetachTheme(context)),
}, child: Focus(autofocus: true, child: builder(context))))); }, child: Focus(autofocus: true, child: builder(context)))));
@override @override
@ -101,10 +102,17 @@ class VeilidChatApp extends StatelessWidget {
final localizationDelegate = LocalizedApp.of(context).delegate; final localizationDelegate = LocalizedApp.of(context).delegate;
return ThemeProvider( return ThemeProvider(
initTheme: initialThemeData, initTheme: initialThemeData,
builder: (_, theme) => LocalizationProvider( builder: (context, theme) => LocalizationProvider(
state: LocalizationProvider.of(context).state, state: LocalizationProvider.of(context).state,
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [
BlocProvider<PreferencesCubit>(
create: (context) =>
PreferencesCubit(PreferencesRepository.instance),
),
BlocProvider<NotificationsCubit>(
create: (context) => NotificationsCubit(
const NotificationsState(queue: IList.empty()))),
BlocProvider<ConnectionStateCubit>( BlocProvider<ConnectionStateCubit>(
create: (context) => create: (context) =>
ConnectionStateCubit(ProcessorRepository.instance)), ConnectionStateCubit(ProcessorRepository.instance)),
@ -124,10 +132,6 @@ class VeilidChatApp extends StatelessWidget {
create: (context) => create: (context) =>
ActiveLocalAccountCubit(AccountRepository.instance), ActiveLocalAccountCubit(AccountRepository.instance),
), ),
BlocProvider<PreferencesCubit>(
create: (context) =>
PreferencesCubit(PreferencesRepository.instance),
),
BlocProvider<PerAccountCollectionBlocMapCubit>( BlocProvider<PerAccountCollectionBlocMapCubit>(
create: (context) => PerAccountCollectionBlocMapCubit( create: (context) => PerAccountCollectionBlocMapCubit(
accountRepository: AccountRepository.instance, accountRepository: AccountRepository.instance,

View File

@ -13,6 +13,7 @@ import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
import '../../conversation/conversation.dart'; import '../../conversation/conversation.dart';
import '../../notifications/notifications.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../chat.dart'; import '../chat.dart';
@ -27,10 +28,10 @@ class ChatComponentWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); // final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; // final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; // final scaleConfig = theme.extension<ScaleConfig>()!;
final textTheme = theme.textTheme; // final textTheme = theme.textTheme;
// Get the account info // Get the account info
final accountInfo = context.watch<AccountInfoCubit>().state; final accountInfo = context.watch<AccountInfoCubit>().state;
@ -221,14 +222,15 @@ class ChatComponentWidget extends StatelessWidget {
onSendPressed: (pt) { onSendPressed: (pt) {
try { try {
if (!messageIsValid) { if (!messageIsValid) {
showErrorToast(context, context.read<NotificationsCubit>().error(
translate('chat.message_too_long')); text:
translate('chat.message_too_long'));
return; return;
} }
_handleSendPressed(chatComponentCubit, pt); _handleSendPressed(chatComponentCubit, pt);
} on FormatException { } on FormatException {
showErrorToast(context, context.read<NotificationsCubit>().error(
translate('chat.message_too_long')); text: translate('chat.message_too_long'));
} }
}, },
listBottomWidget: messageIsValid listBottomWidget: messageIsValid
@ -267,8 +269,11 @@ class ChatComponentWidget extends StatelessWidget {
ChatComponentCubit chatComponentCubit, ChatComponentCubit chatComponentCubit,
WindowState<types.Message> messageWindow, WindowState<types.Message> messageWindow,
ScrollNotification notification) async { ScrollNotification notification) async {
print( debugPrint(
'_handlePageForward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); '_handlePageForward: messagesState.length=${messageWindow.length} '
'messagesState.windowTail=${messageWindow.windowTail} '
'messagesState.windowCount=${messageWindow.windowCount} '
'ScrollNotification=$notification');
// Go forward a page // Go forward a page
final tail = min(messageWindow.length, final tail = min(messageWindow.length,
@ -299,8 +304,11 @@ class ChatComponentWidget extends StatelessWidget {
WindowState<types.Message> messageWindow, WindowState<types.Message> messageWindow,
ScrollNotification notification, ScrollNotification notification,
) async { ) async {
print( debugPrint(
'_handlePageBackward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); '_handlePageBackward: messagesState.length=${messageWindow.length} '
'messagesState.windowTail=${messageWindow.windowTail} '
'messagesState.windowCount=${messageWindow.windowCount} '
'ScrollNotification=$notification');
// Go back a page // Go back a page
final tail = max( final tail = max(

View File

@ -54,7 +54,7 @@ class ContactInvitationListCubit
return dhtRecord; return dhtRecord;
} }
Future<Uint8List> createInvitation( Future<(Uint8List, TypedKey)> createInvitation(
{required proto.Profile profile, {required proto.Profile profile,
required EncryptionKeyType encryptionKeyType, required EncryptionKeyType encryptionKeyType,
required String encryptionKey, required String encryptionKey,
@ -82,6 +82,7 @@ class ContactInvitationListCubit
// to and it will be eventually encrypted with the DH of the contact's // to and it will be eventually encrypted with the DH of the contact's
// identity key // identity key
late final Uint8List signedContactInvitationBytes; late final Uint8List signedContactInvitationBytes;
late final TypedKey contactRequestInboxKey;
await (await pool.createRecord( await (await pool.createRecord(
debugName: 'ContactInvitationListCubit::createInvitation::' debugName: 'ContactInvitationListCubit::createInvitation::'
'LocalConversation', 'LocalConversation',
@ -119,6 +120,9 @@ class ContactInvitationListCubit
]), ]),
crypto: const VeilidCryptoPublic())) crypto: const VeilidCryptoPublic()))
.deleteScope((contactRequestInbox) async { .deleteScope((contactRequestInbox) async {
// Keep the contact request inbox key
contactRequestInboxKey = contactRequestInbox.key;
// Store ContactRequest in owner subkey // Store ContactRequest in owner subkey
await contactRequestInbox.eventualWriteProtobuf(creq); await contactRequestInbox.eventualWriteProtobuf(creq);
// Store an empty invitation response // Store an empty invitation response
@ -158,7 +162,7 @@ class ContactInvitationListCubit
}); });
}); });
return signedContactInvitationBytes; return (signedContactInvitationBytes, contactRequestInboxKey);
} }
Future<void> deleteInvitation( Future<void> deleteInvitation(

View File

@ -1,8 +1,9 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:veilid_support/veilid_support.dart';
class InvitationGeneratorCubit extends FutureCubit<Uint8List> { class InvitationGeneratorCubit extends FutureCubit<(Uint8List, TypedKey)> {
InvitationGeneratorCubit(super.fut); InvitationGeneratorCubit(super.fut);
InvitationGeneratorCubit.value(super.v) : super.value(); InvitationGeneratorCubit.value(super.v) : super.value();
} }

View File

@ -1,9 +1,11 @@
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:flutter_translate/flutter_translate.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 '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
import '../../notifications/notifications.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import 'cubits.dart'; import 'cubits.dart';
@ -22,11 +24,13 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
{required AccountInfo accountInfo, {required AccountInfo accountInfo,
required AccountRecordCubit accountRecordCubit, required AccountRecordCubit accountRecordCubit,
required ContactInvitationListCubit contactInvitationListCubit, required ContactInvitationListCubit contactInvitationListCubit,
required ContactListCubit contactListCubit}) required ContactListCubit contactListCubit,
required NotificationsCubit notificationsCubit})
: _accountInfo = accountInfo, : _accountInfo = accountInfo,
_accountRecordCubit = accountRecordCubit, _accountRecordCubit = accountRecordCubit,
_contactInvitationListCubit = contactInvitationListCubit, _contactInvitationListCubit = contactInvitationListCubit,
_contactListCubit = contactListCubit { _contactListCubit = contactListCubit,
_notificationsCubit = notificationsCubit {
// React to invitation status changes // React to invitation status changes
_singleInvitationStatusProcessor.follow( _singleInvitationStatusProcessor.follow(
stream, state, _invitationStatusListener); stream, state, _invitationStatusListener);
@ -81,11 +85,22 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
localConversationRecordKey: localConversationRecordKey:
acceptedContact.localConversationRecordKey, acceptedContact.localConversationRecordKey,
); );
// Notify about acceptance
_notificationsCubit.info(
text: translate('waiting_invitation.accepted',
args: {'name': acceptedContact.remoteProfile.name}));
} else { } else {
// Reject // Reject
await _contactInvitationListCubit.deleteInvitation( await _contactInvitationListCubit.deleteInvitation(
accepted: false, accepted: false,
contactRequestInboxRecordKey: contactRequestInboxRecordKey); contactRequestInboxRecordKey: contactRequestInboxRecordKey);
// Notify about rejection
_notificationsCubit.info(
text: translate(
'waiting_invitation.rejected',
));
} }
} }
} }
@ -108,6 +123,7 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
final AccountRecordCubit _accountRecordCubit; final AccountRecordCubit _accountRecordCubit;
final ContactInvitationListCubit _contactInvitationListCubit; final ContactInvitationListCubit _contactInvitationListCubit;
final ContactListCubit _contactListCubit; final ContactListCubit _contactListCubit;
final NotificationsCubit _notificationsCubit;
final _singleInvitationStatusProcessor = final _singleInvitationStatusProcessor =
SingleStateProcessor<WaitingInvitationsBlocMapState>(); SingleStateProcessor<WaitingInvitationsBlocMapState>();
} }

View File

@ -7,19 +7,22 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../notifications/notifications.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../contact_invitation.dart'; import '../contact_invitation.dart';
class ContactInvitationDisplayDialog extends StatelessWidget { class ContactInvitationDisplayDialog extends StatelessWidget {
const ContactInvitationDisplayDialog._({ const ContactInvitationDisplayDialog._({
required this.modalContext, required this.locator,
required this.message, required this.message,
}); });
final BuildContext modalContext; final Locator locator;
final String message; final String message;
@override @override
@ -27,7 +30,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties properties
..add(StringProperty('message', message)) ..add(StringProperty('message', message))
..add(DiagnosticsProperty<BuildContext>('modalContext', modalContext)); ..add(DiagnosticsProperty<Locator>('locator', locator));
} }
String makeTextInvite(String message, Uint8List data) { String makeTextInvite(String message, Uint8List data) {
@ -48,72 +51,87 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
final scaleConfig = theme.extension<ScaleConfig>()!; final scaleConfig = theme.extension<ScaleConfig>()!;
final signedContactInvitationBytesV = final generatorOutputV = context.watch<InvitationGeneratorCubit>().state;
context.watch<InvitationGeneratorCubit>().state;
final cardsize = final cardsize =
min<double>(MediaQuery.of(context).size.shortestSide - 48.0, 400); min<double>(MediaQuery.of(context).size.shortestSide - 48.0, 400);
return PopControl( return BlocListener<ContactInvitationListCubit,
dismissible: !signedContactInvitationBytesV.isLoading, ContactInvitiationListState>(
child: Dialog( bloc: locator<ContactInvitationListCubit>(),
shape: RoundedRectangleBorder( listener: (context, state) {
side: const BorderSide(width: 2), final listState = state.state.asData?.value;
borderRadius: final data = generatorOutputV.asData?.value;
BorderRadius.circular(16 * scaleConfig.borderRadiusScale)),
backgroundColor: Colors.white, if (listState != null && data != null) {
child: ConstrainedBox( final idx = listState.indexWhere((x) =>
constraints: BoxConstraints( x.value.contactRequestInbox.recordKey.toVeilid() == data.$2);
minWidth: cardsize, if (idx == -1) {
maxWidth: cardsize, // This invitation is gone, close it
minHeight: cardsize, Navigator.pop(context);
maxHeight: cardsize), }
child: signedContactInvitationBytesV.when( }
loading: buildProgressIndicator, },
data: (data) => Column(children: [ child: PopControl(
FittedBox( dismissible: !generatorOutputV.isLoading,
child: Text( child: Dialog(
translate( shape: RoundedRectangleBorder(
'create_invitation_dialog.contact_invitation'), side: const BorderSide(width: 2),
style: textTheme.headlineSmall! borderRadius: BorderRadius.circular(
.copyWith(color: Colors.black))) 16 * scaleConfig.borderRadiusScale)),
.paddingAll(8), backgroundColor: Colors.white,
FittedBox( child: ConstrainedBox(
child: QrImageView.withQr( constraints: BoxConstraints(
size: 300, minWidth: cardsize,
qr: QrCode.fromUint8List( maxWidth: cardsize,
data: data, minHeight: cardsize,
errorCorrectLevel: maxHeight: cardsize),
QrErrorCorrectLevel.L))) child: generatorOutputV.when(
.expanded(), loading: buildProgressIndicator,
Text(message, data: (data) => Column(children: [
softWrap: true, FittedBox(
style: textTheme.labelLarge! child: Text(
.copyWith(color: Colors.black)) translate('create_invitation_dialog'
.paddingAll(8), '.contact_invitation'),
ElevatedButton.icon( style: textTheme.headlineSmall!
icon: const Icon(Icons.copy), .copyWith(color: Colors.black)))
style: ElevatedButton.styleFrom( .paddingAll(8),
foregroundColor: Colors.black, FittedBox(
backgroundColor: Colors.white, child: QrImageView.withQr(
side: const BorderSide()), size: 300,
label: Text(translate( qr: QrCode.fromUint8List(
'create_invitation_dialog.copy_invitation')), data: data.$1,
onPressed: () async { errorCorrectLevel:
showInfoToast( QrErrorCorrectLevel.L)))
context, .expanded(),
translate( Text(message,
'create_invitation_dialog.invitation_copied')); softWrap: true,
await Clipboard.setData(ClipboardData( style: textTheme.labelLarge!
text: makeTextInvite(message, data))); .copyWith(color: Colors.black))
}, .paddingAll(8),
).paddingAll(16), ElevatedButton.icon(
]), icon: const Icon(Icons.copy),
error: errorPage)))); style: ElevatedButton.styleFrom(
foregroundColor: Colors.black,
backgroundColor: Colors.white,
side: const BorderSide()),
label: Text(translate(
'create_invitation_dialog.copy_invitation')),
onPressed: () async {
context.read<NotificationsCubit>().info(
text: translate('create_invitation_dialog'
'.invitation_copied'));
await Clipboard.setData(ClipboardData(
text: makeTextInvite(message, data.$1)));
},
).paddingAll(16),
]),
error: errorPage)))));
} }
static Future<void> show( static Future<void> show(
{required BuildContext context, {required BuildContext context,
required Locator locator,
required InvitationGeneratorCubit Function(BuildContext) create, required InvitationGeneratorCubit Function(BuildContext) create,
required String message}) async { required String message}) async {
await showPopControlDialog<void>( await showPopControlDialog<void>(
@ -121,7 +139,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
builder: (context) => BlocProvider( builder: (context) => BlocProvider(
create: create, create: create,
child: ContactInvitationDisplayDialog._( child: ContactInvitationDisplayDialog._(
modalContext: context, locator: locator,
message: message, message: message,
))); )));
} }

View File

@ -52,9 +52,13 @@ class ContactInvitationItemWidget extends StatelessWidget {
} }
await ContactInvitationDisplayDialog.show( await ContactInvitationDisplayDialog.show(
context: context, context: context,
locator: context.read,
message: contactInvitationRecord.message, message: contactInvitationRecord.message,
create: (context) => InvitationGeneratorCubit.value( create: (context) => InvitationGeneratorCubit.value((
Uint8List.fromList(contactInvitationRecord.invitation))); Uint8List.fromList(contactInvitationRecord.invitation),
contactInvitationRecord.contactRequestInbox.recordKey
.toVeilid()
)));
}, },
endActions: [ endActions: [
SliderTileAction( SliderTileAction(

View File

@ -5,16 +5,17 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:provider/provider.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 '../../notifications/notifications.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../contact_invitation.dart'; import '../contact_invitation.dart';
class CreateInvitationDialog extends StatefulWidget { class CreateInvitationDialog extends StatefulWidget {
const CreateInvitationDialog._({required this.modalContext}); const CreateInvitationDialog._({required this.locator});
@override @override
CreateInvitationDialogState createState() => CreateInvitationDialogState(); CreateInvitationDialogState createState() => CreateInvitationDialogState();
@ -23,16 +24,15 @@ class CreateInvitationDialog extends StatefulWidget {
await StyledDialog.show<void>( await StyledDialog.show<void>(
context: context, context: context,
title: translate('create_invitation_dialog.title'), title: translate('create_invitation_dialog.title'),
child: CreateInvitationDialog._(modalContext: context)); child: CreateInvitationDialog._(locator: context.read));
} }
final BuildContext modalContext; final Locator locator;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties properties.add(DiagnosticsProperty<Locator>('locator', locator));
.add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
} }
} }
@ -86,8 +86,8 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
if (!mounted) { if (!mounted) {
return; return;
} }
showErrorToast( context.read<NotificationsCubit>().error(
context, translate('create_invitation_dialog.pin_does_not_match')); text: translate('create_invitation_dialog.pin_does_not_match'));
setState(() { setState(() {
_encryptionKeyType = EncryptionKeyType.none; _encryptionKeyType = EncryptionKeyType.none;
_encryptionKey = ''; _encryptionKey = '';
@ -124,8 +124,8 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
if (!mounted) { if (!mounted) {
return; return;
} }
showErrorToast(context, context.read<NotificationsCubit>().error(
translate('create_invitation_dialog.password_does_not_match')); text: translate('create_invitation_dialog.password_does_not_match'));
setState(() { setState(() {
_encryptionKeyType = EncryptionKeyType.none; _encryptionKeyType = EncryptionKeyType.none;
_encryptionKey = ''; _encryptionKey = '';
@ -138,13 +138,9 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
// Start generation // Start generation
final contactInvitationListCubit = final contactInvitationListCubit =
widget.modalContext.read<ContactInvitationListCubit>(); widget.locator<ContactInvitationListCubit>();
final profile = widget.modalContext final profile =
.read<AccountRecordCubit>() widget.locator<AccountRecordCubit>().state.asData?.value.profile;
.state
.asData
?.value
.profile;
if (profile == null) { if (profile == null) {
return; return;
} }
@ -158,6 +154,7 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
await ContactInvitationDisplayDialog.show( await ContactInvitationDisplayDialog.show(
context: context, context: context,
locator: widget.locator,
message: _messageTextController.text, message: _messageTextController.text,
create: (context) => InvitationGeneratorCubit(generator)); create: (context) => InvitationGeneratorCubit(generator));

View File

@ -9,6 +9,7 @@ import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
import '../../notifications/notifications.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../contact_invitation.dart'; import '../contact_invitation.dart';
@ -102,7 +103,9 @@ class InvitationDialogState extends State<InvitationDialog> {
} }
} else { } else {
if (mounted) { if (mounted) {
showErrorToast(context, 'invitation_dialog.failed_to_accept'); context
.read<NotificationsCubit>()
.error(text: 'invitation_dialog.failed_to_accept');
} }
} }
} }
@ -124,7 +127,9 @@ class InvitationDialogState extends State<InvitationDialog> {
// do nothing right now // do nothing right now
} else { } else {
if (mounted) { if (mounted) {
showErrorToast(context, 'invitation_dialog.failed_to_reject'); context
.read<NotificationsCubit>()
.error(text: 'invitation_dialog.failed_to_reject');
} }
} }
} }
@ -218,7 +223,7 @@ class InvitationDialogState extends State<InvitationDialog> {
errorText = translate('invitation_dialog.invalid_password'); errorText = translate('invitation_dialog.invalid_password');
} }
if (mounted) { if (mounted) {
showErrorToast(context, errorText); context.read<NotificationsCubit>().error(text: errorText);
} }
setState(() { setState(() {
_isValidating = false; _isValidating = false;
@ -233,7 +238,7 @@ class InvitationDialogState extends State<InvitationDialog> {
errorText = translate('invitation_dialog.invalid_invitation'); errorText = translate('invitation_dialog.invalid_invitation');
} }
if (mounted) { if (mounted) {
showErrorToast(context, errorText); context.read<NotificationsCubit>().error(text: errorText);
} }
setState(() { setState(() {
_isValidating = false; _isValidating = false;

View File

@ -12,6 +12,7 @@ import 'package:pasteboard/pasteboard.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:zxing2/qrcode.dart'; import 'package:zxing2/qrcode.dart';
import '../../notifications/notifications.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import 'invitation_dialog.dart'; import 'invitation_dialog.dart';
@ -269,13 +270,18 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
)); ));
} on MobileScannerException catch (e) { } on MobileScannerException catch (e) {
if (e.errorCode == MobileScannerErrorCode.permissionDenied) { if (e.errorCode == MobileScannerErrorCode.permissionDenied) {
showErrorToast( context
context, translate('scan_invitation_dialog.permission_error')); .read<NotificationsCubit>()
.error(text: translate('scan_invitation_dialog.permission_error'));
} else { } else {
showErrorToast(context, translate('scan_invitation_dialog.error')); context
.read<NotificationsCubit>()
.error(text: translate('scan_invitation_dialog.error'));
} }
} on Exception catch (_) { } on Exception catch (_) {
showErrorToast(context, translate('scan_invitation_dialog.error')); context
.read<NotificationsCubit>()
.error(text: translate('scan_invitation_dialog.error'));
} }
return null; return null;
@ -285,8 +291,9 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
final imageBytes = await Pasteboard.image; final imageBytes = await Pasteboard.image;
if (imageBytes == null) { if (imageBytes == null) {
if (context.mounted) { if (context.mounted) {
showErrorToast( context
context, translate('scan_invitation_dialog.not_an_image')); .read<NotificationsCubit>()
.error(text: translate('scan_invitation_dialog.not_an_image'));
} }
return null; return null;
} }
@ -294,8 +301,8 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
final image = img.decodeImage(imageBytes); final image = img.decodeImage(imageBytes);
if (image == null) { if (image == null) {
if (context.mounted) { if (context.mounted) {
showErrorToast(context, context.read<NotificationsCubit>().error(
translate('scan_invitation_dialog.could_not_decode_image')); text: translate('scan_invitation_dialog.could_not_decode_image'));
} }
return null; return null;
} }
@ -319,8 +326,8 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
return Uint8List.fromList(segs[0].toList()); return Uint8List.fromList(segs[0].toList());
} on Exception catch (_) { } on Exception catch (_) {
if (context.mounted) { if (context.mounted) {
showErrorToast( context.read<NotificationsCubit>().error(
context, translate('scan_invitation_dialog.not_a_valid_qr_code')); text: translate('scan_invitation_dialog.not_a_valid_qr_code'));
} }
return null; return null;
} }

View File

@ -1,9 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:quickalert/quickalert.dart';
import 'package:radix_colors/radix_colors.dart'; import 'package:radix_colors/radix_colors.dart';
import '../tools/tools.dart'; import '../tools/tools.dart';

View File

@ -0,0 +1,26 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../notifications.dart';
class NotificationsCubit extends Cubit<NotificationsState> {
NotificationsCubit(super.initialState);
void info({required String text, String? title}) {
emit(state.copyWith(
queue: state.queue.add(NotificationItem(
type: NotificationType.info, text: text, title: title))));
}
void error({required String text, String? title}) {
emit(state.copyWith(
queue: state.queue.add(NotificationItem(
type: NotificationType.info, text: text, title: title))));
}
IList<NotificationItem> popAll() {
final out = state.queue;
emit(state.copyWith(queue: state.queue.clear()));
return out;
}
}

View File

@ -0,0 +1,23 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'notifications_state.freezed.dart';
enum NotificationType {
info,
error,
}
@freezed
class NotificationItem with _$NotificationItem {
const factory NotificationItem(
{required NotificationType type,
required String text,
String? title}) = _NotificationItem;
}
@freezed
class NotificationsState with _$NotificationsState {
const factory NotificationsState({required IList<NotificationItem> queue}) =
_NotificationsState;
}

View File

@ -0,0 +1,290 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'notifications_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$NotificationItem {
NotificationType get type => throw _privateConstructorUsedError;
String get text => throw _privateConstructorUsedError;
String? get title => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$NotificationItemCopyWith<NotificationItem> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $NotificationItemCopyWith<$Res> {
factory $NotificationItemCopyWith(
NotificationItem value, $Res Function(NotificationItem) then) =
_$NotificationItemCopyWithImpl<$Res, NotificationItem>;
@useResult
$Res call({NotificationType type, String text, String? title});
}
/// @nodoc
class _$NotificationItemCopyWithImpl<$Res, $Val extends NotificationItem>
implements $NotificationItemCopyWith<$Res> {
_$NotificationItemCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? type = null,
Object? text = null,
Object? title = freezed,
}) {
return _then(_value.copyWith(
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as NotificationType,
text: null == text
? _value.text
: text // ignore: cast_nullable_to_non_nullable
as String,
title: freezed == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
}
/// @nodoc
abstract class _$$NotificationItemImplCopyWith<$Res>
implements $NotificationItemCopyWith<$Res> {
factory _$$NotificationItemImplCopyWith(_$NotificationItemImpl value,
$Res Function(_$NotificationItemImpl) then) =
__$$NotificationItemImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({NotificationType type, String text, String? title});
}
/// @nodoc
class __$$NotificationItemImplCopyWithImpl<$Res>
extends _$NotificationItemCopyWithImpl<$Res, _$NotificationItemImpl>
implements _$$NotificationItemImplCopyWith<$Res> {
__$$NotificationItemImplCopyWithImpl(_$NotificationItemImpl _value,
$Res Function(_$NotificationItemImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? type = null,
Object? text = null,
Object? title = freezed,
}) {
return _then(_$NotificationItemImpl(
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as NotificationType,
text: null == text
? _value.text
: text // ignore: cast_nullable_to_non_nullable
as String,
title: freezed == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
class _$NotificationItemImpl implements _NotificationItem {
const _$NotificationItemImpl(
{required this.type, required this.text, this.title});
@override
final NotificationType type;
@override
final String text;
@override
final String? title;
@override
String toString() {
return 'NotificationItem(type: $type, text: $text, title: $title)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$NotificationItemImpl &&
(identical(other.type, type) || other.type == type) &&
(identical(other.text, text) || other.text == text) &&
(identical(other.title, title) || other.title == title));
}
@override
int get hashCode => Object.hash(runtimeType, type, text, title);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$NotificationItemImplCopyWith<_$NotificationItemImpl> get copyWith =>
__$$NotificationItemImplCopyWithImpl<_$NotificationItemImpl>(
this, _$identity);
}
abstract class _NotificationItem implements NotificationItem {
const factory _NotificationItem(
{required final NotificationType type,
required final String text,
final String? title}) = _$NotificationItemImpl;
@override
NotificationType get type;
@override
String get text;
@override
String? get title;
@override
@JsonKey(ignore: true)
_$$NotificationItemImplCopyWith<_$NotificationItemImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$NotificationsState {
IList<NotificationItem> get queue => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$NotificationsStateCopyWith<NotificationsState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $NotificationsStateCopyWith<$Res> {
factory $NotificationsStateCopyWith(
NotificationsState value, $Res Function(NotificationsState) then) =
_$NotificationsStateCopyWithImpl<$Res, NotificationsState>;
@useResult
$Res call({IList<NotificationItem> queue});
}
/// @nodoc
class _$NotificationsStateCopyWithImpl<$Res, $Val extends NotificationsState>
implements $NotificationsStateCopyWith<$Res> {
_$NotificationsStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? queue = null,
}) {
return _then(_value.copyWith(
queue: null == queue
? _value.queue
: queue // ignore: cast_nullable_to_non_nullable
as IList<NotificationItem>,
) as $Val);
}
}
/// @nodoc
abstract class _$$NotificationsStateImplCopyWith<$Res>
implements $NotificationsStateCopyWith<$Res> {
factory _$$NotificationsStateImplCopyWith(_$NotificationsStateImpl value,
$Res Function(_$NotificationsStateImpl) then) =
__$$NotificationsStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({IList<NotificationItem> queue});
}
/// @nodoc
class __$$NotificationsStateImplCopyWithImpl<$Res>
extends _$NotificationsStateCopyWithImpl<$Res, _$NotificationsStateImpl>
implements _$$NotificationsStateImplCopyWith<$Res> {
__$$NotificationsStateImplCopyWithImpl(_$NotificationsStateImpl _value,
$Res Function(_$NotificationsStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? queue = null,
}) {
return _then(_$NotificationsStateImpl(
queue: null == queue
? _value.queue
: queue // ignore: cast_nullable_to_non_nullable
as IList<NotificationItem>,
));
}
}
/// @nodoc
class _$NotificationsStateImpl implements _NotificationsState {
const _$NotificationsStateImpl({required this.queue});
@override
final IList<NotificationItem> queue;
@override
String toString() {
return 'NotificationsState(queue: $queue)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$NotificationsStateImpl &&
const DeepCollectionEquality().equals(other.queue, queue));
}
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(queue));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$NotificationsStateImplCopyWith<_$NotificationsStateImpl> get copyWith =>
__$$NotificationsStateImplCopyWithImpl<_$NotificationsStateImpl>(
this, _$identity);
}
abstract class _NotificationsState implements NotificationsState {
const factory _NotificationsState(
{required final IList<NotificationItem> queue}) =
_$NotificationsStateImpl;
@override
IList<NotificationItem> get queue;
@override
@JsonKey(ignore: true)
_$$NotificationsStateImplCopyWith<_$NotificationsStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,3 @@
export 'cubits/notifications_cubit.dart';
export 'models/notifications_state.dart';
export 'views/notifications_widget.dart';

View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:motion_toast/motion_toast.dart';
import '../../theme/theme.dart';
import '../notifications.dart';
class NotificationsWidget extends StatelessWidget {
const NotificationsWidget({required Widget child, super.key})
: _child = child;
////////////////////////////////////////////////////////////////////////////
// Public API
@override
Widget build(BuildContext context) {
final notificationsCubit = context.read<NotificationsCubit>();
return BlocListener<NotificationsCubit, NotificationsState>(
bloc: notificationsCubit,
listener: (context, state) {
if (state.queue.isNotEmpty) {
final queue = notificationsCubit.popAll();
for (final notificationItem in queue) {
switch (notificationItem.type) {
case NotificationType.info:
_info(
context: context,
text: notificationItem.text,
title: notificationItem.title);
case NotificationType.error:
_error(
context: context,
text: notificationItem.text,
title: notificationItem.title);
}
}
}
},
child: _child);
}
////////////////////////////////////////////////////////////////////////////
// Private Implementation
void _info(
{required BuildContext context, required String text, String? title}) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
MotionToast(
title: title != null ? Text(title) : null,
description: Text(text),
constraints: BoxConstraints.loose(const Size(400, 100)),
contentPadding: const EdgeInsets.all(16),
primaryColor: scale.tertiaryScale.elementBackground,
secondaryColor: scale.tertiaryScale.calloutBackground,
borderRadius: 12 * scaleConfig.borderRadiusScale,
toastDuration: const Duration(seconds: 2),
animationDuration: const Duration(milliseconds: 500),
displayBorder: scaleConfig.useVisualIndicators,
icon: Icons.info,
).show(context);
}
void _error(
{required BuildContext context, required String text, String? title}) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
MotionToast(
title: title != null ? Text(title) : null,
description: Text(text),
constraints: BoxConstraints.loose(const Size(400, 100)),
contentPadding: const EdgeInsets.all(16),
primaryColor: scale.errorScale.elementBackground,
secondaryColor: scale.errorScale.calloutBackground,
borderRadius: 12 * scaleConfig.borderRadiusScale,
toastDuration: const Duration(seconds: 4),
animationDuration: const Duration(milliseconds: 1000),
displayBorder: scaleConfig.useVisualIndicators,
icon: Icons.error,
).show(context);
}
////////////////////////////////////////////////////////////////////////////
final Widget _child;
}

View File

@ -15,6 +15,7 @@ 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';
import '../views/router_shell.dart';
part 'router_cubit.freezed.dart'; part 'router_cubit.freezed.dart';
part 'router_cubit.g.dart'; part 'router_cubit.g.dart';
@ -58,42 +59,47 @@ class RouterCubit extends Cubit<RouterState> {
/// Our application routes /// Our application routes
List<RouteBase> get routes => [ List<RouteBase> get routes => [
GoRoute( ShellRoute(
path: '/', builder: (context, state, child) => RouterShell(child: child),
builder: (context, state) => const HomeScreen(), routes: [
), GoRoute(
GoRoute( path: '/',
path: '/edit_account', builder: (context, state) => const HomeScreen(),
builder: (context, state) { ),
final extra = state.extra! as List<Object?>; GoRoute(
return EditAccountPage( path: '/edit_account',
superIdentityRecordKey: extra[0]! as TypedKey, builder: (context, state) {
existingProfile: extra[1]! as proto.Profile, final extra = state.extra! as List<Object?>;
accountRecord: extra[2]! as OwnedDHTRecordPointer, return EditAccountPage(
); superIdentityRecordKey: extra[0]! as TypedKey,
}, existingProfile: extra[1]! as proto.Profile,
), accountRecord: extra[2]! as OwnedDHTRecordPointer,
GoRoute( );
path: '/new_account', },
builder: (context, state) => const NewAccountPage(), ),
), GoRoute(
GoRoute( path: '/new_account',
path: '/new_account/recovery_key', builder: (context, state) => const NewAccountPage(),
builder: (context, state) { ),
final extra = state.extra! as List<Object?>; GoRoute(
path: '/new_account/recovery_key',
builder: (context, state) {
final extra = state.extra! as List<Object?>;
return ShowRecoveryKeyPage( return ShowRecoveryKeyPage(
writableSuperIdentity: extra[0]! as WritableSuperIdentity, writableSuperIdentity:
name: extra[1]! as String); extra[0]! as WritableSuperIdentity,
}), name: extra[1]! as String);
GoRoute( }),
path: '/settings', GoRoute(
builder: (context, state) => const SettingsPage(), path: '/settings',
), builder: (context, state) => const SettingsPage(),
GoRoute( ),
path: '/developer', GoRoute(
builder: (context, state) => const DeveloperPage(), path: '/developer',
) builder: (context, state) => const DeveloperPage(),
)
])
]; ];
/// Redirects when our state changes /// Redirects when our state changes

View File

@ -1 +1,2 @@
export 'cubit/router_cubit.dart'; export 'cubits/router_cubit.dart';
export 'views/router_shell.dart';

View File

@ -0,0 +1,12 @@
import 'package:flutter/widgets.dart';
import '../../notifications/notifications.dart';
class RouterShell extends StatelessWidget {
const RouterShell({required Widget child, super.key}) : _child = child;
@override
Widget build(BuildContext context) => NotificationsWidget(child: _child);
final Widget _child;
}

View File

@ -8,7 +8,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:motion_toast/motion_toast.dart';
import 'package:quickalert/quickalert.dart'; import 'package:quickalert/quickalert.dart';
import 'package:sliver_expandable/sliver_expandable.dart'; import 'package:sliver_expandable/sliver_expandable.dart';
@ -132,46 +131,6 @@ Future<void> showErrorModal(
); );
} }
void showErrorToast(BuildContext context, String message) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
MotionToast(
//title: Text(translate('toast.error')),
description: Text(message),
constraints: BoxConstraints.loose(const Size(400, 100)),
contentPadding: const EdgeInsets.all(16),
primaryColor: scale.errorScale.elementBackground,
secondaryColor: scale.errorScale.calloutBackground,
borderRadius: 12 * scaleConfig.borderRadiusScale,
toastDuration: const Duration(seconds: 4),
animationDuration: const Duration(milliseconds: 1000),
displayBorder: scaleConfig.useVisualIndicators,
icon: Icons.error,
).show(context);
}
void showInfoToast(BuildContext context, String message) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
MotionToast(
//title: Text(translate('toast.info')),
description: Text(message),
constraints: BoxConstraints.loose(const Size(400, 100)),
contentPadding: const EdgeInsets.all(16),
primaryColor: scale.tertiaryScale.elementBackground,
secondaryColor: scale.tertiaryScale.calloutBackground,
borderRadius: 12 * scaleConfig.borderRadiusScale,
toastDuration: const Duration(seconds: 2),
animationDuration: const Duration(milliseconds: 500),
displayBorder: scaleConfig.useVisualIndicators,
icon: Icons.info,
).show(context);
}
SliverAppBar styledSliverAppBar( SliverAppBar styledSliverAppBar(
{required BuildContext context, required String title, Color? titleColor}) { {required BuildContext context, required String title, Color? titleColor}) {
final theme = Theme.of(context); final theme = Theme.of(context);

View File

@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:loggy/loggy.dart'; import 'package:loggy/loggy.dart';
@ -16,6 +17,7 @@ import 'package:veilid_support/veilid_support.dart';
import 'package:xterm/xterm.dart'; import 'package:xterm/xterm.dart';
import '../../layout/layout.dart'; import '../../layout/layout.dart';
import '../../notifications/notifications.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import 'history_text_editing_controller.dart'; import 'history_text_editing_controller.dart';
@ -133,7 +135,9 @@ class _DeveloperPageState extends State<DeveloperPage> {
Future<void> clear(BuildContext context) async { Future<void> clear(BuildContext context) async {
globalDebugTerminal.buffer.clear(); globalDebugTerminal.buffer.clear();
if (context.mounted) { if (context.mounted) {
showInfoToast(context, translate('developer.cleared')); context
.read<NotificationsCubit>()
.info(text: translate('developer.cleared'));
} }
} }
@ -144,7 +148,9 @@ class _DeveloperPageState extends State<DeveloperPage> {
_terminalController.clearSelection(); _terminalController.clearSelection();
await Clipboard.setData(ClipboardData(text: text)); await Clipboard.setData(ClipboardData(text: text));
if (context.mounted) { if (context.mounted) {
showInfoToast(context, translate('developer.copied')); context
.read<NotificationsCubit>()
.info(text: translate('developer.copied'));
} }
} }
} }
@ -153,7 +159,9 @@ class _DeveloperPageState extends State<DeveloperPage> {
final text = globalDebugTerminal.buffer.getText(); final text = globalDebugTerminal.buffer.getText();
await Clipboard.setData(ClipboardData(text: text)); await Clipboard.setData(ClipboardData(text: text));
if (context.mounted) { if (context.mounted) {
showInfoToast(context, translate('developer.copied_all')); context
.read<NotificationsCubit>()
.info(text: translate('developer.copied_all'));
} }
} }

View File

@ -25,7 +25,9 @@ mixin _$IdentityInstance {
throw _privateConstructorUsedError; // Public key of identity instance throw _privateConstructorUsedError; // Public key of identity instance
FixedEncodedString43 get publicKey => FixedEncodedString43 get publicKey =>
throw _privateConstructorUsedError; // Secret key of identity instance throw _privateConstructorUsedError; // Secret key of identity instance
// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt // Encrypted with appended salt, key is DeriveSharedSecret(
// password = SuperIdentity.secret,
// salt = publicKey)
// Used to recover accounts without generating a new instance // Used to recover accounts without generating a new instance
@Uint8ListJsonConverter() @Uint8ListJsonConverter()
Uint8List get encryptedSecretKey => Uint8List get encryptedSecretKey =>
@ -179,7 +181,9 @@ class _$IdentityInstanceImpl extends _IdentityInstance {
@override @override
final FixedEncodedString43 publicKey; final FixedEncodedString43 publicKey;
// Secret key of identity instance // Secret key of identity instance
// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt // Encrypted with appended salt, key is DeriveSharedSecret(
// password = SuperIdentity.secret,
// salt = publicKey)
// Used to recover accounts without generating a new instance // Used to recover accounts without generating a new instance
@override @override
@Uint8ListJsonConverter() @Uint8ListJsonConverter()
@ -257,7 +261,9 @@ abstract class _IdentityInstance extends IdentityInstance {
@override // Public key of identity instance @override // Public key of identity instance
FixedEncodedString43 get publicKey; FixedEncodedString43 get publicKey;
@override // Secret key of identity instance @override // Secret key of identity instance
// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt // Encrypted with appended salt, key is DeriveSharedSecret(
// password = SuperIdentity.secret,
// salt = publicKey)
// Used to recover accounts without generating a new instance // Used to recover accounts without generating a new instance
@Uint8ListJsonConverter() @Uint8ListJsonConverter()
Uint8List get encryptedSecretKey; Uint8List get encryptedSecretKey;