mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-09-07 23:31:53 +00:00
contact invitation accept notifications
This commit is contained in:
parent
6080c2f0c6
commit
1455aabe6c
@ -153,6 +153,10 @@
|
||||
"invalid_pin": "Invalid PIN",
|
||||
"invalid_password": "Invalid password"
|
||||
},
|
||||
"waiting_invitation": {
|
||||
"accepted": "Contact invitation accepted from {name}",
|
||||
"reject": "Contact invitation was rejected"
|
||||
},
|
||||
"paste_invitation_dialog": {
|
||||
"title": "Paste Contact Invite",
|
||||
"paste_invite_here": "Paste your contact invite here:",
|
||||
|
@ -11,6 +11,7 @@ import '../../chat_list/chat_list.dart';
|
||||
import '../../contact_invitation/contact_invitation.dart';
|
||||
import '../../contacts/contacts.dart';
|
||||
import '../../conversation/conversation.dart';
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../account_manager.dart';
|
||||
|
||||
@ -146,6 +147,7 @@ class PerAccountCollectionCubit extends Cubit<PerAccountCollectionState> {
|
||||
accountRecordCubit!,
|
||||
contactInvitationListCubit,
|
||||
contactListCubit,
|
||||
_locator<NotificationsCubit>(),
|
||||
));
|
||||
|
||||
// ActiveChatCubit
|
||||
@ -262,13 +264,15 @@ class PerAccountCollectionCubit extends Cubit<PerAccountCollectionState> {
|
||||
AccountInfo,
|
||||
AccountRecordCubit,
|
||||
ContactInvitationListCubit,
|
||||
ContactListCubit
|
||||
ContactListCubit,
|
||||
NotificationsCubit,
|
||||
)>(
|
||||
create: (params) => WaitingInvitationsBlocMapCubit(
|
||||
accountInfo: params.$1,
|
||||
accountRecordCubit: params.$2,
|
||||
contactInvitationListCubit: params.$3,
|
||||
contactListCubit: params.$4,
|
||||
notificationsCubit: params.$5,
|
||||
));
|
||||
final activeChatCubitUpdater =
|
||||
BlocUpdater<ActiveChatCubit, bool>(create: (_) => ActiveChatCubit(null));
|
||||
|
@ -11,6 +11,7 @@ import 'package:protobuf/protobuf.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../layout/default_app_bar.dart';
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../../tools/tools.dart';
|
||||
@ -106,12 +107,14 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
||||
final success = await AccountRepository.instance.deleteLocalAccount(
|
||||
widget.superIdentityRecordKey, widget.accountRecord);
|
||||
if (success && mounted) {
|
||||
showInfoToast(
|
||||
context, translate('edit_account_page.account_removed'));
|
||||
context
|
||||
.read<NotificationsCubit>()
|
||||
.info(text: translate('edit_account_page.account_removed'));
|
||||
GoRouterHelper(context).pop();
|
||||
} else if (mounted) {
|
||||
showErrorToast(
|
||||
context, translate('edit_account_page.failed_to_remove'));
|
||||
context
|
||||
.read<NotificationsCubit>()
|
||||
.error(text: translate('edit_account_page.failed_to_remove'));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@ -172,12 +175,14 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
||||
final success = await AccountRepository.instance.destroyAccount(
|
||||
widget.superIdentityRecordKey, widget.accountRecord);
|
||||
if (success && mounted) {
|
||||
showInfoToast(
|
||||
context, translate('edit_account_page.account_destroyed'));
|
||||
context
|
||||
.read<NotificationsCubit>()
|
||||
.info(text: translate('edit_account_page.account_destroyed'));
|
||||
GoRouterHelper(context).pop();
|
||||
} else if (mounted) {
|
||||
showErrorToast(
|
||||
context, translate('edit_account_page.failed_to_destroy'));
|
||||
context
|
||||
.read<NotificationsCubit>()
|
||||
.error(text: translate('edit_account_page.failed_to_destroy'));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
|
28
lib/app.dart
28
lib/app.dart
@ -1,5 +1,6 @@
|
||||
import 'package:animated_theme_switcher/animated_theme_switcher.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/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -13,6 +14,7 @@ import 'package:veilid_support/veilid_support.dart';
|
||||
import 'account_manager/account_manager.dart';
|
||||
import 'init.dart';
|
||||
import 'layout/splash.dart';
|
||||
import 'notifications/notifications.dart';
|
||||
import 'router/router.dart';
|
||||
import 'settings/settings.dart';
|
||||
import 'theme/theme.dart';
|
||||
@ -24,8 +26,8 @@ class ReloadThemeIntent extends Intent {
|
||||
const ReloadThemeIntent();
|
||||
}
|
||||
|
||||
class AttachDetachThemeIntent extends Intent {
|
||||
const AttachDetachThemeIntent();
|
||||
class AttachDetachIntent extends Intent {
|
||||
const AttachDetachIntent();
|
||||
}
|
||||
|
||||
class VeilidChatApp extends StatelessWidget {
|
||||
@ -55,7 +57,7 @@ class VeilidChatApp extends StatelessWidget {
|
||||
});
|
||||
}
|
||||
|
||||
void _attachDetachTheme(BuildContext context) {
|
||||
void _attachDetach(BuildContext context) {
|
||||
singleFuture(this, () async {
|
||||
if (ProcessorRepository.instance.processorConnectionState.isAttached) {
|
||||
log.info('Detaching');
|
||||
@ -77,14 +79,13 @@ class VeilidChatApp extends StatelessWidget {
|
||||
const ReloadThemeIntent(),
|
||||
LogicalKeySet(
|
||||
LogicalKeyboardKey.alt, LogicalKeyboardKey.keyD):
|
||||
const AttachDetachThemeIntent(),
|
||||
const AttachDetachIntent(),
|
||||
},
|
||||
child: Actions(actions: <Type, Action<Intent>>{
|
||||
ReloadThemeIntent: CallbackAction<ReloadThemeIntent>(
|
||||
onInvoke: (intent) => _reloadTheme(context)),
|
||||
AttachDetachThemeIntent:
|
||||
CallbackAction<AttachDetachThemeIntent>(
|
||||
onInvoke: (intent) => _attachDetachTheme(context)),
|
||||
AttachDetachIntent: CallbackAction<AttachDetachIntent>(
|
||||
onInvoke: (intent) => _attachDetach(context)),
|
||||
}, child: Focus(autofocus: true, child: builder(context)))));
|
||||
|
||||
@override
|
||||
@ -101,10 +102,17 @@ class VeilidChatApp extends StatelessWidget {
|
||||
final localizationDelegate = LocalizedApp.of(context).delegate;
|
||||
return ThemeProvider(
|
||||
initTheme: initialThemeData,
|
||||
builder: (_, theme) => LocalizationProvider(
|
||||
builder: (context, theme) => LocalizationProvider(
|
||||
state: LocalizationProvider.of(context).state,
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<PreferencesCubit>(
|
||||
create: (context) =>
|
||||
PreferencesCubit(PreferencesRepository.instance),
|
||||
),
|
||||
BlocProvider<NotificationsCubit>(
|
||||
create: (context) => NotificationsCubit(
|
||||
const NotificationsState(queue: IList.empty()))),
|
||||
BlocProvider<ConnectionStateCubit>(
|
||||
create: (context) =>
|
||||
ConnectionStateCubit(ProcessorRepository.instance)),
|
||||
@ -124,10 +132,6 @@ class VeilidChatApp extends StatelessWidget {
|
||||
create: (context) =>
|
||||
ActiveLocalAccountCubit(AccountRepository.instance),
|
||||
),
|
||||
BlocProvider<PreferencesCubit>(
|
||||
create: (context) =>
|
||||
PreferencesCubit(PreferencesRepository.instance),
|
||||
),
|
||||
BlocProvider<PerAccountCollectionBlocMapCubit>(
|
||||
create: (context) => PerAccountCollectionBlocMapCubit(
|
||||
accountRepository: AccountRepository.instance,
|
||||
|
@ -13,6 +13,7 @@ import 'package:veilid_support/veilid_support.dart';
|
||||
import '../../account_manager/account_manager.dart';
|
||||
import '../../contacts/contacts.dart';
|
||||
import '../../conversation/conversation.dart';
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../theme/theme.dart';
|
||||
import '../chat.dart';
|
||||
|
||||
@ -27,10 +28,10 @@ class ChatComponentWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
final textTheme = theme.textTheme;
|
||||
// final theme = Theme.of(context);
|
||||
// final scale = theme.extension<ScaleScheme>()!;
|
||||
// final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
// final textTheme = theme.textTheme;
|
||||
|
||||
// Get the account info
|
||||
final accountInfo = context.watch<AccountInfoCubit>().state;
|
||||
@ -221,14 +222,15 @@ class ChatComponentWidget extends StatelessWidget {
|
||||
onSendPressed: (pt) {
|
||||
try {
|
||||
if (!messageIsValid) {
|
||||
showErrorToast(context,
|
||||
translate('chat.message_too_long'));
|
||||
context.read<NotificationsCubit>().error(
|
||||
text:
|
||||
translate('chat.message_too_long'));
|
||||
return;
|
||||
}
|
||||
_handleSendPressed(chatComponentCubit, pt);
|
||||
} on FormatException {
|
||||
showErrorToast(context,
|
||||
translate('chat.message_too_long'));
|
||||
context.read<NotificationsCubit>().error(
|
||||
text: translate('chat.message_too_long'));
|
||||
}
|
||||
},
|
||||
listBottomWidget: messageIsValid
|
||||
@ -267,8 +269,11 @@ class ChatComponentWidget extends StatelessWidget {
|
||||
ChatComponentCubit chatComponentCubit,
|
||||
WindowState<types.Message> messageWindow,
|
||||
ScrollNotification notification) async {
|
||||
print(
|
||||
'_handlePageForward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification');
|
||||
debugPrint(
|
||||
'_handlePageForward: messagesState.length=${messageWindow.length} '
|
||||
'messagesState.windowTail=${messageWindow.windowTail} '
|
||||
'messagesState.windowCount=${messageWindow.windowCount} '
|
||||
'ScrollNotification=$notification');
|
||||
|
||||
// Go forward a page
|
||||
final tail = min(messageWindow.length,
|
||||
@ -299,8 +304,11 @@ class ChatComponentWidget extends StatelessWidget {
|
||||
WindowState<types.Message> messageWindow,
|
||||
ScrollNotification notification,
|
||||
) async {
|
||||
print(
|
||||
'_handlePageBackward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification');
|
||||
debugPrint(
|
||||
'_handlePageBackward: messagesState.length=${messageWindow.length} '
|
||||
'messagesState.windowTail=${messageWindow.windowTail} '
|
||||
'messagesState.windowCount=${messageWindow.windowCount} '
|
||||
'ScrollNotification=$notification');
|
||||
|
||||
// Go back a page
|
||||
final tail = max(
|
||||
|
@ -54,7 +54,7 @@ class ContactInvitationListCubit
|
||||
return dhtRecord;
|
||||
}
|
||||
|
||||
Future<Uint8List> createInvitation(
|
||||
Future<(Uint8List, TypedKey)> createInvitation(
|
||||
{required proto.Profile profile,
|
||||
required EncryptionKeyType encryptionKeyType,
|
||||
required String encryptionKey,
|
||||
@ -82,6 +82,7 @@ class ContactInvitationListCubit
|
||||
// to and it will be eventually encrypted with the DH of the contact's
|
||||
// identity key
|
||||
late final Uint8List signedContactInvitationBytes;
|
||||
late final TypedKey contactRequestInboxKey;
|
||||
await (await pool.createRecord(
|
||||
debugName: 'ContactInvitationListCubit::createInvitation::'
|
||||
'LocalConversation',
|
||||
@ -119,6 +120,9 @@ class ContactInvitationListCubit
|
||||
]),
|
||||
crypto: const VeilidCryptoPublic()))
|
||||
.deleteScope((contactRequestInbox) async {
|
||||
// Keep the contact request inbox key
|
||||
contactRequestInboxKey = contactRequestInbox.key;
|
||||
|
||||
// Store ContactRequest in owner subkey
|
||||
await contactRequestInbox.eventualWriteProtobuf(creq);
|
||||
// Store an empty invitation response
|
||||
@ -158,7 +162,7 @@ class ContactInvitationListCubit
|
||||
});
|
||||
});
|
||||
|
||||
return signedContactInvitationBytes;
|
||||
return (signedContactInvitationBytes, contactRequestInboxKey);
|
||||
}
|
||||
|
||||
Future<void> deleteInvitation(
|
||||
|
@ -1,8 +1,9 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
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.value(super.v) : super.value();
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import 'package:async_tools/async_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 '../../account_manager/account_manager.dart';
|
||||
import '../../contacts/contacts.dart';
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import 'cubits.dart';
|
||||
|
||||
@ -22,11 +24,13 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||
{required AccountInfo accountInfo,
|
||||
required AccountRecordCubit accountRecordCubit,
|
||||
required ContactInvitationListCubit contactInvitationListCubit,
|
||||
required ContactListCubit contactListCubit})
|
||||
required ContactListCubit contactListCubit,
|
||||
required NotificationsCubit notificationsCubit})
|
||||
: _accountInfo = accountInfo,
|
||||
_accountRecordCubit = accountRecordCubit,
|
||||
_contactInvitationListCubit = contactInvitationListCubit,
|
||||
_contactListCubit = contactListCubit {
|
||||
_contactListCubit = contactListCubit,
|
||||
_notificationsCubit = notificationsCubit {
|
||||
// React to invitation status changes
|
||||
_singleInvitationStatusProcessor.follow(
|
||||
stream, state, _invitationStatusListener);
|
||||
@ -81,11 +85,22 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||
localConversationRecordKey:
|
||||
acceptedContact.localConversationRecordKey,
|
||||
);
|
||||
|
||||
// Notify about acceptance
|
||||
_notificationsCubit.info(
|
||||
text: translate('waiting_invitation.accepted',
|
||||
args: {'name': acceptedContact.remoteProfile.name}));
|
||||
} else {
|
||||
// Reject
|
||||
await _contactInvitationListCubit.deleteInvitation(
|
||||
accepted: false,
|
||||
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 ContactInvitationListCubit _contactInvitationListCubit;
|
||||
final ContactListCubit _contactListCubit;
|
||||
final NotificationsCubit _notificationsCubit;
|
||||
final _singleInvitationStatusProcessor =
|
||||
SingleStateProcessor<WaitingInvitationsBlocMapState>();
|
||||
}
|
||||
|
@ -7,19 +7,22 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../contact_invitation.dart';
|
||||
|
||||
class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
const ContactInvitationDisplayDialog._({
|
||||
required this.modalContext,
|
||||
required this.locator,
|
||||
required this.message,
|
||||
});
|
||||
|
||||
final BuildContext modalContext;
|
||||
final Locator locator;
|
||||
final String message;
|
||||
|
||||
@override
|
||||
@ -27,7 +30,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('message', message))
|
||||
..add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
|
||||
..add(DiagnosticsProperty<Locator>('locator', locator));
|
||||
}
|
||||
|
||||
String makeTextInvite(String message, Uint8List data) {
|
||||
@ -48,72 +51,87 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
final textTheme = theme.textTheme;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
final signedContactInvitationBytesV =
|
||||
context.watch<InvitationGeneratorCubit>().state;
|
||||
final generatorOutputV = context.watch<InvitationGeneratorCubit>().state;
|
||||
|
||||
final cardsize =
|
||||
min<double>(MediaQuery.of(context).size.shortestSide - 48.0, 400);
|
||||
|
||||
return PopControl(
|
||||
dismissible: !signedContactInvitationBytesV.isLoading,
|
||||
child: Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: const BorderSide(width: 2),
|
||||
borderRadius:
|
||||
BorderRadius.circular(16 * scaleConfig.borderRadiusScale)),
|
||||
backgroundColor: Colors.white,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: cardsize,
|
||||
maxWidth: cardsize,
|
||||
minHeight: cardsize,
|
||||
maxHeight: cardsize),
|
||||
child: signedContactInvitationBytesV.when(
|
||||
loading: buildProgressIndicator,
|
||||
data: (data) => Column(children: [
|
||||
FittedBox(
|
||||
child: Text(
|
||||
translate(
|
||||
'create_invitation_dialog.contact_invitation'),
|
||||
style: textTheme.headlineSmall!
|
||||
.copyWith(color: Colors.black)))
|
||||
.paddingAll(8),
|
||||
FittedBox(
|
||||
child: QrImageView.withQr(
|
||||
size: 300,
|
||||
qr: QrCode.fromUint8List(
|
||||
data: data,
|
||||
errorCorrectLevel:
|
||||
QrErrorCorrectLevel.L)))
|
||||
.expanded(),
|
||||
Text(message,
|
||||
softWrap: true,
|
||||
style: textTheme.labelLarge!
|
||||
.copyWith(color: Colors.black))
|
||||
.paddingAll(8),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.copy),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.black,
|
||||
backgroundColor: Colors.white,
|
||||
side: const BorderSide()),
|
||||
label: Text(translate(
|
||||
'create_invitation_dialog.copy_invitation')),
|
||||
onPressed: () async {
|
||||
showInfoToast(
|
||||
context,
|
||||
translate(
|
||||
'create_invitation_dialog.invitation_copied'));
|
||||
await Clipboard.setData(ClipboardData(
|
||||
text: makeTextInvite(message, data)));
|
||||
},
|
||||
).paddingAll(16),
|
||||
]),
|
||||
error: errorPage))));
|
||||
return BlocListener<ContactInvitationListCubit,
|
||||
ContactInvitiationListState>(
|
||||
bloc: locator<ContactInvitationListCubit>(),
|
||||
listener: (context, state) {
|
||||
final listState = state.state.asData?.value;
|
||||
final data = generatorOutputV.asData?.value;
|
||||
|
||||
if (listState != null && data != null) {
|
||||
final idx = listState.indexWhere((x) =>
|
||||
x.value.contactRequestInbox.recordKey.toVeilid() == data.$2);
|
||||
if (idx == -1) {
|
||||
// This invitation is gone, close it
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: PopControl(
|
||||
dismissible: !generatorOutputV.isLoading,
|
||||
child: Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: const BorderSide(width: 2),
|
||||
borderRadius: BorderRadius.circular(
|
||||
16 * scaleConfig.borderRadiusScale)),
|
||||
backgroundColor: Colors.white,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: cardsize,
|
||||
maxWidth: cardsize,
|
||||
minHeight: cardsize,
|
||||
maxHeight: cardsize),
|
||||
child: generatorOutputV.when(
|
||||
loading: buildProgressIndicator,
|
||||
data: (data) => Column(children: [
|
||||
FittedBox(
|
||||
child: Text(
|
||||
translate('create_invitation_dialog'
|
||||
'.contact_invitation'),
|
||||
style: textTheme.headlineSmall!
|
||||
.copyWith(color: Colors.black)))
|
||||
.paddingAll(8),
|
||||
FittedBox(
|
||||
child: QrImageView.withQr(
|
||||
size: 300,
|
||||
qr: QrCode.fromUint8List(
|
||||
data: data.$1,
|
||||
errorCorrectLevel:
|
||||
QrErrorCorrectLevel.L)))
|
||||
.expanded(),
|
||||
Text(message,
|
||||
softWrap: true,
|
||||
style: textTheme.labelLarge!
|
||||
.copyWith(color: Colors.black))
|
||||
.paddingAll(8),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.copy),
|
||||
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(
|
||||
{required BuildContext context,
|
||||
required Locator locator,
|
||||
required InvitationGeneratorCubit Function(BuildContext) create,
|
||||
required String message}) async {
|
||||
await showPopControlDialog<void>(
|
||||
@ -121,7 +139,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
builder: (context) => BlocProvider(
|
||||
create: create,
|
||||
child: ContactInvitationDisplayDialog._(
|
||||
modalContext: context,
|
||||
locator: locator,
|
||||
message: message,
|
||||
)));
|
||||
}
|
||||
|
@ -52,9 +52,13 @@ class ContactInvitationItemWidget extends StatelessWidget {
|
||||
}
|
||||
await ContactInvitationDisplayDialog.show(
|
||||
context: context,
|
||||
locator: context.read,
|
||||
message: contactInvitationRecord.message,
|
||||
create: (context) => InvitationGeneratorCubit.value(
|
||||
Uint8List.fromList(contactInvitationRecord.invitation)));
|
||||
create: (context) => InvitationGeneratorCubit.value((
|
||||
Uint8List.fromList(contactInvitationRecord.invitation),
|
||||
contactInvitationRecord.contactRequestInbox.recordKey
|
||||
.toVeilid()
|
||||
)));
|
||||
},
|
||||
endActions: [
|
||||
SliderTileAction(
|
||||
|
@ -5,16 +5,17 @@ import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../account_manager/account_manager.dart';
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../theme/theme.dart';
|
||||
import '../contact_invitation.dart';
|
||||
|
||||
class CreateInvitationDialog extends StatefulWidget {
|
||||
const CreateInvitationDialog._({required this.modalContext});
|
||||
const CreateInvitationDialog._({required this.locator});
|
||||
|
||||
@override
|
||||
CreateInvitationDialogState createState() => CreateInvitationDialogState();
|
||||
@ -23,16 +24,15 @@ class CreateInvitationDialog extends StatefulWidget {
|
||||
await StyledDialog.show<void>(
|
||||
context: context,
|
||||
title: translate('create_invitation_dialog.title'),
|
||||
child: CreateInvitationDialog._(modalContext: context));
|
||||
child: CreateInvitationDialog._(locator: context.read));
|
||||
}
|
||||
|
||||
final BuildContext modalContext;
|
||||
final Locator locator;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
.add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
|
||||
properties.add(DiagnosticsProperty<Locator>('locator', locator));
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,8 +86,8 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
showErrorToast(
|
||||
context, translate('create_invitation_dialog.pin_does_not_match'));
|
||||
context.read<NotificationsCubit>().error(
|
||||
text: translate('create_invitation_dialog.pin_does_not_match'));
|
||||
setState(() {
|
||||
_encryptionKeyType = EncryptionKeyType.none;
|
||||
_encryptionKey = '';
|
||||
@ -124,8 +124,8 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
showErrorToast(context,
|
||||
translate('create_invitation_dialog.password_does_not_match'));
|
||||
context.read<NotificationsCubit>().error(
|
||||
text: translate('create_invitation_dialog.password_does_not_match'));
|
||||
setState(() {
|
||||
_encryptionKeyType = EncryptionKeyType.none;
|
||||
_encryptionKey = '';
|
||||
@ -138,13 +138,9 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
|
||||
// Start generation
|
||||
final contactInvitationListCubit =
|
||||
widget.modalContext.read<ContactInvitationListCubit>();
|
||||
final profile = widget.modalContext
|
||||
.read<AccountRecordCubit>()
|
||||
.state
|
||||
.asData
|
||||
?.value
|
||||
.profile;
|
||||
widget.locator<ContactInvitationListCubit>();
|
||||
final profile =
|
||||
widget.locator<AccountRecordCubit>().state.asData?.value.profile;
|
||||
if (profile == null) {
|
||||
return;
|
||||
}
|
||||
@ -158,6 +154,7 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
|
||||
await ContactInvitationDisplayDialog.show(
|
||||
context: context,
|
||||
locator: widget.locator,
|
||||
message: _messageTextController.text,
|
||||
create: (context) => InvitationGeneratorCubit(generator));
|
||||
|
||||
|
@ -9,6 +9,7 @@ import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../account_manager/account_manager.dart';
|
||||
import '../../contacts/contacts.dart';
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../theme/theme.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import '../contact_invitation.dart';
|
||||
@ -102,7 +103,9 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||
}
|
||||
} else {
|
||||
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
|
||||
} else {
|
||||
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');
|
||||
}
|
||||
if (mounted) {
|
||||
showErrorToast(context, errorText);
|
||||
context.read<NotificationsCubit>().error(text: errorText);
|
||||
}
|
||||
setState(() {
|
||||
_isValidating = false;
|
||||
@ -233,7 +238,7 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||
errorText = translate('invitation_dialog.invalid_invitation');
|
||||
}
|
||||
if (mounted) {
|
||||
showErrorToast(context, errorText);
|
||||
context.read<NotificationsCubit>().error(text: errorText);
|
||||
}
|
||||
setState(() {
|
||||
_isValidating = false;
|
||||
|
@ -12,6 +12,7 @@ import 'package:pasteboard/pasteboard.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../theme/theme.dart';
|
||||
import 'invitation_dialog.dart';
|
||||
|
||||
@ -269,13 +270,18 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
|
||||
));
|
||||
} on MobileScannerException catch (e) {
|
||||
if (e.errorCode == MobileScannerErrorCode.permissionDenied) {
|
||||
showErrorToast(
|
||||
context, translate('scan_invitation_dialog.permission_error'));
|
||||
context
|
||||
.read<NotificationsCubit>()
|
||||
.error(text: translate('scan_invitation_dialog.permission_error'));
|
||||
} else {
|
||||
showErrorToast(context, translate('scan_invitation_dialog.error'));
|
||||
context
|
||||
.read<NotificationsCubit>()
|
||||
.error(text: translate('scan_invitation_dialog.error'));
|
||||
}
|
||||
} on Exception catch (_) {
|
||||
showErrorToast(context, translate('scan_invitation_dialog.error'));
|
||||
context
|
||||
.read<NotificationsCubit>()
|
||||
.error(text: translate('scan_invitation_dialog.error'));
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -285,8 +291,9 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
|
||||
final imageBytes = await Pasteboard.image;
|
||||
if (imageBytes == null) {
|
||||
if (context.mounted) {
|
||||
showErrorToast(
|
||||
context, translate('scan_invitation_dialog.not_an_image'));
|
||||
context
|
||||
.read<NotificationsCubit>()
|
||||
.error(text: translate('scan_invitation_dialog.not_an_image'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -294,8 +301,8 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
|
||||
final image = img.decodeImage(imageBytes);
|
||||
if (image == null) {
|
||||
if (context.mounted) {
|
||||
showErrorToast(context,
|
||||
translate('scan_invitation_dialog.could_not_decode_image'));
|
||||
context.read<NotificationsCubit>().error(
|
||||
text: translate('scan_invitation_dialog.could_not_decode_image'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -319,8 +326,8 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
|
||||
return Uint8List.fromList(segs[0].toList());
|
||||
} on Exception catch (_) {
|
||||
if (context.mounted) {
|
||||
showErrorToast(
|
||||
context, translate('scan_invitation_dialog.not_a_valid_qr_code'));
|
||||
context.read<NotificationsCubit>().error(
|
||||
text: translate('scan_invitation_dialog.not_a_valid_qr_code'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -1,9 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.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 '../tools/tools.dart';
|
||||
|
26
lib/notifications/cubits/notifications_cubit.dart
Normal file
26
lib/notifications/cubits/notifications_cubit.dart
Normal 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;
|
||||
}
|
||||
}
|
23
lib/notifications/models/notifications_state.dart
Normal file
23
lib/notifications/models/notifications_state.dart
Normal 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;
|
||||
}
|
290
lib/notifications/models/notifications_state.freezed.dart
Normal file
290
lib/notifications/models/notifications_state.freezed.dart
Normal 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;
|
||||
}
|
3
lib/notifications/notifications.dart
Normal file
3
lib/notifications/notifications.dart
Normal file
@ -0,0 +1,3 @@
|
||||
export 'cubits/notifications_cubit.dart';
|
||||
export 'models/notifications_state.dart';
|
||||
export 'views/notifications_widget.dart';
|
91
lib/notifications/views/notifications_widget.dart
Normal file
91
lib/notifications/views/notifications_widget.dart
Normal 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;
|
||||
}
|
@ -15,6 +15,7 @@ import '../../proto/proto.dart' as proto;
|
||||
import '../../settings/settings.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_processor/views/developer.dart';
|
||||
import '../views/router_shell.dart';
|
||||
|
||||
part 'router_cubit.freezed.dart';
|
||||
part 'router_cubit.g.dart';
|
||||
@ -58,42 +59,47 @@ class RouterCubit extends Cubit<RouterState> {
|
||||
|
||||
/// Our application routes
|
||||
List<RouteBase> get routes => [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/edit_account',
|
||||
builder: (context, state) {
|
||||
final extra = state.extra! as List<Object?>;
|
||||
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(
|
||||
path: '/new_account/recovery_key',
|
||||
builder: (context, state) {
|
||||
final extra = state.extra! as List<Object?>;
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => RouterShell(child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/edit_account',
|
||||
builder: (context, state) {
|
||||
final extra = state.extra! as List<Object?>;
|
||||
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(
|
||||
path: '/new_account/recovery_key',
|
||||
builder: (context, state) {
|
||||
final extra = state.extra! as List<Object?>;
|
||||
|
||||
return ShowRecoveryKeyPage(
|
||||
writableSuperIdentity: extra[0]! as WritableSuperIdentity,
|
||||
name: extra[1]! as String);
|
||||
}),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
builder: (context, state) => const SettingsPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/developer',
|
||||
builder: (context, state) => const DeveloperPage(),
|
||||
)
|
||||
return ShowRecoveryKeyPage(
|
||||
writableSuperIdentity:
|
||||
extra[0]! as WritableSuperIdentity,
|
||||
name: extra[1]! as String);
|
||||
}),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
builder: (context, state) => const SettingsPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/developer',
|
||||
builder: (context, state) => const DeveloperPage(),
|
||||
)
|
||||
])
|
||||
];
|
||||
|
||||
/// Redirects when our state changes
|
@ -1 +1,2 @@
|
||||
export 'cubit/router_cubit.dart';
|
||||
export 'cubits/router_cubit.dart';
|
||||
export 'views/router_shell.dart';
|
||||
|
12
lib/router/views/router_shell.dart
Normal file
12
lib/router/views/router_shell.dart
Normal 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;
|
||||
}
|
@ -8,7 +8,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||
import 'package:motion_toast/motion_toast.dart';
|
||||
import 'package:quickalert/quickalert.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(
|
||||
{required BuildContext context, required String title, Color? titleColor}) {
|
||||
final theme = Theme.of(context);
|
||||
|
@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:loggy/loggy.dart';
|
||||
@ -16,6 +17,7 @@ import 'package:veilid_support/veilid_support.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
|
||||
import '../../layout/layout.dart';
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../theme/theme.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import 'history_text_editing_controller.dart';
|
||||
@ -133,7 +135,9 @@ class _DeveloperPageState extends State<DeveloperPage> {
|
||||
Future<void> clear(BuildContext context) async {
|
||||
globalDebugTerminal.buffer.clear();
|
||||
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();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
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();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (context.mounted) {
|
||||
showInfoToast(context, translate('developer.copied_all'));
|
||||
context
|
||||
.read<NotificationsCubit>()
|
||||
.info(text: translate('developer.copied_all'));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,9 @@ mixin _$IdentityInstance {
|
||||
throw _privateConstructorUsedError; // Public key of identity instance
|
||||
FixedEncodedString43 get publicKey =>
|
||||
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
|
||||
@Uint8ListJsonConverter()
|
||||
Uint8List get encryptedSecretKey =>
|
||||
@ -179,7 +181,9 @@ class _$IdentityInstanceImpl extends _IdentityInstance {
|
||||
@override
|
||||
final FixedEncodedString43 publicKey;
|
||||
// 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
|
||||
@override
|
||||
@Uint8ListJsonConverter()
|
||||
@ -257,7 +261,9 @@ abstract class _IdentityInstance extends IdentityInstance {
|
||||
@override // Public key of identity instance
|
||||
FixedEncodedString43 get publicKey;
|
||||
@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
|
||||
@Uint8ListJsonConverter()
|
||||
Uint8List get encryptedSecretKey;
|
||||
|
Loading…
Reference in New Issue
Block a user