mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-10-01 06:55:46 -04:00
contact invitation accept notifications
This commit is contained in:
parent
6080c2f0c6
commit
1455aabe6c
@ -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:",
|
||||||
|
@ -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));
|
||||||
|
@ -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) {
|
||||||
|
28
lib/app.dart
28
lib/app.dart
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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>();
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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));
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
|
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 '../../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
|
@ -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_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);
|
||||||
|
@ -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'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user