mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-06-25 14:50:34 -04:00
break everything
This commit is contained in:
parent
e898074387
commit
29210c89d2
121 changed files with 2892 additions and 2608 deletions
55
lib/old_to_refactor/components/account_bubble.dart
Normal file
55
lib/old_to_refactor/components/account_bubble.dart
Normal file
|
@ -0,0 +1,55 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:circular_profile_avatar/circular_profile_avatar.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../entities/local_account.dart';
|
||||
import '../providers/logins.dart';
|
||||
|
||||
class AccountBubble extends ConsumerWidget {
|
||||
const AccountBubble({required this.account, super.key});
|
||||
final LocalAccount account;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||
final logins = ref.watch(loginsProvider);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: CircularProfileAvatar('',
|
||||
child: Container(color: Theme.of(context).disabledColor))),
|
||||
const Expanded(child: Text('Placeholder'))
|
||||
]));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<LocalAccount>('account', account));
|
||||
}
|
||||
}
|
||||
|
||||
class AddAccountBubble extends ConsumerWidget {
|
||||
const AddAccountBubble({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||
final logins = ref.watch(loginsProvider);
|
||||
|
||||
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
CircularProfileAvatar('',
|
||||
borderWidth: 4,
|
||||
borderColor: Theme.of(context).unselectedWidgetColor,
|
||||
child: Container(
|
||||
color: Colors.blue, child: const Icon(Icons.add, size: 50))),
|
||||
const Text('Add Account').paddingLTRB(0, 4, 0, 0)
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class BottomSheetActionButton extends ConsumerStatefulWidget {
|
||||
const BottomSheetActionButton(
|
||||
{required this.bottomSheetBuilder,
|
||||
required this.builder,
|
||||
this.foregroundColor,
|
||||
this.backgroundColor,
|
||||
this.shape,
|
||||
super.key});
|
||||
final Color? foregroundColor;
|
||||
final Color? backgroundColor;
|
||||
final ShapeBorder? shape;
|
||||
final Widget Function(BuildContext) builder;
|
||||
final Widget Function(BuildContext) bottomSheetBuilder;
|
||||
|
||||
@override
|
||||
BottomSheetActionButtonState createState() => BottomSheetActionButtonState();
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(ObjectFlagProperty<Widget Function(BuildContext p1)>.has(
|
||||
'bottomSheetBuilder', bottomSheetBuilder))
|
||||
..add(ColorProperty('foregroundColor', foregroundColor))
|
||||
..add(ColorProperty('backgroundColor', backgroundColor))
|
||||
..add(DiagnosticsProperty<ShapeBorder?>('shape', shape))
|
||||
..add(ObjectFlagProperty<Widget? Function(BuildContext p1)>.has(
|
||||
'builder', builder));
|
||||
}
|
||||
}
|
||||
|
||||
class BottomSheetActionButtonState
|
||||
extends ConsumerState<BottomSheetActionButton> {
|
||||
bool _showFab = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
//
|
||||
return _showFab
|
||||
? FloatingActionButton(
|
||||
elevation: 0,
|
||||
hoverElevation: 0,
|
||||
shape: widget.shape,
|
||||
foregroundColor: widget.foregroundColor,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
child: widget.builder(context),
|
||||
onPressed: () async {
|
||||
await showModalBottomSheet<void>(
|
||||
context: context, builder: widget.bottomSheetBuilder);
|
||||
},
|
||||
)
|
||||
: Container();
|
||||
}
|
||||
|
||||
void showFloatingActionButton(bool value) {
|
||||
setState(() {
|
||||
_showFab = value;
|
||||
});
|
||||
}
|
||||
}
|
205
lib/old_to_refactor/components/chat_component.dart
Normal file
205
lib/old_to_refactor/components/chat_component.dart
Normal file
|
@ -0,0 +1,205 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../providers/account.dart';
|
||||
import '../providers/chat.dart';
|
||||
import '../providers/conversation.dart';
|
||||
import '../tools/tools.dart';
|
||||
import '../veilid_init.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
|
||||
class ChatComponent extends ConsumerStatefulWidget {
|
||||
const ChatComponent(
|
||||
{required this.activeAccountInfo,
|
||||
required this.activeChat,
|
||||
required this.activeChatContact,
|
||||
super.key});
|
||||
|
||||
final ActiveAccountInfo activeAccountInfo;
|
||||
final TypedKey activeChat;
|
||||
final proto.Contact activeChatContact;
|
||||
|
||||
@override
|
||||
ChatComponentState createState() => ChatComponentState();
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<ActiveAccountInfo>(
|
||||
'activeAccountInfo', activeAccountInfo))
|
||||
..add(DiagnosticsProperty<TypedKey>('activeChat', activeChat))
|
||||
..add(DiagnosticsProperty<proto.Contact>(
|
||||
'activeChatContact', activeChatContact));
|
||||
}
|
||||
}
|
||||
|
||||
class ChatComponentState extends ConsumerState<ChatComponent> {
|
||||
final _unfocusNode = FocusNode();
|
||||
late final types.User _localUser;
|
||||
late final types.User _remoteUser;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_localUser = types.User(
|
||||
id: widget.activeAccountInfo.localAccount.identityMaster
|
||||
.identityPublicTypedKey()
|
||||
.toString(),
|
||||
firstName: widget.activeAccountInfo.account.profile.name,
|
||||
);
|
||||
_remoteUser = types.User(
|
||||
id: proto.TypedKeyProto.fromProto(
|
||||
widget.activeChatContact.identityPublicKey)
|
||||
.toString(),
|
||||
firstName: widget.activeChatContact.remoteProfile.name);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unfocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
types.Message protoMessageToMessage(proto.Message message) {
|
||||
final isLocal = message.author ==
|
||||
widget.activeAccountInfo.localAccount.identityMaster
|
||||
.identityPublicTypedKey()
|
||||
.toProto();
|
||||
|
||||
final textMessage = types.TextMessage(
|
||||
author: isLocal ? _localUser : _remoteUser,
|
||||
createdAt: (message.timestamp ~/ 1000).toInt(),
|
||||
id: message.timestamp.toString(),
|
||||
text: message.text,
|
||||
);
|
||||
return textMessage;
|
||||
}
|
||||
|
||||
Future<void> _addMessage(proto.Message protoMessage) async {
|
||||
if (protoMessage.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final message = protoMessageToMessage(protoMessage);
|
||||
|
||||
// setState(() {
|
||||
// _messages.insert(0, message);
|
||||
// });
|
||||
|
||||
// Now add the message to the conversation messages
|
||||
final localConversationRecordKey = proto.TypedKeyProto.fromProto(
|
||||
widget.activeChatContact.localConversationRecordKey);
|
||||
final remoteIdentityPublicKey = proto.TypedKeyProto.fromProto(
|
||||
widget.activeChatContact.identityPublicKey);
|
||||
|
||||
await addLocalConversationMessage(
|
||||
activeAccountInfo: widget.activeAccountInfo,
|
||||
localConversationRecordKey: localConversationRecordKey,
|
||||
remoteIdentityPublicKey: remoteIdentityPublicKey,
|
||||
message: protoMessage);
|
||||
|
||||
ref.invalidate(activeConversationMessagesProvider);
|
||||
}
|
||||
|
||||
Future<void> _handleSendPressed(types.PartialText message) async {
|
||||
final protoMessage = proto.Message()
|
||||
..author = widget.activeAccountInfo.localAccount.identityMaster
|
||||
.identityPublicTypedKey()
|
||||
.toProto()
|
||||
..timestamp = (await eventualVeilid.future).now().toInt64()
|
||||
..text = message.text;
|
||||
//..signature = signature;
|
||||
|
||||
await _addMessage(protoMessage);
|
||||
}
|
||||
|
||||
void _handleAttachmentPressed() {
|
||||
//
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final chatTheme = makeChatTheme(scale, textTheme);
|
||||
final contactName = widget.activeChatContact.editedProfile.name;
|
||||
|
||||
final protoMessages =
|
||||
ref.watch(activeConversationMessagesProvider).asData?.value;
|
||||
if (protoMessages == null) {
|
||||
return waitingPage(context);
|
||||
}
|
||||
final messages = <types.Message>[];
|
||||
for (final protoMessage in protoMessages) {
|
||||
final message = protoMessageToMessage(protoMessage);
|
||||
messages.insert(0, message);
|
||||
}
|
||||
|
||||
return DefaultTextStyle(
|
||||
style: textTheme.bodySmall!,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: scale.primaryScale.subtleBorder,
|
||||
),
|
||||
child: Row(children: [
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.fromSTEB(
|
||||
16, 0, 16, 0),
|
||||
child: Text(contactName,
|
||||
textAlign: TextAlign.start,
|
||||
style: textTheme.titleMedium),
|
||||
)),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () async {
|
||||
ref.read(activeChatStateProvider.notifier).state =
|
||||
null;
|
||||
}).paddingLTRB(16, 0, 16, 0)
|
||||
]),
|
||||
),
|
||||
Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(),
|
||||
child: Chat(
|
||||
theme: chatTheme,
|
||||
messages: messages,
|
||||
//onAttachmentPressed: _handleAttachmentPressed,
|
||||
//onMessageTap: _handleMessageTap,
|
||||
//onPreviewDataFetched: _handlePreviewDataFetched,
|
||||
|
||||
onSendPressed: (message) {
|
||||
unawaited(_handleSendPressed(message));
|
||||
},
|
||||
//showUserAvatars: false,
|
||||
//showUserNames: true,
|
||||
user: _localUser,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../providers/account.dart';
|
||||
import '../providers/chat.dart';
|
||||
import '../theme/theme.dart';
|
||||
|
||||
class ChatSingleContactItemWidget extends ConsumerWidget {
|
||||
const ChatSingleContactItemWidget({required this.contact, super.key});
|
||||
|
||||
final proto.Contact contact;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
final activeChat = ref.watch(activeChatStateProvider);
|
||||
final remoteConversationRecordKey =
|
||||
proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey);
|
||||
final selected = activeChat == remoteConversationRecordKey;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(0, 4, 0, 0),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: ShapeDecoration(
|
||||
color: scale.tertiaryScale.subtleBorder,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
)),
|
||||
child: Slidable(
|
||||
key: ObjectKey(contact),
|
||||
endActionPane: ActionPane(
|
||||
motion: const DrawerMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (context) async {
|
||||
final activeAccountInfo =
|
||||
await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo != null) {
|
||||
await deleteChat(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
remoteConversationRecordKey:
|
||||
remoteConversationRecordKey);
|
||||
ref.invalidate(fetchChatListProvider);
|
||||
}
|
||||
},
|
||||
backgroundColor: scale.tertiaryScale.background,
|
||||
foregroundColor: scale.tertiaryScale.text,
|
||||
icon: Icons.delete,
|
||||
label: translate('button.delete'),
|
||||
padding: const EdgeInsets.all(2)),
|
||||
// SlidableAction(
|
||||
// onPressed: (context) => (),
|
||||
// backgroundColor: scale.secondaryScale.background,
|
||||
// foregroundColor: scale.secondaryScale.text,
|
||||
// icon: Icons.edit,
|
||||
// label: 'Edit',
|
||||
// ),
|
||||
],
|
||||
),
|
||||
|
||||
// The child of the Slidable is what the user sees when the
|
||||
// component is not dragged.
|
||||
child: ListTile(
|
||||
onTap: () async {
|
||||
ref.read(activeChatStateProvider.notifier).state =
|
||||
remoteConversationRecordKey;
|
||||
ref.invalidate(fetchChatListProvider);
|
||||
},
|
||||
title: Text(contact.editedProfile.name),
|
||||
|
||||
/// xxx show last message here
|
||||
subtitle: (contact.editedProfile.pronouns.isNotEmpty)
|
||||
? Text(contact.editedProfile.pronouns)
|
||||
: null,
|
||||
iconColor: scale.tertiaryScale.background,
|
||||
textColor: scale.tertiaryScale.text,
|
||||
selected: selected,
|
||||
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
|
||||
leading: const Icon(Icons.chat))));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<proto.Contact>('contact', contact));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:searchable_listview/searchable_listview.dart';
|
||||
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../tools/tools.dart';
|
||||
import 'chat_single_contact_item_widget.dart';
|
||||
import 'empty_chat_list_widget.dart';
|
||||
|
||||
class ChatSingleContactListWidget extends ConsumerWidget {
|
||||
ChatSingleContactListWidget(
|
||||
{required IList<proto.Contact> contactList,
|
||||
required this.chatList,
|
||||
super.key})
|
||||
: contactMap = IMap.fromIterable(contactList,
|
||||
keyMapper: (c) => c.remoteConversationRecordKey,
|
||||
valueMapper: (c) => c);
|
||||
|
||||
final IMap<proto.TypedKey, proto.Contact> contactMap;
|
||||
final IList<proto.Chat> chatList;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return SizedBox.expand(
|
||||
child: styledTitleContainer(
|
||||
context: context,
|
||||
title: translate('chat_list.chats'),
|
||||
child: SizedBox.expand(
|
||||
child: (chatList.isEmpty)
|
||||
? const EmptyChatListWidget()
|
||||
: SearchableList<proto.Chat>(
|
||||
autoFocusOnSearch: false,
|
||||
initialList: chatList.toList(),
|
||||
builder: (l, i, c) {
|
||||
final contact =
|
||||
contactMap[c.remoteConversationKey];
|
||||
if (contact == null) {
|
||||
return const Text('...');
|
||||
}
|
||||
return ChatSingleContactItemWidget(
|
||||
contact: contact);
|
||||
},
|
||||
filter: (value) {
|
||||
final lowerValue = value.toLowerCase();
|
||||
return chatList.where((c) {
|
||||
final contact =
|
||||
contactMap[c.remoteConversationKey];
|
||||
if (contact == null) {
|
||||
return false;
|
||||
}
|
||||
return contact.editedProfile.name
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
contact.editedProfile.pronouns
|
||||
.toLowerCase()
|
||||
.contains(lowerValue);
|
||||
}).toList();
|
||||
},
|
||||
spaceBetweenSearchAndList: 4,
|
||||
inputDecoration: InputDecoration(
|
||||
labelText: translate('chat_list.search'),
|
||||
contentPadding: const EdgeInsets.all(2),
|
||||
fillColor: scale.primaryScale.text,
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: scale.primaryScale.hoverBorder,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
).paddingAll(8))))
|
||||
.paddingLTRB(8, 8, 8, 65);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<IMap<proto.TypedKey, proto.Contact>>(
|
||||
'contactMap', contactMap))
|
||||
..add(IterableProperty<proto.Chat>('chatList', chatList));
|
||||
}
|
||||
}
|
152
lib/old_to_refactor/components/contact_invitation_display.dart
Normal file
152
lib/old_to_refactor/components/contact_invitation_display.dart
Normal file
|
@ -0,0 +1,152 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:basic_utils/basic_utils.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
|
||||
class ContactInvitationDisplayDialog extends ConsumerStatefulWidget {
|
||||
const ContactInvitationDisplayDialog({
|
||||
required this.name,
|
||||
required this.message,
|
||||
required this.generator,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String message;
|
||||
final FutureOr<Uint8List> generator;
|
||||
|
||||
@override
|
||||
ContactInvitationDisplayDialogState createState() =>
|
||||
ContactInvitationDisplayDialogState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('name', name))
|
||||
..add(StringProperty('message', message))
|
||||
..add(DiagnosticsProperty<FutureOr<Uint8List>?>('generator', generator));
|
||||
}
|
||||
}
|
||||
|
||||
class ContactInvitationDisplayDialogState
|
||||
extends ConsumerState<ContactInvitationDisplayDialog> {
|
||||
final focusNode = FocusNode();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
late final AutoDisposeFutureProvider<Uint8List?> _generateFutureProvider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_generateFutureProvider =
|
||||
AutoDisposeFutureProvider<Uint8List>((ref) async => widget.generator);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String makeTextInvite(String message, Uint8List data) {
|
||||
final invite = StringUtils.addCharAtPosition(
|
||||
base64UrlNoPadEncode(data), '\n', 40,
|
||||
repeat: true);
|
||||
final msg = message.isNotEmpty ? '$message\n' : '';
|
||||
return '$msg'
|
||||
'--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
|
||||
'$invite\n'
|
||||
'---- END VEILIDCHAT CONTACT INVITE -----\n';
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
//final scale = theme.extension<ScaleScheme>()!;
|
||||
final textTheme = theme.textTheme;
|
||||
|
||||
final signedContactInvitationBytesV = ref.watch(_generateFutureProvider);
|
||||
final cardsize =
|
||||
min<double>(MediaQuery.of(context).size.shortestSide - 48.0, 400);
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.white,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: cardsize,
|
||||
maxWidth: cardsize,
|
||||
minHeight: cardsize,
|
||||
maxHeight: cardsize),
|
||||
child: signedContactInvitationBytesV.when(
|
||||
loading: () => buildProgressIndicator(context),
|
||||
data: (data) {
|
||||
if (data == null) {
|
||||
Navigator.of(context).pop();
|
||||
return const Text('');
|
||||
}
|
||||
return Form(
|
||||
key: formKey,
|
||||
child: Column(children: [
|
||||
FittedBox(
|
||||
child: Text(
|
||||
translate(
|
||||
'send_invite_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(widget.message,
|
||||
softWrap: true,
|
||||
style: textTheme.labelLarge!
|
||||
.copyWith(color: Colors.black))
|
||||
.paddingAll(8),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.copy),
|
||||
label: Text(
|
||||
translate('send_invite_dialog.copy_invitation')),
|
||||
onPressed: () async {
|
||||
showInfoToast(
|
||||
context,
|
||||
translate(
|
||||
'send_invite_dialog.invitation_copied'));
|
||||
await Clipboard.setData(ClipboardData(
|
||||
text: makeTextInvite(widget.message, data)));
|
||||
},
|
||||
).paddingAll(16),
|
||||
]));
|
||||
},
|
||||
error: (e, s) {
|
||||
Navigator.of(context).pop();
|
||||
showErrorToast(context,
|
||||
translate('send_invite_dialog.failed_to_generate'));
|
||||
return const Text('');
|
||||
})));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode))
|
||||
..add(DiagnosticsProperty<GlobalKey<FormState>>('formKey', formKey));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../providers/account.dart';
|
||||
import '../providers/contact_invite.dart';
|
||||
import '../tools/tools.dart';
|
||||
import 'contact_invitation_display.dart';
|
||||
|
||||
class ContactInvitationItemWidget extends ConsumerWidget {
|
||||
const ContactInvitationItemWidget(
|
||||
{required this.contactInvitationRecord, super.key});
|
||||
|
||||
final proto.ContactInvitationRecord contactInvitationRecord;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<proto.ContactInvitationRecord>(
|
||||
'contactInvitationRecord', contactInvitationRecord));
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: ShapeDecoration(
|
||||
color: scale.tertiaryScale.subtleBorder,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
)),
|
||||
child: Slidable(
|
||||
// Specify a key if the Slidable is dismissible.
|
||||
key: ObjectKey(contactInvitationRecord),
|
||||
endActionPane: ActionPane(
|
||||
// A motion is a widget used to control how the pane animates.
|
||||
motion: const DrawerMotion(),
|
||||
|
||||
// A pane can dismiss the Slidable.
|
||||
//dismissible: DismissiblePane(onDismissed: () {}),
|
||||
|
||||
// All actions are defined in the children parameter.
|
||||
children: [
|
||||
// A SlidableAction can have an icon and/or a label.
|
||||
SlidableAction(
|
||||
onPressed: (context) async {
|
||||
final activeAccountInfo =
|
||||
await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo != null) {
|
||||
await deleteContactInvitation(
|
||||
accepted: false,
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
contactInvitationRecord: contactInvitationRecord);
|
||||
ref.invalidate(fetchContactInvitationRecordsProvider);
|
||||
}
|
||||
},
|
||||
backgroundColor: scale.tertiaryScale.background,
|
||||
foregroundColor: scale.tertiaryScale.text,
|
||||
icon: Icons.delete,
|
||||
label: translate('button.delete'),
|
||||
padding: const EdgeInsets.all(2)),
|
||||
],
|
||||
),
|
||||
|
||||
// startActionPane: ActionPane(
|
||||
// motion: const DrawerMotion(),
|
||||
// children: [
|
||||
// SlidableAction(
|
||||
// // An action can be bigger than the others.
|
||||
// flex: 2,
|
||||
// onPressed: (context) => (),
|
||||
// backgroundColor: Color(0xFF7BC043),
|
||||
// foregroundColor: Colors.white,
|
||||
// icon: Icons.archive,
|
||||
// label: 'Archive',
|
||||
// ),
|
||||
// SlidableAction(
|
||||
// onPressed: (context) => (),
|
||||
// backgroundColor: Color(0xFF0392CF),
|
||||
// foregroundColor: Colors.white,
|
||||
// icon: Icons.save,
|
||||
// label: 'Save',
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
|
||||
// The child of the Slidable is what the user sees when the
|
||||
// component is not dragged.
|
||||
child: ListTile(
|
||||
//title: Text(translate('contact_list.invitation')),
|
||||
onTap: () async {
|
||||
final activeAccountInfo =
|
||||
await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => ContactInvitationDisplayDialog(
|
||||
name: activeAccountInfo.localAccount.name,
|
||||
message: contactInvitationRecord.message,
|
||||
generator: Uint8List.fromList(
|
||||
contactInvitationRecord.invitation),
|
||||
));
|
||||
}
|
||||
},
|
||||
title: Text(
|
||||
contactInvitationRecord.message.isEmpty
|
||||
? translate('contact_list.invitation')
|
||||
: contactInvitationRecord.message,
|
||||
softWrap: true,
|
||||
),
|
||||
iconColor: scale.tertiaryScale.background,
|
||||
textColor: scale.tertiaryScale.text,
|
||||
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
|
||||
leading: const Icon(Icons.person_add))));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../tools/tools.dart';
|
||||
import 'contact_invitation_item_widget.dart';
|
||||
|
||||
class ContactInvitationListWidget extends ConsumerStatefulWidget {
|
||||
const ContactInvitationListWidget({
|
||||
required this.contactInvitationRecordList,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final IList<proto.ContactInvitationRecord> contactInvitationRecordList;
|
||||
|
||||
@override
|
||||
ContactInvitationListWidgetState createState() =>
|
||||
ContactInvitationListWidgetState();
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IterableProperty<proto.ContactInvitationRecord>(
|
||||
'contactInvitationRecordList', contactInvitationRecordList));
|
||||
}
|
||||
}
|
||||
|
||||
class ContactInvitationListWidgetState
|
||||
extends ConsumerState<ContactInvitationListWidget> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.fromLTRB(4, 0, 4, 4),
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
)),
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: ShapeDecoration(
|
||||
color: scale.primaryScale.subtleBackground,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
)),
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: widget.contactInvitationRecordList.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index < 0 ||
|
||||
index >= widget.contactInvitationRecordList.length) {
|
||||
return null;
|
||||
}
|
||||
return ContactInvitationItemWidget(
|
||||
contactInvitationRecord:
|
||||
widget.contactInvitationRecordList[index],
|
||||
key: ObjectKey(widget.contactInvitationRecordList[index]))
|
||||
.paddingLTRB(4, 2, 4, 2);
|
||||
},
|
||||
findChildIndexCallback: (key) {
|
||||
final index = widget.contactInvitationRecordList.indexOf(
|
||||
(key as ObjectKey).value! as proto.ContactInvitationRecord);
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
return index;
|
||||
},
|
||||
).paddingLTRB(4, 6, 4, 6)),
|
||||
);
|
||||
}
|
||||
}
|
122
lib/old_to_refactor/components/contact_item_widget.dart
Normal file
122
lib/old_to_refactor/components/contact_item_widget.dart
Normal file
|
@ -0,0 +1,122 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../pages/main_pager/main_pager.dart';
|
||||
import '../providers/account.dart';
|
||||
import '../providers/chat.dart';
|
||||
import '../providers/contact.dart';
|
||||
import '../theme/theme.dart';
|
||||
|
||||
class ContactItemWidget extends ConsumerWidget {
|
||||
const ContactItemWidget({required this.contact, super.key});
|
||||
|
||||
final proto.Contact contact;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
final remoteConversationKey =
|
||||
proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(0, 4, 0, 0),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: ShapeDecoration(
|
||||
color: scale.tertiaryScale.subtleBorder,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
)),
|
||||
child: Slidable(
|
||||
key: ObjectKey(contact),
|
||||
endActionPane: ActionPane(
|
||||
motion: const DrawerMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (context) async {
|
||||
final activeAccountInfo =
|
||||
await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo != null) {
|
||||
await deleteContact(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
contact: contact);
|
||||
ref
|
||||
..invalidate(fetchContactListProvider)
|
||||
..invalidate(fetchChatListProvider);
|
||||
}
|
||||
},
|
||||
backgroundColor: scale.tertiaryScale.background,
|
||||
foregroundColor: scale.tertiaryScale.text,
|
||||
icon: Icons.delete,
|
||||
label: translate('button.delete'),
|
||||
padding: const EdgeInsets.all(2)),
|
||||
// SlidableAction(
|
||||
// onPressed: (context) => (),
|
||||
// backgroundColor: scale.secondaryScale.background,
|
||||
// foregroundColor: scale.secondaryScale.text,
|
||||
// icon: Icons.edit,
|
||||
// label: 'Edit',
|
||||
// ),
|
||||
],
|
||||
),
|
||||
|
||||
// The child of the Slidable is what the user sees when the
|
||||
// component is not dragged.
|
||||
child: ListTile(
|
||||
onTap: () async {
|
||||
final activeAccountInfo =
|
||||
await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo != null) {
|
||||
// Start a chat
|
||||
await getOrCreateChatSingleContact(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
remoteConversationRecordKey: remoteConversationKey);
|
||||
ref
|
||||
..invalidate(fetchContactListProvider)
|
||||
..invalidate(fetchChatListProvider);
|
||||
// Click over to chats
|
||||
if (context.mounted) {
|
||||
await MainPager.of(context)?.pageController.animateToPage(
|
||||
1,
|
||||
duration: 250.ms,
|
||||
curve: Curves.easeInOut);
|
||||
}
|
||||
}
|
||||
|
||||
// // ignore: use_build_context_synchronously
|
||||
// if (!context.mounted) {
|
||||
// return;
|
||||
// }
|
||||
// await showDialog<void>(
|
||||
// context: context,
|
||||
// builder: (context) => ContactInvitationDisplayDialog(
|
||||
// name: activeAccountInfo.localAccount.name,
|
||||
// message: contactInvitationRecord.message,
|
||||
// generator: Uint8List.fromList(
|
||||
// contactInvitationRecord.invitation),
|
||||
// ));
|
||||
// }
|
||||
},
|
||||
title: Text(contact.editedProfile.name),
|
||||
subtitle: (contact.editedProfile.pronouns.isNotEmpty)
|
||||
? Text(contact.editedProfile.pronouns)
|
||||
: null,
|
||||
iconColor: scale.tertiaryScale.background,
|
||||
textColor: scale.tertiaryScale.text,
|
||||
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
|
||||
leading: const Icon(Icons.person))));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<proto.Contact>('contact', contact));
|
||||
}
|
||||
}
|
68
lib/old_to_refactor/components/contact_list_widget.dart
Normal file
68
lib/old_to_refactor/components/contact_list_widget.dart
Normal file
|
@ -0,0 +1,68 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:searchable_listview/searchable_listview.dart';
|
||||
|
||||
import '../proto/proto.dart' as proto;
|
||||
import '../tools/tools.dart';
|
||||
import 'contact_item_widget.dart';
|
||||
import 'empty_contact_list_widget.dart';
|
||||
|
||||
class ContactListWidget extends ConsumerWidget {
|
||||
const ContactListWidget({required this.contactList, super.key});
|
||||
final IList<proto.Contact> contactList;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IterableProperty<proto.Contact>('contactList', contactList));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return SizedBox.expand(
|
||||
child: styledTitleContainer(
|
||||
context: context,
|
||||
title: translate('contact_list.title'),
|
||||
child: SizedBox.expand(
|
||||
child: (contactList.isEmpty)
|
||||
? const EmptyContactListWidget()
|
||||
: SearchableList<proto.Contact>(
|
||||
autoFocusOnSearch: false,
|
||||
initialList: contactList.toList(),
|
||||
builder: (l, i, c) => ContactItemWidget(contact: c),
|
||||
filter: (value) {
|
||||
final lowerValue = value.toLowerCase();
|
||||
return contactList
|
||||
.where((element) =>
|
||||
element.editedProfile.name
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
element.editedProfile.pronouns
|
||||
.toLowerCase()
|
||||
.contains(lowerValue))
|
||||
.toList();
|
||||
},
|
||||
spaceBetweenSearchAndList: 4,
|
||||
inputDecoration: InputDecoration(
|
||||
labelText: translate('contact_list.search'),
|
||||
contentPadding: const EdgeInsets.all(2),
|
||||
fillColor: scale.primaryScale.text,
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: scale.primaryScale.hoverBorder,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
).paddingAll(8),
|
||||
))).paddingLTRB(8, 0, 8, 8);
|
||||
}
|
||||
}
|
18
lib/old_to_refactor/components/default_app_bar.dart
Normal file
18
lib/old_to_refactor/components/default_app_bar.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class DefaultAppBar extends AppBar {
|
||||
DefaultAppBar(
|
||||
{required super.title, super.key, Widget? leading, super.actions})
|
||||
: super(
|
||||
leading: leading ??
|
||||
Container(
|
||||
margin: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(32),
|
||||
shape: BoxShape.circle),
|
||||
child:
|
||||
SvgPicture.asset('assets/images/vlogo.svg', height: 32)
|
||||
.paddingAll(4)));
|
||||
}
|
34
lib/old_to_refactor/components/empty_chat_list_widget.dart
Normal file
34
lib/old_to_refactor/components/empty_chat_list_widget.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
|
||||
class EmptyChatListWidget extends ConsumerWidget {
|
||||
const EmptyChatListWidget({super.key});
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chat,
|
||||
color: scale.primaryScale.border,
|
||||
size: 48,
|
||||
),
|
||||
Text(
|
||||
translate('chat_list.start_a_conversation'),
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
color: scale.primaryScale.border,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
36
lib/old_to_refactor/components/empty_chat_widget.dart
Normal file
36
lib/old_to_refactor/components/empty_chat_widget.dart
Normal file
|
@ -0,0 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class EmptyChatWidget extends ConsumerWidget {
|
||||
const EmptyChatWidget({super.key});
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
//
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chat,
|
||||
color: Theme.of(context).disabledColor,
|
||||
size: 48,
|
||||
),
|
||||
Text(
|
||||
'Say Something',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
|
||||
class EmptyContactListWidget extends ConsumerWidget {
|
||||
const EmptyContactListWidget({super.key});
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_add_sharp,
|
||||
color: scale.primaryScale.subtleBorder,
|
||||
size: 48,
|
||||
),
|
||||
Text(
|
||||
translate('contact_list.invite_people'),
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
color: scale.primaryScale.subtleBorder,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
128
lib/old_to_refactor/components/enter_password.dart
Normal file
128
lib/old_to_refactor/components/enter_password.dart
Normal file
|
@ -0,0 +1,128 @@
|
|||
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_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
|
||||
class EnterPasswordDialog extends ConsumerStatefulWidget {
|
||||
const EnterPasswordDialog({
|
||||
this.matchPass,
|
||||
this.description,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String? matchPass;
|
||||
final String? description;
|
||||
|
||||
@override
|
||||
EnterPasswordDialogState createState() => EnterPasswordDialogState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('reenter', matchPass))
|
||||
..add(StringProperty('description', description));
|
||||
}
|
||||
}
|
||||
|
||||
class EnterPasswordDialogState extends ConsumerState<EnterPasswordDialog> {
|
||||
final passwordController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
bool _passwordVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
passwordController.dispose();
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: scale.grayScale.subtleBackground,
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.matchPass == null
|
||||
? translate('enter_password_dialog.enter_password')
|
||||
: translate('enter_password_dialog.reenter_password'),
|
||||
style: theme.textTheme.titleLarge,
|
||||
).paddingAll(16),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
enableSuggestions: false,
|
||||
obscureText:
|
||||
!_passwordVisible, //This will obscure text dynamically
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.singleLineFormatter
|
||||
],
|
||||
onSubmitted: (password) {
|
||||
Navigator.pop(context, password);
|
||||
},
|
||||
onChanged: (_) {
|
||||
setState(() {});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: widget.matchPass == null
|
||||
? null
|
||||
: Icon(Icons.check_circle,
|
||||
color: passwordController.text == widget.matchPass
|
||||
? scale.primaryScale.background
|
||||
: scale.grayScale.subtleBackground),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: scale.primaryScale.text,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
)).paddingAll(16),
|
||||
if (widget.description != null)
|
||||
SizedBox(
|
||||
width: 400,
|
||||
child: Text(
|
||||
widget.description!,
|
||||
textAlign: TextAlign.center,
|
||||
).paddingAll(16))
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<TextEditingController>(
|
||||
'passwordController', passwordController))
|
||||
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode))
|
||||
..add(DiagnosticsProperty<GlobalKey<FormState>>('formKey', formKey));
|
||||
}
|
||||
}
|
142
lib/old_to_refactor/components/enter_pin.dart
Normal file
142
lib/old_to_refactor/components/enter_pin.dart
Normal file
|
@ -0,0 +1,142 @@
|
|||
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_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:pinput/pinput.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
|
||||
class EnterPinDialog extends ConsumerStatefulWidget {
|
||||
const EnterPinDialog({
|
||||
required this.reenter,
|
||||
required this.description,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool reenter;
|
||||
final String? description;
|
||||
|
||||
@override
|
||||
EnterPinDialogState createState() => EnterPinDialogState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('description', description))
|
||||
..add(DiagnosticsProperty<bool>('reenter', reenter));
|
||||
}
|
||||
}
|
||||
|
||||
class EnterPinDialogState extends ConsumerState<EnterPinDialog> {
|
||||
final pinController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
pinController.dispose();
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final focusedBorderColor = scale.primaryScale.hoverBorder;
|
||||
final fillColor = scale.primaryScale.elementBackground;
|
||||
final borderColor = scale.primaryScale.border;
|
||||
|
||||
final defaultPinTheme = PinTheme(
|
||||
width: 56,
|
||||
height: 60,
|
||||
textStyle: TextStyle(fontSize: 22, color: scale.primaryScale.text),
|
||||
decoration: BoxDecoration(
|
||||
color: fillColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
);
|
||||
|
||||
/// Optionally you can use form to validate the Pinput
|
||||
return Dialog(
|
||||
backgroundColor: scale.grayScale.subtleBackground,
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
!widget.reenter
|
||||
? translate('enter_pin_dialog.enter_pin')
|
||||
: translate('enter_pin_dialog.reenter_pin'),
|
||||
style: theme.textTheme.titleLarge,
|
||||
).paddingAll(16),
|
||||
Directionality(
|
||||
// Specify direction if desired
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Pinput(
|
||||
controller: pinController,
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
defaultPinTheme: defaultPinTheme,
|
||||
enableSuggestions: false,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.digitsOnly
|
||||
],
|
||||
hapticFeedbackType: HapticFeedbackType.lightImpact,
|
||||
onCompleted: (pin) {
|
||||
Navigator.pop(context, pin);
|
||||
},
|
||||
cursor: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 9),
|
||||
width: 22,
|
||||
height: 1,
|
||||
color: focusedBorderColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
focusedPinTheme: defaultPinTheme.copyWith(
|
||||
height: 68,
|
||||
width: 64,
|
||||
decoration: defaultPinTheme.decoration!.copyWith(
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
),
|
||||
).paddingAll(16),
|
||||
),
|
||||
if (widget.description != null)
|
||||
SizedBox(
|
||||
width: 400,
|
||||
child: Text(
|
||||
widget.description!,
|
||||
textAlign: TextAlign.center,
|
||||
).paddingAll(16))
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<TextEditingController>(
|
||||
'pinController', pinController))
|
||||
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode))
|
||||
..add(DiagnosticsProperty<GlobalKey<FormState>>('formKey', formKey));
|
||||
}
|
||||
}
|
343
lib/old_to_refactor/components/invite_dialog.dart
Normal file
343
lib/old_to_refactor/components/invite_dialog.dart
Normal file
|
@ -0,0 +1,343 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../entities/local_account.dart';
|
||||
import '../providers/account.dart';
|
||||
import '../providers/contact.dart';
|
||||
import '../providers/contact_invite.dart';
|
||||
import '../tools/tools.dart';
|
||||
import 'enter_password.dart';
|
||||
import 'enter_pin.dart';
|
||||
import 'profile_widget.dart';
|
||||
|
||||
class InviteDialog extends ConsumerStatefulWidget {
|
||||
const InviteDialog(
|
||||
{required this.onValidationCancelled,
|
||||
required this.onValidationSuccess,
|
||||
required this.onValidationFailed,
|
||||
required this.inviteControlIsValid,
|
||||
required this.buildInviteControl,
|
||||
super.key});
|
||||
|
||||
final void Function() onValidationCancelled;
|
||||
final void Function() onValidationSuccess;
|
||||
final void Function() onValidationFailed;
|
||||
final bool Function() inviteControlIsValid;
|
||||
final Widget Function(
|
||||
BuildContext context,
|
||||
InviteDialogState dialogState,
|
||||
Future<void> Function({required Uint8List inviteData})
|
||||
validateInviteData) buildInviteControl;
|
||||
|
||||
@override
|
||||
InviteDialogState createState() => InviteDialogState();
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(ObjectFlagProperty<void Function()>.has(
|
||||
'onValidationCancelled', onValidationCancelled))
|
||||
..add(ObjectFlagProperty<void Function()>.has(
|
||||
'onValidationSuccess', onValidationSuccess))
|
||||
..add(ObjectFlagProperty<void Function()>.has(
|
||||
'onValidationFailed', onValidationFailed))
|
||||
..add(ObjectFlagProperty<void Function()>.has(
|
||||
'inviteControlIsValid', inviteControlIsValid))
|
||||
..add(ObjectFlagProperty<
|
||||
Widget Function(
|
||||
BuildContext context,
|
||||
InviteDialogState dialogState,
|
||||
Future<void> Function({required Uint8List inviteData})
|
||||
validateInviteData)>.has(
|
||||
'buildInviteControl', buildInviteControl));
|
||||
}
|
||||
}
|
||||
|
||||
class InviteDialogState extends ConsumerState<InviteDialog> {
|
||||
ValidContactInvitation? _validInvitation;
|
||||
bool _isValidating = false;
|
||||
bool _isAccepting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool get isValidating => _isValidating;
|
||||
bool get isAccepting => _isAccepting;
|
||||
|
||||
Future<void> _onAccept() async {
|
||||
final navigator = Navigator.of(context);
|
||||
|
||||
setState(() {
|
||||
_isAccepting = true;
|
||||
});
|
||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
setState(() {
|
||||
_isAccepting = false;
|
||||
});
|
||||
navigator.pop();
|
||||
return;
|
||||
}
|
||||
final validInvitation = _validInvitation;
|
||||
if (validInvitation != null) {
|
||||
final acceptedContact =
|
||||
await acceptContactInvitation(activeAccountInfo, validInvitation);
|
||||
if (acceptedContact != null) {
|
||||
// initiator when accept is received will create
|
||||
// contact in the case of a 'note to self'
|
||||
final isSelf =
|
||||
activeAccountInfo.localAccount.identityMaster.identityPublicKey ==
|
||||
acceptedContact.remoteIdentity.identityPublicKey;
|
||||
if (!isSelf) {
|
||||
await createContact(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
profile: acceptedContact.profile,
|
||||
remoteIdentity: acceptedContact.remoteIdentity,
|
||||
remoteConversationRecordKey:
|
||||
acceptedContact.remoteConversationRecordKey,
|
||||
localConversationRecordKey:
|
||||
acceptedContact.localConversationRecordKey,
|
||||
);
|
||||
}
|
||||
ref
|
||||
..invalidate(fetchContactInvitationRecordsProvider)
|
||||
..invalidate(fetchContactListProvider);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
showErrorToast(context, 'invite_dialog.failed_to_accept');
|
||||
}
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_isAccepting = false;
|
||||
});
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
Future<void> _onReject() async {
|
||||
final navigator = Navigator.of(context);
|
||||
|
||||
setState(() {
|
||||
_isAccepting = true;
|
||||
});
|
||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
setState(() {
|
||||
_isAccepting = false;
|
||||
});
|
||||
navigator.pop();
|
||||
return;
|
||||
}
|
||||
final validInvitation = _validInvitation;
|
||||
if (validInvitation != null) {
|
||||
if (await rejectContactInvitation(activeAccountInfo, validInvitation)) {
|
||||
// do nothing right now
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
showErrorToast(context, 'invite_dialog.failed_to_reject');
|
||||
}
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_isAccepting = false;
|
||||
});
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
Future<void> _validateInviteData({
|
||||
required Uint8List inviteData,
|
||||
}) async {
|
||||
try {
|
||||
final activeAccountInfo =
|
||||
await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
setState(() {
|
||||
_isValidating = false;
|
||||
_validInvitation = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
final contactInvitationRecords =
|
||||
await ref.read(fetchContactInvitationRecordsProvider.future);
|
||||
|
||||
setState(() {
|
||||
_isValidating = true;
|
||||
_validInvitation = null;
|
||||
});
|
||||
final validatedContactInvitation = await validateContactInvitation(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
contactInvitationRecords: contactInvitationRecords,
|
||||
inviteData: inviteData,
|
||||
getEncryptionKeyCallback:
|
||||
(cs, encryptionKeyType, encryptedSecret) async {
|
||||
String encryptionKey;
|
||||
switch (encryptionKeyType) {
|
||||
case EncryptionKeyType.none:
|
||||
encryptionKey = '';
|
||||
case EncryptionKeyType.pin:
|
||||
final description =
|
||||
translate('invite_dialog.protected_with_pin');
|
||||
if (!context.mounted) {
|
||||
return null;
|
||||
}
|
||||
final pin = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => EnterPinDialog(
|
||||
reenter: false, description: description));
|
||||
if (pin == null) {
|
||||
return null;
|
||||
}
|
||||
encryptionKey = pin;
|
||||
case EncryptionKeyType.password:
|
||||
final description =
|
||||
translate('invite_dialog.protected_with_password');
|
||||
if (!context.mounted) {
|
||||
return null;
|
||||
}
|
||||
final password = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
EnterPasswordDialog(description: description));
|
||||
if (password == null) {
|
||||
return null;
|
||||
}
|
||||
encryptionKey = password;
|
||||
}
|
||||
return decryptSecretFromBytes(
|
||||
secretBytes: encryptedSecret,
|
||||
cryptoKind: cs.kind(),
|
||||
encryptionKeyType: encryptionKeyType,
|
||||
encryptionKey: encryptionKey);
|
||||
});
|
||||
|
||||
// Check if validation was cancelled
|
||||
if (validatedContactInvitation == null) {
|
||||
setState(() {
|
||||
_isValidating = false;
|
||||
_validInvitation = null;
|
||||
widget.onValidationCancelled();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify expiration
|
||||
// xxx
|
||||
|
||||
setState(() {
|
||||
widget.onValidationSuccess();
|
||||
_isValidating = false;
|
||||
_validInvitation = validatedContactInvitation;
|
||||
});
|
||||
} on ContactInviteInvalidKeyException catch (e) {
|
||||
String errorText;
|
||||
switch (e.type) {
|
||||
case EncryptionKeyType.none:
|
||||
errorText = translate('invite_dialog.invalid_invitation');
|
||||
case EncryptionKeyType.pin:
|
||||
errorText = translate('invite_dialog.invalid_pin');
|
||||
case EncryptionKeyType.password:
|
||||
errorText = translate('invite_dialog.invalid_password');
|
||||
}
|
||||
if (context.mounted) {
|
||||
showErrorToast(context, errorText);
|
||||
}
|
||||
setState(() {
|
||||
_isValidating = false;
|
||||
_validInvitation = null;
|
||||
widget.onValidationFailed();
|
||||
});
|
||||
} on Exception catch (e) {
|
||||
log.debug('exception: $e', e);
|
||||
setState(() {
|
||||
_isValidating = false;
|
||||
_validInvitation = null;
|
||||
widget.onValidationFailed();
|
||||
});
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
// final theme = Theme.of(context);
|
||||
// final scale = theme.extension<ScaleScheme>()!;
|
||||
// final textTheme = theme.textTheme;
|
||||
// final height = MediaQuery.of(context).size.height;
|
||||
|
||||
if (_isAccepting) {
|
||||
return SizedBox(
|
||||
height: 300,
|
||||
width: 300,
|
||||
child: buildProgressIndicator(context).toCenter())
|
||||
.paddingAll(16);
|
||||
}
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
widget.buildInviteControl(context, this, _validateInviteData),
|
||||
if (_isValidating)
|
||||
Column(children: [
|
||||
Text(translate('invite_dialog.validating'))
|
||||
.paddingLTRB(0, 0, 0, 16),
|
||||
buildProgressIndicator(context).paddingAll(16),
|
||||
]).toCenter(),
|
||||
if (_validInvitation == null &&
|
||||
!_isValidating &&
|
||||
widget.inviteControlIsValid())
|
||||
Column(children: [
|
||||
Text(translate('invite_dialog.invalid_invitation')),
|
||||
const Icon(Icons.error)
|
||||
]).paddingAll(16).toCenter(),
|
||||
if (_validInvitation != null && !_isValidating)
|
||||
Column(children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 64),
|
||||
width: double.infinity,
|
||||
child: ProfileWidget(
|
||||
name: _validInvitation!
|
||||
.contactRequestPrivate.profile.name,
|
||||
pronouns: _validInvitation!
|
||||
.contactRequestPrivate.profile.pronouns,
|
||||
)).paddingLTRB(0, 0, 0, 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check_circle),
|
||||
label: Text(translate('button.accept')),
|
||||
onPressed: _onAccept,
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.cancel),
|
||||
label: Text(translate('button.reject')),
|
||||
onPressed: _onReject,
|
||||
)
|
||||
],
|
||||
),
|
||||
])
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<bool>('isValidating', isValidating))
|
||||
..add(DiagnosticsProperty<bool>('isAccepting', isAccepting));
|
||||
}
|
||||
}
|
35
lib/old_to_refactor/components/no_conversation_widget.dart
Normal file
35
lib/old_to_refactor/components/no_conversation_widget.dart
Normal file
|
@ -0,0 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class NoContactWidget extends ConsumerWidget {
|
||||
const NoContactWidget({super.key});
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
//
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.emoji_people_outlined,
|
||||
color: Theme.of(context).disabledColor,
|
||||
size: 48,
|
||||
),
|
||||
Text(
|
||||
'Choose A Conversation To Chat',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
131
lib/old_to_refactor/components/paste_invite_dialog.dart
Normal file
131
lib/old_to_refactor/components/paste_invite_dialog.dart
Normal file
|
@ -0,0 +1,131 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
import 'invite_dialog.dart';
|
||||
|
||||
class PasteInviteDialog extends ConsumerStatefulWidget {
|
||||
const PasteInviteDialog({super.key});
|
||||
|
||||
@override
|
||||
PasteInviteDialogState createState() => PasteInviteDialogState();
|
||||
|
||||
static Future<void> show(BuildContext context) async {
|
||||
await showStyledDialog<void>(
|
||||
context: context,
|
||||
title: translate('paste_invite_dialog.title'),
|
||||
child: const PasteInviteDialog());
|
||||
}
|
||||
}
|
||||
|
||||
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
||||
final _pasteTextController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> _onPasteChanged(
|
||||
String text,
|
||||
Future<void> Function({
|
||||
required Uint8List inviteData,
|
||||
}) validateInviteData) async {
|
||||
final lines = text.split('\n');
|
||||
if (lines.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
var firstline =
|
||||
lines.indexWhere((element) => element.contains('BEGIN VEILIDCHAT'));
|
||||
firstline += 1;
|
||||
|
||||
var lastline =
|
||||
lines.indexWhere((element) => element.contains('END VEILIDCHAT'));
|
||||
if (lastline == -1) {
|
||||
lastline = lines.length;
|
||||
}
|
||||
if (lastline <= firstline) {
|
||||
return;
|
||||
}
|
||||
final inviteDataBase64 = lines
|
||||
.sublist(firstline, lastline)
|
||||
.join()
|
||||
.replaceAll(RegExp(r'[^A-Za-z0-9\-_]'), '');
|
||||
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
|
||||
|
||||
await validateInviteData(inviteData: inviteData);
|
||||
}
|
||||
|
||||
void onValidationCancelled() {
|
||||
_pasteTextController.clear();
|
||||
}
|
||||
|
||||
void onValidationSuccess() {
|
||||
//_pasteTextController.clear();
|
||||
}
|
||||
|
||||
void onValidationFailed() {
|
||||
_pasteTextController.clear();
|
||||
}
|
||||
|
||||
bool inviteControlIsValid() => _pasteTextController.text.isNotEmpty;
|
||||
|
||||
Widget buildInviteControl(
|
||||
BuildContext context,
|
||||
InviteDialogState dialogState,
|
||||
Future<void> Function({required Uint8List inviteData})
|
||||
validateInviteData) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
//final textTheme = theme.textTheme;
|
||||
//final height = MediaQuery.of(context).size.height;
|
||||
|
||||
final monoStyle = TextStyle(
|
||||
fontFamily: 'Source Code Pro',
|
||||
fontSize: 11,
|
||||
color: scale.primaryScale.text,
|
||||
);
|
||||
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(
|
||||
translate('paste_invite_dialog.paste_invite_here'),
|
||||
).paddingLTRB(0, 0, 0, 8),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: TextField(
|
||||
enabled: !dialogState.isValidating,
|
||||
onChanged: (text) async =>
|
||||
_onPasteChanged(text, validateInviteData),
|
||||
style: monoStyle,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
controller: _pasteTextController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
|
||||
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n'
|
||||
'---- END VEILIDCHAT CONTACT INVITE -----\n',
|
||||
//labelText: translate('paste_invite_dialog.paste')
|
||||
),
|
||||
)).paddingLTRB(0, 0, 0, 8)
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return InviteDialog(
|
||||
onValidationCancelled: onValidationCancelled,
|
||||
onValidationSuccess: onValidationSuccess,
|
||||
onValidationFailed: onValidationFailed,
|
||||
inviteControlIsValid: inviteControlIsValid,
|
||||
buildInviteControl: buildInviteControl);
|
||||
}
|
||||
}
|
49
lib/old_to_refactor/components/profile_widget.dart
Normal file
49
lib/old_to_refactor/components/profile_widget.dart
Normal file
|
@ -0,0 +1,49 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
|
||||
class ProfileWidget extends ConsumerWidget {
|
||||
const ProfileWidget({
|
||||
required this.name,
|
||||
this.pronouns,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String? pronouns;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final textTheme = theme.textTheme;
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color: scale.primaryScale.border,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))),
|
||||
child: Column(children: [
|
||||
Text(
|
||||
name,
|
||||
style: textTheme.headlineSmall,
|
||||
textAlign: TextAlign.left,
|
||||
).paddingAll(4),
|
||||
if (pronouns != null && pronouns!.isNotEmpty)
|
||||
Text(pronouns!, style: textTheme.bodyMedium).paddingLTRB(4, 0, 4, 4),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('name', name))
|
||||
..add(StringProperty('pronouns', pronouns));
|
||||
}
|
||||
}
|
399
lib/old_to_refactor/components/scan_invite_dialog.dart
Normal file
399
lib/old_to_refactor/components/scan_invite_dialog.dart
Normal file
|
@ -0,0 +1,399 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
import 'invite_dialog.dart';
|
||||
|
||||
class BarcodeOverlay extends CustomPainter {
|
||||
BarcodeOverlay({
|
||||
required this.barcode,
|
||||
required this.arguments,
|
||||
required this.boxFit,
|
||||
required this.capture,
|
||||
});
|
||||
|
||||
final BarcodeCapture capture;
|
||||
final Barcode barcode;
|
||||
final MobileScannerArguments arguments;
|
||||
final BoxFit boxFit;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (barcode.corners == null) {
|
||||
return;
|
||||
}
|
||||
final adjustedSize = applyBoxFit(boxFit, arguments.size, size);
|
||||
|
||||
var verticalPadding = size.height - adjustedSize.destination.height;
|
||||
var horizontalPadding = size.width - adjustedSize.destination.width;
|
||||
if (verticalPadding > 0) {
|
||||
verticalPadding = verticalPadding / 2;
|
||||
} else {
|
||||
verticalPadding = 0;
|
||||
}
|
||||
|
||||
if (horizontalPadding > 0) {
|
||||
horizontalPadding = horizontalPadding / 2;
|
||||
} else {
|
||||
horizontalPadding = 0;
|
||||
}
|
||||
|
||||
final ratioWidth =
|
||||
(Platform.isIOS ? capture.width! : arguments.size.width) /
|
||||
adjustedSize.destination.width;
|
||||
final ratioHeight =
|
||||
(Platform.isIOS ? capture.height! : arguments.size.height) /
|
||||
adjustedSize.destination.height;
|
||||
|
||||
final adjustedOffset = <Offset>[];
|
||||
for (final offset in barcode.corners!) {
|
||||
adjustedOffset.add(
|
||||
Offset(
|
||||
offset.dx / ratioWidth + horizontalPadding,
|
||||
offset.dy / ratioHeight + verticalPadding,
|
||||
),
|
||||
);
|
||||
}
|
||||
final cutoutPath = Path()..addPolygon(adjustedOffset, true);
|
||||
|
||||
final backgroundPaint = Paint()
|
||||
..color = Colors.red.withOpacity(0.3)
|
||||
..style = PaintingStyle.fill
|
||||
..blendMode = BlendMode.dstOut;
|
||||
|
||||
canvas.drawPath(cutoutPath, backgroundPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class ScannerOverlay extends CustomPainter {
|
||||
ScannerOverlay(this.scanWindow);
|
||||
|
||||
final Rect scanWindow;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final backgroundPath = Path()..addRect(Rect.largest);
|
||||
final cutoutPath = Path()..addRect(scanWindow);
|
||||
|
||||
final backgroundPaint = Paint()
|
||||
..color = Colors.black.withOpacity(0.5)
|
||||
..style = PaintingStyle.fill
|
||||
..blendMode = BlendMode.dstOut;
|
||||
|
||||
final backgroundWithCutout = Path.combine(
|
||||
PathOperation.difference,
|
||||
backgroundPath,
|
||||
cutoutPath,
|
||||
);
|
||||
canvas.drawPath(backgroundWithCutout, backgroundPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class ScanInviteDialog extends ConsumerStatefulWidget {
|
||||
const ScanInviteDialog({super.key});
|
||||
|
||||
@override
|
||||
ScanInviteDialogState createState() => ScanInviteDialogState();
|
||||
|
||||
static Future<void> show(BuildContext context) async {
|
||||
await showStyledDialog<void>(
|
||||
context: context,
|
||||
title: translate('scan_invite_dialog.title'),
|
||||
child: const ScanInviteDialog());
|
||||
}
|
||||
}
|
||||
|
||||
class ScanInviteDialogState extends ConsumerState<ScanInviteDialog> {
|
||||
bool scanned = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void onValidationCancelled() {
|
||||
setState(() {
|
||||
scanned = false;
|
||||
});
|
||||
}
|
||||
|
||||
void onValidationSuccess() {}
|
||||
void onValidationFailed() {
|
||||
setState(() {
|
||||
scanned = false;
|
||||
});
|
||||
}
|
||||
|
||||
bool inviteControlIsValid() => false; // _pasteTextController.text.isNotEmpty;
|
||||
|
||||
Future<Uint8List?> scanQRImage(BuildContext context) async {
|
||||
final theme = Theme.of(context);
|
||||
//final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final windowSize = MediaQuery.of(context).size;
|
||||
//final maxDialogWidth = min(windowSize.width - 64.0, 800.0 - 64.0);
|
||||
//final maxDialogHeight = windowSize.height - 64.0;
|
||||
|
||||
final scanWindow = Rect.fromCenter(
|
||||
center: MediaQuery.of(context).size.center(Offset.zero),
|
||||
width: 200,
|
||||
height: 200,
|
||||
);
|
||||
|
||||
final cameraController = MobileScannerController();
|
||||
try {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
MobileScanner(
|
||||
fit: BoxFit.contain,
|
||||
scanWindow: scanWindow,
|
||||
controller: cameraController,
|
||||
errorBuilder: (context, error, child) =>
|
||||
ScannerErrorWidget(error: error),
|
||||
onDetect: (c) {
|
||||
final barcode = c.barcodes.firstOrNull;
|
||||
|
||||
final barcodeBytes = barcode?.rawBytes;
|
||||
if (barcodeBytes != null) {
|
||||
cameraController.dispose();
|
||||
Navigator.pop(context, barcodeBytes);
|
||||
}
|
||||
}),
|
||||
CustomPaint(
|
||||
painter: ScannerOverlay(scanWindow),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
height: 100,
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: ValueListenableBuilder(
|
||||
valueListenable: cameraController.torchState,
|
||||
builder: (context, state, child) {
|
||||
switch (state) {
|
||||
case TorchState.off:
|
||||
return Icon(Icons.flash_off,
|
||||
color:
|
||||
scale.grayScale.subtleBackground);
|
||||
case TorchState.on:
|
||||
return Icon(Icons.flash_on,
|
||||
color: scale.primaryScale.background);
|
||||
}
|
||||
},
|
||||
),
|
||||
iconSize: 32,
|
||||
onPressed: cameraController.toggleTorch,
|
||||
),
|
||||
SizedBox(
|
||||
width: windowSize.width - 120,
|
||||
height: 50,
|
||||
child: FittedBox(
|
||||
child: Text(
|
||||
translate('scan_invite_dialog.instructions'),
|
||||
overflow: TextOverflow.fade,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.copyWith(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: ValueListenableBuilder(
|
||||
valueListenable:
|
||||
cameraController.cameraFacingState,
|
||||
builder: (context, state, child) {
|
||||
switch (state) {
|
||||
case CameraFacing.front:
|
||||
return const Icon(Icons.camera_front);
|
||||
case CameraFacing.back:
|
||||
return const Icon(Icons.camera_rear);
|
||||
}
|
||||
},
|
||||
),
|
||||
iconSize: 32,
|
||||
onPressed: cameraController.switchCamera,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.close,
|
||||
color: scale.grayScale.background),
|
||||
iconSize: 32,
|
||||
onPressed: () => {
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((_) {
|
||||
cameraController.dispose();
|
||||
Navigator.pop(context, null);
|
||||
})
|
||||
})),
|
||||
],
|
||||
));
|
||||
} on MobileScannerException catch (e) {
|
||||
if (e.errorCode == MobileScannerErrorCode.permissionDenied) {
|
||||
showErrorToast(
|
||||
context, translate('scan_invite_dialog.permission_error'));
|
||||
} else {
|
||||
showErrorToast(context, translate('scan_invite_dialog.error'));
|
||||
}
|
||||
} on Exception catch (_) {
|
||||
showErrorToast(context, translate('scan_invite_dialog.error'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Uint8List?> pasteQRImage(BuildContext context) async {
|
||||
final imageBytes = await Pasteboard.image;
|
||||
if (imageBytes == null) {
|
||||
if (context.mounted) {
|
||||
showErrorToast(context, translate('scan_invite_dialog.not_an_image'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final image = img.decodeImage(imageBytes);
|
||||
if (image == null) {
|
||||
if (context.mounted) {
|
||||
showErrorToast(
|
||||
context, translate('scan_invite_dialog.could_not_decode_image'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final source = RGBLuminanceSource(
|
||||
image.width,
|
||||
image.height,
|
||||
image
|
||||
.convert(numChannels: 4)
|
||||
.getBytes(order: img.ChannelOrder.abgr)
|
||||
.buffer
|
||||
.asInt32List());
|
||||
final bitmap = BinaryBitmap(HybridBinarizer(source));
|
||||
|
||||
final reader = QRCodeReader();
|
||||
final result = reader.decode(bitmap);
|
||||
|
||||
final segs = result.resultMetadata[ResultMetadataType.byteSegments]!
|
||||
as List<Int8List>;
|
||||
return Uint8List.fromList(segs[0].toList());
|
||||
} on Exception catch (_) {
|
||||
if (context.mounted) {
|
||||
showErrorToast(
|
||||
context, translate('scan_invite_dialog.not_a_valid_qr_code'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildInviteControl(
|
||||
BuildContext context,
|
||||
InviteDialogState dialogState,
|
||||
Future<void> Function({required Uint8List inviteData})
|
||||
validateInviteData) {
|
||||
//final theme = Theme.of(context);
|
||||
//final scale = theme.extension<ScaleScheme>()!;
|
||||
//final textTheme = theme.textTheme;
|
||||
//final height = MediaQuery.of(context).size.height;
|
||||
|
||||
if (isiOS || isAndroid) {
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (!scanned)
|
||||
Text(
|
||||
translate('scan_invite_dialog.scan_qr_here'),
|
||||
).paddingLTRB(0, 0, 0, 8),
|
||||
if (!scanned)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: ElevatedButton(
|
||||
onPressed: dialogState.isValidating
|
||||
? null
|
||||
: () async {
|
||||
final inviteData = await scanQRImage(context);
|
||||
if (inviteData != null) {
|
||||
setState(() {
|
||||
scanned = true;
|
||||
});
|
||||
await validateInviteData(inviteData: inviteData);
|
||||
}
|
||||
},
|
||||
child: Text(translate('scan_invite_dialog.scan'))),
|
||||
).paddingLTRB(0, 0, 0, 8)
|
||||
]);
|
||||
}
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (!scanned)
|
||||
Text(
|
||||
translate('scan_invite_dialog.paste_qr_here'),
|
||||
).paddingLTRB(0, 0, 0, 8),
|
||||
if (!scanned)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: ElevatedButton(
|
||||
onPressed: dialogState.isValidating
|
||||
? null
|
||||
: () async {
|
||||
final inviteData = await pasteQRImage(context);
|
||||
if (inviteData != null) {
|
||||
await validateInviteData(inviteData: inviteData);
|
||||
setState(() {
|
||||
scanned = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Text(translate('scan_invite_dialog.paste'))),
|
||||
).paddingLTRB(0, 0, 0, 8)
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return InviteDialog(
|
||||
onValidationCancelled: onValidationCancelled,
|
||||
onValidationSuccess: onValidationSuccess,
|
||||
onValidationFailed: onValidationFailed,
|
||||
inviteControlIsValid: inviteControlIsValid,
|
||||
buildInviteControl: buildInviteControl);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<bool>('scanned', scanned));
|
||||
}
|
||||
}
|
248
lib/old_to_refactor/components/send_invite_dialog.dart
Normal file
248
lib/old_to_refactor/components/send_invite_dialog.dart
Normal file
|
@ -0,0 +1,248 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
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_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../entities/local_account.dart';
|
||||
import '../providers/account.dart';
|
||||
import '../providers/contact_invite.dart';
|
||||
import '../tools/tools.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
import 'contact_invitation_display.dart';
|
||||
import 'enter_password.dart';
|
||||
import 'enter_pin.dart';
|
||||
|
||||
class SendInviteDialog extends ConsumerStatefulWidget {
|
||||
const SendInviteDialog({super.key});
|
||||
|
||||
@override
|
||||
SendInviteDialogState createState() => SendInviteDialogState();
|
||||
|
||||
static Future<void> show(BuildContext context) async {
|
||||
await showStyledDialog<void>(
|
||||
context: context,
|
||||
title: translate('send_invite_dialog.title'),
|
||||
child: const SendInviteDialog());
|
||||
}
|
||||
}
|
||||
|
||||
class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
|
||||
final _messageTextController = TextEditingController(
|
||||
text: translate('send_invite_dialog.connect_with_me'));
|
||||
|
||||
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
|
||||
String _encryptionKey = '';
|
||||
Timestamp? _expiration;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> _onNoneEncryptionSelected(bool selected) async {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_encryptionKeyType = EncryptionKeyType.none;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onPinEncryptionSelected(bool selected) async {
|
||||
final description = translate('send_invite_dialog.pin_description');
|
||||
final pin = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
EnterPinDialog(reenter: false, description: description));
|
||||
if (pin == null) {
|
||||
return;
|
||||
}
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
final matchpin = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => EnterPinDialog(
|
||||
reenter: true,
|
||||
description: description,
|
||||
));
|
||||
if (matchpin == null) {
|
||||
return;
|
||||
} else if (pin == matchpin) {
|
||||
setState(() {
|
||||
_encryptionKeyType = EncryptionKeyType.pin;
|
||||
_encryptionKey = pin;
|
||||
});
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
showErrorToast(
|
||||
context, translate('send_invite_dialog.pin_does_not_match'));
|
||||
setState(() {
|
||||
_encryptionKeyType = EncryptionKeyType.none;
|
||||
_encryptionKey = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onPasswordEncryptionSelected(bool selected) async {
|
||||
final description = translate('send_invite_dialog.password_description');
|
||||
final password = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => EnterPasswordDialog(description: description));
|
||||
if (password == null) {
|
||||
return;
|
||||
}
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
final matchpass = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => EnterPasswordDialog(
|
||||
matchPass: password,
|
||||
description: description,
|
||||
));
|
||||
if (matchpass == null) {
|
||||
return;
|
||||
} else if (password == matchpass) {
|
||||
setState(() {
|
||||
_encryptionKeyType = EncryptionKeyType.password;
|
||||
_encryptionKey = password;
|
||||
});
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
showErrorToast(
|
||||
context, translate('send_invite_dialog.password_does_not_match'));
|
||||
setState(() {
|
||||
_encryptionKeyType = EncryptionKeyType.none;
|
||||
_encryptionKey = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onGenerateButtonPressed() async {
|
||||
final navigator = Navigator.of(context);
|
||||
|
||||
// Start generation
|
||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
navigator.pop();
|
||||
return;
|
||||
}
|
||||
final generator = createContactInvitation(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
encryptionKeyType: _encryptionKeyType,
|
||||
encryptionKey: _encryptionKey,
|
||||
message: _messageTextController.text,
|
||||
expiration: _expiration);
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => ContactInvitationDisplayDialog(
|
||||
name: activeAccountInfo.localAccount.name,
|
||||
message: _messageTextController.text,
|
||||
generator: generator,
|
||||
));
|
||||
// if (ret == null) {
|
||||
// return;
|
||||
// }
|
||||
ref.invalidate(fetchContactInvitationRecordsProvider);
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final windowSize = MediaQuery.of(context).size;
|
||||
final maxDialogWidth = min(windowSize.width - 64.0, 800.0 - 64.0);
|
||||
final maxDialogHeight = windowSize.height - 64.0;
|
||||
|
||||
final theme = Theme.of(context);
|
||||
//final scale = theme.extension<ScaleScheme>()!;
|
||||
final textTheme = theme.textTheme;
|
||||
return ConstrainedBox(
|
||||
constraints:
|
||||
BoxConstraints(maxHeight: maxDialogHeight, maxWidth: maxDialogWidth),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
translate('send_invite_dialog.message_to_contact'),
|
||||
).paddingAll(8),
|
||||
TextField(
|
||||
controller: _messageTextController,
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(128),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: translate('send_invite_dialog.enter_message_hint'),
|
||||
labelText: translate('send_invite_dialog.message')),
|
||||
).paddingAll(8),
|
||||
const SizedBox(height: 10),
|
||||
Text(translate('send_invite_dialog.protect_this_invitation'),
|
||||
style: textTheme.labelLarge)
|
||||
.paddingAll(8),
|
||||
Wrap(spacing: 5, children: [
|
||||
ChoiceChip(
|
||||
label: Text(translate('send_invite_dialog.unlocked')),
|
||||
selected: _encryptionKeyType == EncryptionKeyType.none,
|
||||
onSelected: _onNoneEncryptionSelected,
|
||||
),
|
||||
ChoiceChip(
|
||||
label: Text(translate('send_invite_dialog.pin')),
|
||||
selected: _encryptionKeyType == EncryptionKeyType.pin,
|
||||
onSelected: _onPinEncryptionSelected,
|
||||
),
|
||||
ChoiceChip(
|
||||
label: Text(translate('send_invite_dialog.password')),
|
||||
selected: _encryptionKeyType == EncryptionKeyType.password,
|
||||
onSelected: _onPasswordEncryptionSelected,
|
||||
)
|
||||
]).paddingAll(8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 60,
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ElevatedButton(
|
||||
onPressed: _onGenerateButtonPressed,
|
||||
child: Text(
|
||||
translate('send_invite_dialog.generate'),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(translate('send_invite_dialog.note')).paddingAll(8),
|
||||
Text(
|
||||
translate('send_invite_dialog.note_text'),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
).paddingAll(8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<TextEditingController>(
|
||||
'messageTextController', _messageTextController));
|
||||
}
|
||||
}
|
66
lib/old_to_refactor/components/signal_strength_meter.dart
Normal file
66
lib/old_to_refactor/components/signal_strength_meter.dart
Normal file
|
@ -0,0 +1,66 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:signal_strength_indicator/signal_strength_indicator.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../providers/connection_state.dart';
|
||||
import '../tools/tools.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
|
||||
class SignalStrengthMeterWidget extends ConsumerWidget {
|
||||
const SignalStrengthMeterWidget({super.key});
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
const iconSize = 16.0;
|
||||
final connState = ref.watch(connectionStateProvider);
|
||||
|
||||
late final double value;
|
||||
late final Color color;
|
||||
late final Color inactiveColor;
|
||||
switch (connState.attachment.state) {
|
||||
case AttachmentState.detached:
|
||||
return Icon(Icons.signal_cellular_nodata,
|
||||
size: iconSize, color: scale.grayScale.text);
|
||||
case AttachmentState.detaching:
|
||||
return Icon(Icons.signal_cellular_off,
|
||||
size: iconSize, color: scale.grayScale.text);
|
||||
case AttachmentState.attaching:
|
||||
value = 0;
|
||||
color = scale.primaryScale.text;
|
||||
case AttachmentState.attachedWeak:
|
||||
value = 1;
|
||||
color = scale.primaryScale.text;
|
||||
case AttachmentState.attachedStrong:
|
||||
value = 2;
|
||||
color = scale.primaryScale.text;
|
||||
case AttachmentState.attachedGood:
|
||||
value = 3;
|
||||
color = scale.primaryScale.text;
|
||||
case AttachmentState.fullyAttached:
|
||||
value = 4;
|
||||
color = scale.primaryScale.text;
|
||||
case AttachmentState.overAttached:
|
||||
value = 4;
|
||||
color = scale.secondaryScale.subtleText;
|
||||
}
|
||||
inactiveColor = scale.grayScale.subtleText;
|
||||
|
||||
return GestureDetector(
|
||||
onLongPress: () async {
|
||||
await context.push('/developer');
|
||||
},
|
||||
child: SignalStrengthIndicator.bars(
|
||||
value: value,
|
||||
activeColor: color,
|
||||
inactiveColor: inactiveColor,
|
||||
size: iconSize,
|
||||
barCount: 4,
|
||||
spacing: 1,
|
||||
));
|
||||
}
|
||||
}
|
3
lib/old_to_refactor/entities/entities.dart
Normal file
3
lib/old_to_refactor/entities/entities.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
export 'local_account.dart';
|
||||
export 'preferences.dart';
|
||||
export 'user_login.dart';
|
42
lib/old_to_refactor/entities/preferences.dart
Normal file
42
lib/old_to_refactor/entities/preferences.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
import 'package:change_case/change_case.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'preferences.freezed.dart';
|
||||
part 'preferences.g.dart';
|
||||
|
||||
// Lock preference changes how frequently the messenger locks its
|
||||
// interface and requires the identitySecretKey to be entered (pin/password/etc)
|
||||
@freezed
|
||||
class LockPreference with _$LockPreference {
|
||||
const factory LockPreference({
|
||||
required int inactivityLockSecs,
|
||||
required bool lockWhenSwitching,
|
||||
required bool lockWithSystemLock,
|
||||
}) = _LockPreference;
|
||||
|
||||
factory LockPreference.fromJson(dynamic json) =>
|
||||
_$LockPreferenceFromJson(json as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
// Theme supports multiple translations
|
||||
enum LanguagePreference {
|
||||
englishUS;
|
||||
|
||||
factory LanguagePreference.fromJson(dynamic j) =>
|
||||
LanguagePreference.values.byName((j as String).toCamelCase());
|
||||
String toJson() => name.toPascalCase();
|
||||
}
|
||||
|
||||
// Preferences are stored in a table locally and globally affect all
|
||||
// accounts imported/added and the app in general
|
||||
@freezed
|
||||
class Preferences with _$Preferences {
|
||||
const factory Preferences({
|
||||
required ThemePreferences themePreferences,
|
||||
required LanguagePreference language,
|
||||
required LockPreference locking,
|
||||
}) = _Preferences;
|
||||
|
||||
factory Preferences.fromJson(dynamic json) =>
|
||||
_$PreferencesFromJson(json as Map<String, dynamic>);
|
||||
}
|
396
lib/old_to_refactor/entities/preferences.freezed.dart
Normal file
396
lib/old_to_refactor/entities/preferences.freezed.dart
Normal file
|
@ -0,0 +1,396 @@
|
|||
// 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 'preferences.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#custom-getters-and-methods');
|
||||
|
||||
LockPreference _$LockPreferenceFromJson(Map<String, dynamic> json) {
|
||||
return _LockPreference.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$LockPreference {
|
||||
int get inactivityLockSecs => throw _privateConstructorUsedError;
|
||||
bool get lockWhenSwitching => throw _privateConstructorUsedError;
|
||||
bool get lockWithSystemLock => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$LockPreferenceCopyWith<LockPreference> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $LockPreferenceCopyWith<$Res> {
|
||||
factory $LockPreferenceCopyWith(
|
||||
LockPreference value, $Res Function(LockPreference) then) =
|
||||
_$LockPreferenceCopyWithImpl<$Res, LockPreference>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int inactivityLockSecs,
|
||||
bool lockWhenSwitching,
|
||||
bool lockWithSystemLock});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$LockPreferenceCopyWithImpl<$Res, $Val extends LockPreference>
|
||||
implements $LockPreferenceCopyWith<$Res> {
|
||||
_$LockPreferenceCopyWithImpl(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? inactivityLockSecs = null,
|
||||
Object? lockWhenSwitching = null,
|
||||
Object? lockWithSystemLock = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
inactivityLockSecs: null == inactivityLockSecs
|
||||
? _value.inactivityLockSecs
|
||||
: inactivityLockSecs // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
lockWhenSwitching: null == lockWhenSwitching
|
||||
? _value.lockWhenSwitching
|
||||
: lockWhenSwitching // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
lockWithSystemLock: null == lockWithSystemLock
|
||||
? _value.lockWithSystemLock
|
||||
: lockWithSystemLock // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$LockPreferenceImplCopyWith<$Res>
|
||||
implements $LockPreferenceCopyWith<$Res> {
|
||||
factory _$$LockPreferenceImplCopyWith(_$LockPreferenceImpl value,
|
||||
$Res Function(_$LockPreferenceImpl) then) =
|
||||
__$$LockPreferenceImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int inactivityLockSecs,
|
||||
bool lockWhenSwitching,
|
||||
bool lockWithSystemLock});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$LockPreferenceImplCopyWithImpl<$Res>
|
||||
extends _$LockPreferenceCopyWithImpl<$Res, _$LockPreferenceImpl>
|
||||
implements _$$LockPreferenceImplCopyWith<$Res> {
|
||||
__$$LockPreferenceImplCopyWithImpl(
|
||||
_$LockPreferenceImpl _value, $Res Function(_$LockPreferenceImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? inactivityLockSecs = null,
|
||||
Object? lockWhenSwitching = null,
|
||||
Object? lockWithSystemLock = null,
|
||||
}) {
|
||||
return _then(_$LockPreferenceImpl(
|
||||
inactivityLockSecs: null == inactivityLockSecs
|
||||
? _value.inactivityLockSecs
|
||||
: inactivityLockSecs // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
lockWhenSwitching: null == lockWhenSwitching
|
||||
? _value.lockWhenSwitching
|
||||
: lockWhenSwitching // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
lockWithSystemLock: null == lockWithSystemLock
|
||||
? _value.lockWithSystemLock
|
||||
: lockWithSystemLock // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$LockPreferenceImpl implements _LockPreference {
|
||||
const _$LockPreferenceImpl(
|
||||
{required this.inactivityLockSecs,
|
||||
required this.lockWhenSwitching,
|
||||
required this.lockWithSystemLock});
|
||||
|
||||
factory _$LockPreferenceImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$LockPreferenceImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int inactivityLockSecs;
|
||||
@override
|
||||
final bool lockWhenSwitching;
|
||||
@override
|
||||
final bool lockWithSystemLock;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LockPreference(inactivityLockSecs: $inactivityLockSecs, lockWhenSwitching: $lockWhenSwitching, lockWithSystemLock: $lockWithSystemLock)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$LockPreferenceImpl &&
|
||||
(identical(other.inactivityLockSecs, inactivityLockSecs) ||
|
||||
other.inactivityLockSecs == inactivityLockSecs) &&
|
||||
(identical(other.lockWhenSwitching, lockWhenSwitching) ||
|
||||
other.lockWhenSwitching == lockWhenSwitching) &&
|
||||
(identical(other.lockWithSystemLock, lockWithSystemLock) ||
|
||||
other.lockWithSystemLock == lockWithSystemLock));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, inactivityLockSecs, lockWhenSwitching, lockWithSystemLock);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$LockPreferenceImplCopyWith<_$LockPreferenceImpl> get copyWith =>
|
||||
__$$LockPreferenceImplCopyWithImpl<_$LockPreferenceImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$LockPreferenceImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _LockPreference implements LockPreference {
|
||||
const factory _LockPreference(
|
||||
{required final int inactivityLockSecs,
|
||||
required final bool lockWhenSwitching,
|
||||
required final bool lockWithSystemLock}) = _$LockPreferenceImpl;
|
||||
|
||||
factory _LockPreference.fromJson(Map<String, dynamic> json) =
|
||||
_$LockPreferenceImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get inactivityLockSecs;
|
||||
@override
|
||||
bool get lockWhenSwitching;
|
||||
@override
|
||||
bool get lockWithSystemLock;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$LockPreferenceImplCopyWith<_$LockPreferenceImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
Preferences _$PreferencesFromJson(Map<String, dynamic> json) {
|
||||
return _Preferences.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Preferences {
|
||||
ThemePreferences get themePreferences => throw _privateConstructorUsedError;
|
||||
LanguagePreference get language => throw _privateConstructorUsedError;
|
||||
LockPreference get locking => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$PreferencesCopyWith<Preferences> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $PreferencesCopyWith<$Res> {
|
||||
factory $PreferencesCopyWith(
|
||||
Preferences value, $Res Function(Preferences) then) =
|
||||
_$PreferencesCopyWithImpl<$Res, Preferences>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{ThemePreferences themePreferences,
|
||||
LanguagePreference language,
|
||||
LockPreference locking});
|
||||
|
||||
$LockPreferenceCopyWith<$Res> get locking;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences>
|
||||
implements $PreferencesCopyWith<$Res> {
|
||||
_$PreferencesCopyWithImpl(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? themePreferences = freezed,
|
||||
Object? language = null,
|
||||
Object? locking = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
themePreferences: freezed == themePreferences
|
||||
? _value.themePreferences
|
||||
: themePreferences // ignore: cast_nullable_to_non_nullable
|
||||
as ThemePreferences,
|
||||
language: null == language
|
||||
? _value.language
|
||||
: language // ignore: cast_nullable_to_non_nullable
|
||||
as LanguagePreference,
|
||||
locking: null == locking
|
||||
? _value.locking
|
||||
: locking // ignore: cast_nullable_to_non_nullable
|
||||
as LockPreference,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$LockPreferenceCopyWith<$Res> get locking {
|
||||
return $LockPreferenceCopyWith<$Res>(_value.locking, (value) {
|
||||
return _then(_value.copyWith(locking: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$PreferencesImplCopyWith<$Res>
|
||||
implements $PreferencesCopyWith<$Res> {
|
||||
factory _$$PreferencesImplCopyWith(
|
||||
_$PreferencesImpl value, $Res Function(_$PreferencesImpl) then) =
|
||||
__$$PreferencesImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{ThemePreferences themePreferences,
|
||||
LanguagePreference language,
|
||||
LockPreference locking});
|
||||
|
||||
@override
|
||||
$LockPreferenceCopyWith<$Res> get locking;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$PreferencesImplCopyWithImpl<$Res>
|
||||
extends _$PreferencesCopyWithImpl<$Res, _$PreferencesImpl>
|
||||
implements _$$PreferencesImplCopyWith<$Res> {
|
||||
__$$PreferencesImplCopyWithImpl(
|
||||
_$PreferencesImpl _value, $Res Function(_$PreferencesImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? themePreferences = freezed,
|
||||
Object? language = null,
|
||||
Object? locking = null,
|
||||
}) {
|
||||
return _then(_$PreferencesImpl(
|
||||
themePreferences: freezed == themePreferences
|
||||
? _value.themePreferences
|
||||
: themePreferences // ignore: cast_nullable_to_non_nullable
|
||||
as ThemePreferences,
|
||||
language: null == language
|
||||
? _value.language
|
||||
: language // ignore: cast_nullable_to_non_nullable
|
||||
as LanguagePreference,
|
||||
locking: null == locking
|
||||
? _value.locking
|
||||
: locking // ignore: cast_nullable_to_non_nullable
|
||||
as LockPreference,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$PreferencesImpl implements _Preferences {
|
||||
const _$PreferencesImpl(
|
||||
{required this.themePreferences,
|
||||
required this.language,
|
||||
required this.locking});
|
||||
|
||||
factory _$PreferencesImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$PreferencesImplFromJson(json);
|
||||
|
||||
@override
|
||||
final ThemePreferences themePreferences;
|
||||
@override
|
||||
final LanguagePreference language;
|
||||
@override
|
||||
final LockPreference locking;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Preferences(themePreferences: $themePreferences, language: $language, locking: $locking)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$PreferencesImpl &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.themePreferences, themePreferences) &&
|
||||
(identical(other.language, language) ||
|
||||
other.language == language) &&
|
||||
(identical(other.locking, locking) || other.locking == locking));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,
|
||||
const DeepCollectionEquality().hash(themePreferences), language, locking);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$PreferencesImplCopyWith<_$PreferencesImpl> get copyWith =>
|
||||
__$$PreferencesImplCopyWithImpl<_$PreferencesImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$PreferencesImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Preferences implements Preferences {
|
||||
const factory _Preferences(
|
||||
{required final ThemePreferences themePreferences,
|
||||
required final LanguagePreference language,
|
||||
required final LockPreference locking}) = _$PreferencesImpl;
|
||||
|
||||
factory _Preferences.fromJson(Map<String, dynamic> json) =
|
||||
_$PreferencesImpl.fromJson;
|
||||
|
||||
@override
|
||||
ThemePreferences get themePreferences;
|
||||
@override
|
||||
LanguagePreference get language;
|
||||
@override
|
||||
LockPreference get locking;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$PreferencesImplCopyWith<_$PreferencesImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
53
lib/old_to_refactor/entities/preferences.g.dart
Normal file
53
lib/old_to_refactor/entities/preferences.g.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'preferences.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$LockPreferenceImpl _$$LockPreferenceImplFromJson(Map<String, dynamic> json) =>
|
||||
_$LockPreferenceImpl(
|
||||
inactivityLockSecs: json['inactivity_lock_secs'] as int,
|
||||
lockWhenSwitching: json['lock_when_switching'] as bool,
|
||||
lockWithSystemLock: json['lock_with_system_lock'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$LockPreferenceImplToJson(
|
||||
_$LockPreferenceImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'inactivity_lock_secs': instance.inactivityLockSecs,
|
||||
'lock_when_switching': instance.lockWhenSwitching,
|
||||
'lock_with_system_lock': instance.lockWithSystemLock,
|
||||
};
|
||||
|
||||
_$ThemePreferencesImpl _$$ThemePreferencesImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$ThemePreferencesImpl(
|
||||
brightnessPreference:
|
||||
BrightnessPreference.fromJson(json['brightness_preference']),
|
||||
colorPreference: ColorPreference.fromJson(json['color_preference']),
|
||||
displayScale: (json['display_scale'] as num).toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ThemePreferencesImplToJson(
|
||||
_$ThemePreferencesImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'brightness_preference': instance.brightnessPreference.toJson(),
|
||||
'color_preference': instance.colorPreference.toJson(),
|
||||
'display_scale': instance.displayScale,
|
||||
};
|
||||
|
||||
_$PreferencesImpl _$$PreferencesImplFromJson(Map<String, dynamic> json) =>
|
||||
_$PreferencesImpl(
|
||||
themePreferences: ThemePreferences.fromJson(json['theme_preferences']),
|
||||
language: LanguagePreference.fromJson(json['language']),
|
||||
locking: LockPreference.fromJson(json['locking']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$PreferencesImplToJson(_$PreferencesImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'theme_preferences': instance.themePreferences.toJson(),
|
||||
'language': instance.language.toJson(),
|
||||
'locking': instance.locking.toJson(),
|
||||
};
|
0
lib/old_to_refactor/managers/contact_list_manager.dart
Normal file
0
lib/old_to_refactor/managers/contact_list_manager.dart
Normal file
|
@ -0,0 +1 @@
|
|||
|
45
lib/old_to_refactor/pages/chat_only.dart
Normal file
45
lib/old_to_refactor/pages/chat_only.dart
Normal file
|
@ -0,0 +1,45 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../providers/window_control.dart';
|
||||
import 'home.dart';
|
||||
|
||||
class ChatOnlyPage extends StatefulWidget {
|
||||
const ChatOnlyPage({super.key});
|
||||
|
||||
@override
|
||||
ChatOnlyPageState createState() => ChatOnlyPageState();
|
||||
}
|
||||
|
||||
class ChatOnlyPageState extends ConsumerState<ChatOnlyPage>
|
||||
with TickerProviderStateMixin {
|
||||
final _unfocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
setState(() {});
|
||||
await ref.read(windowControlProvider.notifier).changeWindowSetup(
|
||||
TitleBarStyle.normal, OrientationCapability.normal);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unfocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.watch(windowControlProvider);
|
||||
|
||||
return SafeArea(
|
||||
child: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode),
|
||||
child: HomePage.buildChatComponent(context, ref),
|
||||
));
|
||||
}
|
||||
}
|
256
lib/old_to_refactor/pages/developer.dart
Normal file
256
lib/old_to_refactor/pages/developer.dart
Normal file
|
@ -0,0 +1,256 @@
|
|||
import 'package:ansicolor/ansicolor.dart';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:cool_dropdown/cool_dropdown.dart';
|
||||
import 'package:cool_dropdown/models/cool_dropdown_item.dart';
|
||||
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_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:loggy/loggy.dart';
|
||||
import 'package:quickalert/quickalert.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_support/veilid_support.dart';
|
||||
|
||||
final globalDebugTerminal = Terminal(
|
||||
maxLines: 50000,
|
||||
);
|
||||
|
||||
const kDefaultTerminalStyle = TerminalStyle(
|
||||
fontSize: 11,
|
||||
// height: 1.2,
|
||||
fontFamily: 'Source Code Pro');
|
||||
|
||||
class DeveloperPage extends StatefulWidget {
|
||||
const DeveloperPage({super.key});
|
||||
|
||||
@override
|
||||
DeveloperPageState createState() => DeveloperPageState();
|
||||
}
|
||||
|
||||
class DeveloperPageState extends ConsumerState<DeveloperPage> {
|
||||
final _terminalController = TerminalController();
|
||||
final _debugCommandController = TextEditingController();
|
||||
final _logLevelController = DropdownController(duration: 250.ms);
|
||||
final List<CoolDropdownItem<LogLevel>> _logLevelDropdownItems = [];
|
||||
var _logLevelDropDown = log.level.logLevel;
|
||||
var _showEllet = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_terminalController.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
for (var i = 0; i < logLevels.length; i++) {
|
||||
_logLevelDropdownItems.add(CoolDropdownItem<LogLevel>(
|
||||
label: logLevelName(logLevels[i]),
|
||||
icon: Text(logLevelEmoji(logLevels[i])),
|
||||
value: logLevels[i]));
|
||||
}
|
||||
}
|
||||
|
||||
void _debugOut(String out) {
|
||||
final pen = AnsiPen()..cyan(bold: true);
|
||||
final colorOut = pen(out);
|
||||
debugPrint(colorOut);
|
||||
globalDebugTerminal.write(colorOut.replaceAll('\n', '\r\n'));
|
||||
}
|
||||
|
||||
Future<void> _sendDebugCommand(String debugCommand) async {
|
||||
if (debugCommand == 'ellet') {
|
||||
setState(() {
|
||||
_showEllet = !_showEllet;
|
||||
});
|
||||
return;
|
||||
}
|
||||
_debugOut('DEBUG >>>\n$debugCommand\n');
|
||||
try {
|
||||
final out = await Veilid.instance.debug(debugCommand);
|
||||
_debugOut('<<< DEBUG\n$out\n');
|
||||
} on Exception catch (e, st) {
|
||||
_debugOut('<<< ERROR\n$e\n<<< STACK\n$st');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clear(BuildContext context) async {
|
||||
globalDebugTerminal.buffer.clear();
|
||||
if (context.mounted) {
|
||||
showInfoToast(context, translate('developer.cleared'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> copySelection(BuildContext context) async {
|
||||
final selection = _terminalController.selection;
|
||||
if (selection != null) {
|
||||
final text = globalDebugTerminal.buffer.getText(selection);
|
||||
_terminalController.clearSelection();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (context.mounted) {
|
||||
showInfoToast(context, translate('developer.copied'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// if (!_isScrolling && _wantsBottom) {
|
||||
// _scrollToBottom();
|
||||
// }
|
||||
// });
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: scale.primaryScale.text),
|
||||
onPressed: () => GoRouterHelper(context).pop(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
color: scale.primaryScale.text,
|
||||
disabledColor: scale.grayScale.subtleText,
|
||||
onPressed: _terminalController.selection == null
|
||||
? null
|
||||
: () async {
|
||||
await copySelection(context);
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear_all),
|
||||
color: scale.primaryScale.text,
|
||||
disabledColor: scale.grayScale.subtleText,
|
||||
onPressed: () async {
|
||||
await QuickAlert.show(
|
||||
context: context,
|
||||
type: QuickAlertType.confirm,
|
||||
title: translate('developer.are_you_sure_clear'),
|
||||
textColor: scale.primaryScale.text,
|
||||
confirmBtnColor: scale.primaryScale.elementBackground,
|
||||
backgroundColor: scale.primaryScale.subtleBackground,
|
||||
headerBackgroundColor: scale.primaryScale.background,
|
||||
confirmBtnText: translate('button.ok'),
|
||||
cancelBtnText: translate('button.cancel'),
|
||||
onConfirmBtnTap: () async {
|
||||
Navigator.pop(context);
|
||||
if (context.mounted) {
|
||||
await clear(context);
|
||||
}
|
||||
});
|
||||
}),
|
||||
CoolDropdown<LogLevel>(
|
||||
controller: _logLevelController,
|
||||
defaultItem: _logLevelDropdownItems
|
||||
.singleWhere((x) => x.value == _logLevelDropDown),
|
||||
onChange: (value) {
|
||||
setState(() {
|
||||
_logLevelDropDown = value;
|
||||
Loggy('').level = getLogOptions(value);
|
||||
setVeilidLogLevel(value);
|
||||
_logLevelController.close();
|
||||
});
|
||||
},
|
||||
resultOptions: ResultOptions(
|
||||
width: 64,
|
||||
height: 40,
|
||||
render: ResultRender.icon,
|
||||
textStyle: textTheme.labelMedium!
|
||||
.copyWith(color: scale.primaryScale.text),
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
openBoxDecoration: BoxDecoration(
|
||||
color: scale.primaryScale.activeElementBackground),
|
||||
boxDecoration:
|
||||
BoxDecoration(color: scale.primaryScale.elementBackground),
|
||||
),
|
||||
dropdownOptions: DropdownOptions(
|
||||
width: 160,
|
||||
align: DropdownAlign.right,
|
||||
duration: 150.ms,
|
||||
color: scale.primaryScale.elementBackground,
|
||||
borderSide: BorderSide(color: scale.primaryScale.border),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
),
|
||||
dropdownTriangleOptions: const DropdownTriangleOptions(
|
||||
align: DropdownTriangleAlign.right),
|
||||
dropdownItemOptions: DropdownItemOptions(
|
||||
selectedTextStyle: textTheme.labelMedium!
|
||||
.copyWith(color: scale.primaryScale.text),
|
||||
textStyle: textTheme.labelMedium!
|
||||
.copyWith(color: scale.primaryScale.text),
|
||||
selectedBoxDecoration: BoxDecoration(
|
||||
color: scale.primaryScale.activeElementBackground),
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
selectedPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4)),
|
||||
dropdownList: _logLevelDropdownItems,
|
||||
)
|
||||
],
|
||||
title: Text(translate('developer.title'),
|
||||
style:
|
||||
textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.bold)),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(children: [
|
||||
Stack(alignment: AlignmentDirectional.center, children: [
|
||||
Image.asset('assets/images/ellet.png'),
|
||||
TerminalView(globalDebugTerminal,
|
||||
textStyle: kDefaultTerminalStyle,
|
||||
controller: _terminalController,
|
||||
//autofocus: true,
|
||||
backgroundOpacity: _showEllet ? 0.75 : 1.0,
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
await copySelection(context);
|
||||
})
|
||||
]).expanded(),
|
||||
TextField(
|
||||
controller: _debugCommandController,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: scale.primaryScale.border)),
|
||||
fillColor: scale.primaryScale.subtleBackground,
|
||||
hintText: translate('developer.command'),
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: _debugCommandController.text.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
final debugCommand = _debugCommandController.text;
|
||||
_debugCommandController.clear();
|
||||
await _sendDebugCommand(debugCommand);
|
||||
},
|
||||
)),
|
||||
onChanged: (_) {
|
||||
setState(() => {});
|
||||
},
|
||||
onSubmitted: (debugCommand) async {
|
||||
_debugCommandController.clear();
|
||||
await _sendDebugCommand(debugCommand);
|
||||
},
|
||||
).paddingAll(4)
|
||||
])));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<TerminalController>(
|
||||
'terminalController', _terminalController))
|
||||
..add(
|
||||
DiagnosticsProperty<LogLevel>('logLevelDropDown', _logLevelDropDown));
|
||||
}
|
||||
}
|
1
lib/old_to_refactor/pages/edit_account.dart
Normal file
1
lib/old_to_refactor/pages/edit_account.dart
Normal file
|
@ -0,0 +1 @@
|
|||
|
28
lib/old_to_refactor/pages/edit_contact.dart
Normal file
28
lib/old_to_refactor/pages/edit_contact.dart
Normal file
|
@ -0,0 +1,28 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class ContactsPage extends ConsumerWidget {
|
||||
const ContactsPage({super.key});
|
||||
static const path = '/contacts';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => const Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Contacts Page'),
|
||||
// ElevatedButton(
|
||||
// onPressed: () async {
|
||||
// ref.watch(authNotifierProvider.notifier).login(
|
||||
// "myEmail",
|
||||
// "myPassword",
|
||||
// );
|
||||
// },
|
||||
// child: const Text("Login"),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
269
lib/old_to_refactor/pages/home.dart
Normal file
269
lib/old_to_refactor/pages/home.dart
Normal file
|
@ -0,0 +1,269 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../components/chat_component.dart';
|
||||
import '../../components/empty_chat_widget.dart';
|
||||
import '../../components/profile_widget.dart';
|
||||
import '../../entities/local_account.dart';
|
||||
import '../providers/account.dart';
|
||||
import '../providers/chat.dart';
|
||||
import '../providers/contact.dart';
|
||||
import '../../local_accounts/local_accounts.dart';
|
||||
import '../providers/logins.dart';
|
||||
import '../providers/window_control.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_support/veilid_support.dart';
|
||||
import 'main_pager/main_pager.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
HomePageState createState() => HomePageState();
|
||||
|
||||
static Widget buildChatComponent(BuildContext context, WidgetRef ref) {
|
||||
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
|
||||
const IListConst([]);
|
||||
|
||||
final activeChat = ref.watch(activeChatStateProvider);
|
||||
if (activeChat == null) {
|
||||
return const EmptyChatWidget();
|
||||
}
|
||||
|
||||
final activeAccountInfo =
|
||||
ref.watch(fetchActiveAccountProvider).asData?.value;
|
||||
if (activeAccountInfo == null) {
|
||||
return const EmptyChatWidget();
|
||||
}
|
||||
|
||||
final activeChatContactIdx = contactList.indexWhere(
|
||||
(c) =>
|
||||
proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) ==
|
||||
activeChat,
|
||||
);
|
||||
if (activeChatContactIdx == -1) {
|
||||
ref.read(activeChatStateProvider.notifier).state = null;
|
||||
return const EmptyChatWidget();
|
||||
}
|
||||
final activeChatContact = contactList[activeChatContactIdx];
|
||||
|
||||
return ChatComponent(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
activeChat: activeChat,
|
||||
activeChatContact: activeChatContact);
|
||||
}
|
||||
}
|
||||
|
||||
class HomePageState extends ConsumerState<HomePage>
|
||||
with TickerProviderStateMixin {
|
||||
final _unfocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
setState(() {});
|
||||
await ref.read(windowControlProvider.notifier).changeWindowSetup(
|
||||
TitleBarStyle.normal, OrientationCapability.normal);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unfocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget buildAccountList() {
|
||||
return const Column(children: [
|
||||
Center(child: Text('Small Profile')),
|
||||
Center(child: Text('Contact invitations')),
|
||||
Center(child: Text('Contacts'))
|
||||
]);
|
||||
}
|
||||
|
||||
Widget buildUnlockAccount(
|
||||
BuildContext context,
|
||||
IList<LocalAccount> localAccounts,
|
||||
// ignore: prefer_expression_function_bodies
|
||||
) {
|
||||
return const Center(child: Text('unlock account'));
|
||||
}
|
||||
|
||||
/// We have an active, unlocked, user login
|
||||
Widget buildReadyAccount(
|
||||
BuildContext context,
|
||||
IList<LocalAccount> localAccounts,
|
||||
TypedKey activeUserLogin,
|
||||
proto.Account account) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return Column(children: <Widget>[
|
||||
Row(children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
color: scale.secondaryScale.text,
|
||||
constraints: const BoxConstraints.expand(height: 64, width: 64),
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.all(scale.secondaryScale.border),
|
||||
shape: MaterialStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16))))),
|
||||
tooltip: translate('app_bar.settings_tooltip'),
|
||||
onPressed: () async {
|
||||
context.go('/home/settings');
|
||||
}).paddingLTRB(0, 0, 8, 0),
|
||||
ProfileWidget(
|
||||
name: account.profile.name,
|
||||
pronouns: account.profile.pronouns,
|
||||
).expanded(),
|
||||
]).paddingAll(8),
|
||||
MainPager(
|
||||
localAccounts: localAccounts,
|
||||
activeUserLogin: activeUserLogin,
|
||||
account: account)
|
||||
.expanded()
|
||||
]);
|
||||
}
|
||||
|
||||
Widget buildUserPanel() {
|
||||
final localAccountsV = ref.watch(localAccountsProvider);
|
||||
final loginsV = ref.watch(loginsProvider);
|
||||
|
||||
if (!localAccountsV.hasValue || !loginsV.hasValue) {
|
||||
return waitingPage(context);
|
||||
}
|
||||
final localAccounts = localAccountsV.requireValue;
|
||||
final logins = loginsV.requireValue;
|
||||
|
||||
final activeUserLogin = logins.activeUserLogin;
|
||||
if (activeUserLogin == null) {
|
||||
// If no logged in user is active, show the list of account
|
||||
return buildAccountList();
|
||||
}
|
||||
final accountV = ref
|
||||
.watch(fetchAccountProvider(accountMasterRecordKey: activeUserLogin));
|
||||
if (!accountV.hasValue) {
|
||||
return waitingPage(context);
|
||||
}
|
||||
final account = accountV.requireValue;
|
||||
switch (account.status) {
|
||||
case AccountInfoStatus.noAccount:
|
||||
Future.delayed(0.ms, () async {
|
||||
await showErrorModal(context, translate('home.missing_account_title'),
|
||||
translate('home.missing_account_text'));
|
||||
// Delete account
|
||||
await ref
|
||||
.read(localAccountsProvider.notifier)
|
||||
.deleteLocalAccount(activeUserLogin);
|
||||
// Switch to no active user login
|
||||
await ref.read(loginsProvider.notifier).switchToAccount(null);
|
||||
});
|
||||
return waitingPage(context);
|
||||
case AccountInfoStatus.accountInvalid:
|
||||
Future.delayed(0.ms, () async {
|
||||
await showErrorModal(context, translate('home.invalid_account_title'),
|
||||
translate('home.invalid_account_text'));
|
||||
// Delete account
|
||||
await ref
|
||||
.read(localAccountsProvider.notifier)
|
||||
.deleteLocalAccount(activeUserLogin);
|
||||
// Switch to no active user login
|
||||
await ref.read(loginsProvider.notifier).switchToAccount(null);
|
||||
});
|
||||
return waitingPage(context);
|
||||
case AccountInfoStatus.accountLocked:
|
||||
// Show unlock widget
|
||||
return buildUnlockAccount(context, localAccounts);
|
||||
case AccountInfoStatus.accountReady:
|
||||
return buildReadyAccount(
|
||||
context,
|
||||
localAccounts,
|
||||
activeUserLogin,
|
||||
account.account!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget buildPhone(BuildContext context) {
|
||||
return Material(color: Colors.transparent, child: buildUserPanel());
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget buildTabletLeftPane(BuildContext context) {
|
||||
//
|
||||
return Material(color: Colors.transparent, child: buildUserPanel());
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget buildTabletRightPane(BuildContext context) {
|
||||
//
|
||||
return HomePage.buildChatComponent(context, ref);
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget buildTablet(BuildContext context) {
|
||||
final w = MediaQuery.of(context).size.width;
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
final children = [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 300, maxWidth: 300),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: w / 2),
|
||||
child: buildTabletLeftPane(context))),
|
||||
SizedBox(
|
||||
width: 2,
|
||||
height: double.infinity,
|
||||
child: ColoredBox(color: scale.primaryScale.hoverBorder)),
|
||||
Expanded(child: buildTabletRightPane(context)),
|
||||
];
|
||||
|
||||
return Row(
|
||||
children: children,
|
||||
);
|
||||
|
||||
// final theme = MultiSplitViewTheme(
|
||||
// data: isDesktop
|
||||
// ? MultiSplitViewThemeData(
|
||||
// dividerThickness: 1,
|
||||
// dividerPainter: DividerPainters.grooved2(thickness: 1))
|
||||
// : MultiSplitViewThemeData(
|
||||
// dividerThickness: 3,
|
||||
// dividerPainter: DividerPainters.grooved2(thickness: 1)),
|
||||
// child: multiSplitView);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.watch(windowControlProvider);
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return SafeArea(
|
||||
child: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: scale.primaryScale.activeElementBackground),
|
||||
child: responsiveVisibility(
|
||||
context: context,
|
||||
phone: false,
|
||||
)
|
||||
? buildTablet(context)
|
||||
: buildPhone(context),
|
||||
)));
|
||||
}
|
||||
}
|
50
lib/old_to_refactor/pages/index.dart
Normal file
50
lib/old_to_refactor/pages/index.dart
Normal file
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:radix_colors/radix_colors.dart';
|
||||
|
||||
class IndexPage extends StatelessWidget {
|
||||
const IndexPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final monoTextStyle = textTheme.labelSmall!
|
||||
.copyWith(fontFamily: 'Source Code Pro', fontSize: 11);
|
||||
final emojiTextStyle = textTheme.labelSmall!
|
||||
.copyWith(fontFamily: 'Noto Color Emoji', fontSize: 11);
|
||||
|
||||
return Scaffold(
|
||||
body: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
RadixColors.dark.plum.step4,
|
||||
RadixColors.dark.plum.step2,
|
||||
])),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Hack to preload fonts
|
||||
Offstage(child: Text('🧱', style: emojiTextStyle)),
|
||||
// Hack to preload fonts
|
||||
Offstage(child: Text('A', style: monoTextStyle)),
|
||||
// Splash Screen
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/icon.svg',
|
||||
)),
|
||||
Expanded(
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/title.svg',
|
||||
))
|
||||
]))),
|
||||
));
|
||||
}
|
||||
}
|
99
lib/old_to_refactor/pages/main_pager/account.dart
Normal file
99
lib/old_to_refactor/pages/main_pager/account.dart
Normal file
|
@ -0,0 +1,99 @@
|
|||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../../../components/contact_invitation_list_widget.dart';
|
||||
import '../../../components/contact_list_widget.dart';
|
||||
import '../../../entities/local_account.dart';
|
||||
import '../../../proto/proto.dart' as proto;
|
||||
import '../../providers/contact.dart';
|
||||
import '../../providers/contact_invite.dart';
|
||||
import '../../../theme/theme.dart';
|
||||
import '../../../tools/tools.dart';
|
||||
import '../../../veilid_support/veilid_support.dart';
|
||||
|
||||
class AccountPage extends ConsumerStatefulWidget {
|
||||
const AccountPage({
|
||||
required this.localAccounts,
|
||||
required this.activeUserLogin,
|
||||
required this.account,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final IList<LocalAccount> localAccounts;
|
||||
final TypedKey activeUserLogin;
|
||||
final proto.Account account;
|
||||
|
||||
@override
|
||||
AccountPageState createState() => AccountPageState();
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(IterableProperty<LocalAccount>('localAccounts', localAccounts))
|
||||
..add(DiagnosticsProperty<TypedKey>('activeUserLogin', activeUserLogin))
|
||||
..add(DiagnosticsProperty<proto.Account>('account', account));
|
||||
}
|
||||
}
|
||||
|
||||
class AccountPageState extends ConsumerState<AccountPage> {
|
||||
final _unfocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unfocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
final contactInvitationRecordList =
|
||||
ref.watch(fetchContactInvitationRecordsProvider).asData?.value ??
|
||||
const IListConst([]);
|
||||
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
|
||||
const IListConst([]);
|
||||
|
||||
return SizedBox(
|
||||
child: Column(children: <Widget>[
|
||||
if (contactInvitationRecordList.isNotEmpty)
|
||||
ExpansionTile(
|
||||
tilePadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
backgroundColor: scale.primaryScale.border,
|
||||
collapsedBackgroundColor: scale.primaryScale.border,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
collapsedShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
title: Text(
|
||||
translate('account_page.contact_invitations'),
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.titleMedium!
|
||||
.copyWith(color: scale.primaryScale.subtleText),
|
||||
),
|
||||
initiallyExpanded: true,
|
||||
children: [
|
||||
ContactInvitationListWidget(
|
||||
contactInvitationRecordList: contactInvitationRecordList)
|
||||
],
|
||||
).paddingLTRB(8, 0, 8, 8),
|
||||
ContactListWidget(contactList: contactList).expanded(),
|
||||
]));
|
||||
}
|
||||
}
|
100
lib/old_to_refactor/pages/main_pager/chats.dart
Normal file
100
lib/old_to_refactor/pages/main_pager/chats.dart
Normal file
|
@ -0,0 +1,100 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../components/chat_single_contact_list_widget.dart';
|
||||
import '../../../components/empty_chat_list_widget.dart';
|
||||
import '../../../entities/local_account.dart';
|
||||
import '../../../proto/proto.dart' as proto;
|
||||
import '../../providers/account.dart';
|
||||
import '../../providers/chat.dart';
|
||||
import '../../providers/contact.dart';
|
||||
import '../../../local_accounts/local_accounts.dart';
|
||||
import '../../providers/logins.dart';
|
||||
import '../../../tools/tools.dart';
|
||||
import '../../../veilid_support/veilid_support.dart';
|
||||
|
||||
class ChatsPage extends ConsumerStatefulWidget {
|
||||
const ChatsPage({super.key});
|
||||
|
||||
@override
|
||||
ChatsPageState createState() => ChatsPageState();
|
||||
}
|
||||
|
||||
class ChatsPageState extends ConsumerState<ChatsPage> {
|
||||
final _unfocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unfocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// We have an active, unlocked, user login
|
||||
Widget buildChatList(
|
||||
BuildContext context,
|
||||
IList<LocalAccount> localAccounts,
|
||||
TypedKey activeUserLogin,
|
||||
proto.Account account,
|
||||
// ignore: prefer_expression_function_bodies
|
||||
) {
|
||||
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
|
||||
const IListConst([]);
|
||||
final chatList =
|
||||
ref.watch(fetchChatListProvider).asData?.value ?? const IListConst([]);
|
||||
|
||||
return Column(children: <Widget>[
|
||||
if (chatList.isNotEmpty)
|
||||
ChatSingleContactListWidget(
|
||||
contactList: contactList, chatList: chatList)
|
||||
.expanded(),
|
||||
if (chatList.isEmpty) const EmptyChatListWidget().expanded(),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final localAccountsV = ref.watch(localAccountsProvider);
|
||||
final loginsV = ref.watch(loginsProvider);
|
||||
|
||||
if (!localAccountsV.hasValue || !loginsV.hasValue) {
|
||||
return waitingPage(context);
|
||||
}
|
||||
final localAccounts = localAccountsV.requireValue;
|
||||
final logins = loginsV.requireValue;
|
||||
|
||||
final activeUserLogin = logins.activeUserLogin;
|
||||
if (activeUserLogin == null) {
|
||||
// If no logged in user is active show a placeholder
|
||||
return waitingPage(context);
|
||||
}
|
||||
final accountV = ref
|
||||
.watch(fetchAccountProvider(accountMasterRecordKey: activeUserLogin));
|
||||
if (!accountV.hasValue) {
|
||||
return waitingPage(context);
|
||||
}
|
||||
final account = accountV.requireValue;
|
||||
switch (account.status) {
|
||||
case AccountInfoStatus.noAccount:
|
||||
return waitingPage(context);
|
||||
case AccountInfoStatus.accountInvalid:
|
||||
return waitingPage(context);
|
||||
case AccountInfoStatus.accountLocked:
|
||||
return waitingPage(context);
|
||||
case AccountInfoStatus.accountReady:
|
||||
return buildChatList(
|
||||
context,
|
||||
localAccounts,
|
||||
activeUserLogin,
|
||||
account.account!,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
315
lib/old_to_refactor/pages/main_pager/main_pager.dart
Normal file
315
lib/old_to_refactor/pages/main_pager/main_pager.dart
Normal file
|
@ -0,0 +1,315 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:preload_page_view/preload_page_view.dart';
|
||||
import 'package:stylish_bottom_bar/model/bar_items.dart';
|
||||
import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
|
||||
|
||||
import '../../../components/bottom_sheet_action_button.dart';
|
||||
import '../../../components/paste_invite_dialog.dart';
|
||||
import '../../../components/scan_invite_dialog.dart';
|
||||
import '../../../components/send_invite_dialog.dart';
|
||||
import '../../../entities/local_account.dart';
|
||||
import '../../../proto/proto.dart' as proto;
|
||||
import '../../../tools/tools.dart';
|
||||
import '../../../veilid_support/veilid_support.dart';
|
||||
import 'account.dart';
|
||||
import 'chats.dart';
|
||||
|
||||
class MainPager extends ConsumerStatefulWidget {
|
||||
const MainPager(
|
||||
{required this.localAccounts,
|
||||
required this.activeUserLogin,
|
||||
required this.account,
|
||||
super.key});
|
||||
|
||||
final IList<LocalAccount> localAccounts;
|
||||
final TypedKey activeUserLogin;
|
||||
final proto.Account account;
|
||||
|
||||
@override
|
||||
MainPagerState createState() => MainPagerState();
|
||||
|
||||
static MainPagerState? of(BuildContext context) =>
|
||||
context.findAncestorStateOfType<MainPagerState>();
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(IterableProperty<LocalAccount>('localAccounts', localAccounts))
|
||||
..add(DiagnosticsProperty<TypedKey>('activeUserLogin', activeUserLogin))
|
||||
..add(DiagnosticsProperty<proto.Account>('account', account));
|
||||
}
|
||||
}
|
||||
|
||||
class MainPagerState extends ConsumerState<MainPager>
|
||||
with TickerProviderStateMixin {
|
||||
//////////////////////////////////////////////////////////////////
|
||||
|
||||
final _unfocusNode = FocusNode();
|
||||
|
||||
var _currentPage = 0;
|
||||
final pageController = PreloadPageController();
|
||||
|
||||
final _selectedIconList = <IconData>[Icons.person, Icons.chat];
|
||||
// final _unselectedIconList = <IconData>[
|
||||
// Icons.chat_outlined,
|
||||
// Icons.person_outlined
|
||||
// ];
|
||||
final _fabIconList = <IconData>[
|
||||
Icons.person_add_sharp,
|
||||
Icons.add_comment_sharp,
|
||||
];
|
||||
final _bottomLabelList = <String>[
|
||||
translate('pager.account'),
|
||||
translate('pager.chats'),
|
||||
];
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unfocusNode.dispose();
|
||||
pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool onScrollNotification(ScrollNotification notification) {
|
||||
if (notification is UserScrollNotification &&
|
||||
notification.metrics.axis == Axis.vertical) {
|
||||
switch (notification.direction) {
|
||||
case ScrollDirection.forward:
|
||||
// _hideBottomBarAnimationController.reverse();
|
||||
// _fabAnimationController.forward(from: 0);
|
||||
break;
|
||||
case ScrollDirection.reverse:
|
||||
// _hideBottomBarAnimationController.forward();
|
||||
// _fabAnimationController.reverse(from: 1);
|
||||
break;
|
||||
case ScrollDirection.idle:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
BottomBarItem buildBottomBarItem(int index) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
return BottomBarItem(
|
||||
title: Text(_bottomLabelList[index]),
|
||||
icon: Icon(_selectedIconList[index], color: scale.primaryScale.text),
|
||||
selectedIcon:
|
||||
Icon(_selectedIconList[index], color: scale.primaryScale.text),
|
||||
backgroundColor: scale.primaryScale.text,
|
||||
//unSelectedColor: theme.colorScheme.primaryContainer,
|
||||
//selectedColor: theme.colorScheme.primary,
|
||||
//badge: const Text('9+'),
|
||||
//showBadge: true,
|
||||
);
|
||||
}
|
||||
|
||||
List<BottomBarItem> _buildBottomBarItems() {
|
||||
final bottomBarItems = List<BottomBarItem>.empty(growable: true);
|
||||
for (var index = 0; index < _bottomLabelList.length; index++) {
|
||||
final item = buildBottomBarItem(index);
|
||||
bottomBarItems.add(item);
|
||||
}
|
||||
return bottomBarItems;
|
||||
}
|
||||
|
||||
Future<void> scanContactInvitationDialog(BuildContext context) async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
// ignore: prefer_expression_function_bodies
|
||||
builder: (context) {
|
||||
return const AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
contentPadding: EdgeInsets.only(
|
||||
top: 10,
|
||||
),
|
||||
title: Text(
|
||||
'Scan Contact Invite',
|
||||
style: TextStyle(fontSize: 24),
|
||||
),
|
||||
content: ScanInviteDialog());
|
||||
});
|
||||
}
|
||||
|
||||
Widget _newContactInvitationBottomSheetBuilder(
|
||||
// ignore: prefer_expression_function_bodies
|
||||
BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return KeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKeyEvent: (ke) {
|
||||
if (ke.logicalKey == LogicalKeyboardKey.escape) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: Column(children: [
|
||||
Text(translate('accounts_menu.invite_contact'),
|
||||
style: textTheme.titleMedium)
|
||||
.paddingAll(8),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
|
||||
Column(children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await SendInviteDialog.show(context);
|
||||
},
|
||||
iconSize: 64,
|
||||
icon: const Icon(Icons.contact_page),
|
||||
color: scale.primaryScale.background),
|
||||
Text(translate('accounts_menu.create_invite'))
|
||||
]),
|
||||
Column(children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await ScanInviteDialog.show(context);
|
||||
},
|
||||
iconSize: 64,
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
color: scale.primaryScale.background),
|
||||
Text(translate('accounts_menu.scan_invite'))
|
||||
]),
|
||||
Column(children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await PasteInviteDialog.show(context);
|
||||
},
|
||||
iconSize: 64,
|
||||
icon: const Icon(Icons.paste),
|
||||
color: scale.primaryScale.background),
|
||||
Text(translate('accounts_menu.paste_invite'))
|
||||
])
|
||||
]).expanded()
|
||||
])));
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget _onNewChatBottomSheetBuilder(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 200,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Group and custom chat functionality is not available yet')));
|
||||
}
|
||||
|
||||
Widget _bottomSheetBuilder(BuildContext context) {
|
||||
if (_currentPage == 0) {
|
||||
// New contact invitation
|
||||
return _newContactInvitationBottomSheetBuilder(context);
|
||||
} else if (_currentPage == 1) {
|
||||
// New chat
|
||||
return _onNewChatBottomSheetBuilder(context);
|
||||
} else {
|
||||
// Unknown error
|
||||
return waitingPage(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
|
||||
return Scaffold(
|
||||
//extendBody: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
body: NotificationListener<ScrollNotification>(
|
||||
onNotification: onScrollNotification,
|
||||
child: PreloadPageView(
|
||||
controller: pageController,
|
||||
preloadPagesCount: 2,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
_currentPage = index;
|
||||
});
|
||||
},
|
||||
children: [
|
||||
AccountPage(
|
||||
localAccounts: widget.localAccounts,
|
||||
activeUserLogin: widget.activeUserLogin,
|
||||
account: widget.account),
|
||||
const ChatsPage(),
|
||||
])),
|
||||
// appBar: AppBar(
|
||||
// toolbarHeight: 24,
|
||||
// title: Text(
|
||||
// 'C',
|
||||
// style: Theme.of(context).textTheme.headlineSmall,
|
||||
// ),
|
||||
// ),
|
||||
bottomNavigationBar: StylishBottomBar(
|
||||
backgroundColor: scale.primaryScale.hoverBorder,
|
||||
// gradient: LinearGradient(
|
||||
// begin: Alignment.topCenter,
|
||||
// end: Alignment.bottomCenter,
|
||||
// colors: <Color>[
|
||||
// theme.colorScheme.primary,
|
||||
// theme.colorScheme.primaryContainer,
|
||||
// ]),
|
||||
//borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
option: AnimatedBarOptions(
|
||||
// iconSize: 32,
|
||||
//barAnimation: BarAnimation.fade,
|
||||
iconStyle: IconStyle.animated,
|
||||
inkEffect: true,
|
||||
inkColor: scale.primaryScale.hoverBackground,
|
||||
//opacity: 0.3,
|
||||
),
|
||||
items: _buildBottomBarItems(),
|
||||
hasNotch: true,
|
||||
fabLocation: StylishBarFabLocation.end,
|
||||
currentIndex: _currentPage,
|
||||
onTap: (index) async {
|
||||
await pageController.animateToPage(index,
|
||||
duration: 250.ms, curve: Curves.easeInOut);
|
||||
},
|
||||
),
|
||||
|
||||
floatingActionButton: BottomSheetActionButton(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(14))),
|
||||
foregroundColor: scale.secondaryScale.text,
|
||||
backgroundColor: scale.secondaryScale.hoverBorder,
|
||||
builder: (context) => Icon(
|
||||
_fabIconList[_currentPage],
|
||||
color: scale.secondaryScale.text,
|
||||
),
|
||||
bottomSheetBuilder: _bottomSheetBuilder),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<PreloadPageController>(
|
||||
'pageController', pageController));
|
||||
}
|
||||
}
|
175
lib/old_to_refactor/pages/new_account.dart
Normal file
175
lib/old_to_refactor/pages/new_account.dart
Normal file
|
@ -0,0 +1,175 @@
|
|||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../components/default_app_bar.dart';
|
||||
import '../../components/signal_strength_meter.dart';
|
||||
import '../../entities/entities.dart';
|
||||
import '../../local_accounts/local_accounts.dart';
|
||||
import '../providers/logins.dart';
|
||||
import '../providers/window_control.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_support/veilid_support.dart';
|
||||
|
||||
class NewAccountPage extends StatefulWidget {
|
||||
const NewAccountPage({super.key});
|
||||
|
||||
@override
|
||||
NewAccountPageState createState() => NewAccountPageState();
|
||||
}
|
||||
|
||||
class NewAccountPageState extends ConsumerState<NewAccountPage> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
late bool isInAsyncCall = false;
|
||||
static const String formFieldName = 'name';
|
||||
static const String formFieldPronouns = 'pronouns';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
setState(() {});
|
||||
await ref.read(windowControlProvider.notifier).changeWindowSetup(
|
||||
TitleBarStyle.normal, OrientationCapability.portraitOnly);
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates a new master identity, an account associated with the master
|
||||
/// identity, stores the account in the identity key and then logs into
|
||||
/// that account with no password set at this time
|
||||
Future<void> createAccount() async {
|
||||
final localAccounts = ref.read(localAccountsProvider.notifier);
|
||||
final logins = ref.read(loginsProvider.notifier);
|
||||
|
||||
final name = _formKey.currentState!.fields[formFieldName]!.value as String;
|
||||
final pronouns =
|
||||
_formKey.currentState!.fields[formFieldPronouns]!.value as String? ??
|
||||
'';
|
||||
|
||||
final imws = await IdentityMasterWithSecrets.create();
|
||||
try {
|
||||
final localAccount = await localAccounts.newLocalAccount(
|
||||
identityMaster: imws.identityMaster,
|
||||
identitySecret: imws.identitySecret,
|
||||
name: name,
|
||||
pronouns: pronouns);
|
||||
|
||||
// Log in the new account by default with no pin
|
||||
final ok = await logins.login(localAccount.identityMaster.masterRecordKey,
|
||||
EncryptionKeyType.none, '');
|
||||
assert(ok, 'login with none should never fail');
|
||||
} on Exception catch (_) {
|
||||
await imws.delete();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _newAccountForm(BuildContext context,
|
||||
{required Future<void> Function(GlobalKey<FormBuilderState>)
|
||||
onSubmit}) =>
|
||||
FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
Text(translate('new_account_page.header'))
|
||||
.textStyle(context.headlineSmall)
|
||||
.paddingSymmetric(vertical: 16),
|
||||
FormBuilderTextField(
|
||||
autofocus: true,
|
||||
name: formFieldName,
|
||||
decoration:
|
||||
InputDecoration(labelText: translate('account.form_name')),
|
||||
maxLength: 64,
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(),
|
||||
]),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: formFieldPronouns,
|
||||
maxLength: 64,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('account.form_pronouns')),
|
||||
),
|
||||
Row(children: [
|
||||
const Spacer(),
|
||||
Text(translate('new_account_page.instructions'))
|
||||
.toCenter()
|
||||
.flexible(flex: 6),
|
||||
const Spacer(),
|
||||
]).paddingSymmetric(vertical: 4),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
setState(() {
|
||||
isInAsyncCall = true;
|
||||
});
|
||||
try {
|
||||
await onSubmit(_formKey);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isInAsyncCall = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(translate('new_account_page.create')),
|
||||
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.watch(windowControlProvider);
|
||||
|
||||
final localAccounts = ref.watch(localAccountsProvider);
|
||||
final logins = ref.watch(loginsProvider);
|
||||
|
||||
final displayModalHUD =
|
||||
isInAsyncCall || !localAccounts.hasValue || !logins.hasValue;
|
||||
|
||||
return Scaffold(
|
||||
// resizeToAvoidBottomInset: false,
|
||||
appBar: DefaultAppBar(
|
||||
title: Text(translate('new_account_page.titlebar')),
|
||||
actions: [
|
||||
const SignalStrengthMeterWidget(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: translate('app_bar.settings_tooltip'),
|
||||
onPressed: () async {
|
||||
context.go('/new_account/settings');
|
||||
})
|
||||
]),
|
||||
body: _newAccountForm(
|
||||
context,
|
||||
onSubmit: (formKey) async {
|
||||
FocusScope.of(context).unfocus();
|
||||
try {
|
||||
await createAccount();
|
||||
} on Exception catch (e) {
|
||||
if (context.mounted) {
|
||||
await showErrorModal(context, translate('new_account_page.error'),
|
||||
'Exception: $e');
|
||||
}
|
||||
}
|
||||
},
|
||||
).paddingSymmetric(horizontal: 24, vertical: 8),
|
||||
).withModalHUD(context, displayModalHUD);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<bool>('isInAsyncCall', isInAsyncCall));
|
||||
}
|
||||
}
|
139
lib/old_to_refactor/pages/settings.dart
Normal file
139
lib/old_to_refactor/pages/settings.dart
Normal file
|
@ -0,0 +1,139 @@
|
|||
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../../components/default_app_bar.dart';
|
||||
import '../../components/signal_strength_meter.dart';
|
||||
import '../../entities/preferences.dart';
|
||||
import '../providers/window_control.dart';
|
||||
import '../../tools/tools.dart';
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
SettingsPageState createState() => SettingsPageState();
|
||||
}
|
||||
|
||||
class SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
late bool isInAsyncCall = false;
|
||||
// ThemePreferences? themePreferences;
|
||||
static const String formFieldTheme = 'theme';
|
||||
static const String formFieldBrightness = 'brightness';
|
||||
// static const String formFieldTitle = 'title';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
List<DropdownMenuItem<dynamic>> _getThemeDropdownItems() {
|
||||
const colorPrefs = ColorPreference.values;
|
||||
final colorNames = {
|
||||
ColorPreference.scarlet: translate('themes.scarlet'),
|
||||
ColorPreference.vapor: translate('themes.vapor'),
|
||||
ColorPreference.babydoll: translate('themes.babydoll'),
|
||||
ColorPreference.gold: translate('themes.gold'),
|
||||
ColorPreference.garden: translate('themes.garden'),
|
||||
ColorPreference.forest: translate('themes.forest'),
|
||||
ColorPreference.arctic: translate('themes.arctic'),
|
||||
ColorPreference.lapis: translate('themes.lapis'),
|
||||
ColorPreference.eggplant: translate('themes.eggplant'),
|
||||
ColorPreference.lime: translate('themes.lime'),
|
||||
ColorPreference.grim: translate('themes.grim'),
|
||||
ColorPreference.contrast: translate('themes.contrast')
|
||||
};
|
||||
|
||||
return colorPrefs
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(colorNames[e]!)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<DropdownMenuItem<dynamic>> _getBrightnessDropdownItems() {
|
||||
const brightnessPrefs = BrightnessPreference.values;
|
||||
final brightnessNames = {
|
||||
BrightnessPreference.system: translate('brightness.system'),
|
||||
BrightnessPreference.light: translate('brightness.light'),
|
||||
BrightnessPreference.dark: translate('brightness.dark')
|
||||
};
|
||||
|
||||
return brightnessPrefs
|
||||
.map(
|
||||
(e) => DropdownMenuItem(value: e, child: Text(brightnessNames[e]!)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.watch(windowControlProvider);
|
||||
final themeService = ref.watch(themeServiceProvider).valueOrNull;
|
||||
if (themeService == null) {
|
||||
return waitingPage(context);
|
||||
}
|
||||
final themePreferences = themeService.load();
|
||||
|
||||
return ThemeSwitchingArea(
|
||||
child: Scaffold(
|
||||
// resizeToAvoidBottomInset: false,
|
||||
appBar: DefaultAppBar(
|
||||
title: Text(translate('settings_page.titlebar')),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.pop<void>(),
|
||||
),
|
||||
actions: <Widget>[
|
||||
const SignalStrengthMeterWidget().paddingLTRB(16, 0, 16, 0),
|
||||
]),
|
||||
|
||||
body: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
ThemeSwitcher.withTheme(
|
||||
builder: (_, switcher, theme) => FormBuilderDropdown(
|
||||
name: formFieldTheme,
|
||||
decoration: InputDecoration(
|
||||
label: Text(translate('settings_page.color_theme'))),
|
||||
items: _getThemeDropdownItems(),
|
||||
initialValue: themePreferences.colorPreference,
|
||||
onChanged: (value) async {
|
||||
final newPrefs = themePreferences.copyWith(
|
||||
colorPreference: value as ColorPreference);
|
||||
await themeService.save(newPrefs);
|
||||
switcher.changeTheme(theme: themeService.get(newPrefs));
|
||||
ref.invalidate(themeServiceProvider);
|
||||
setState(() {});
|
||||
})),
|
||||
ThemeSwitcher.withTheme(
|
||||
builder: (_, switcher, theme) => FormBuilderDropdown(
|
||||
name: formFieldBrightness,
|
||||
decoration: InputDecoration(
|
||||
label:
|
||||
Text(translate('settings_page.brightness_mode'))),
|
||||
items: _getBrightnessDropdownItems(),
|
||||
initialValue: themePreferences.brightnessPreference,
|
||||
onChanged: (value) async {
|
||||
final newPrefs = themePreferences.copyWith(
|
||||
brightnessPreference: value as BrightnessPreference);
|
||||
await themeService.save(newPrefs);
|
||||
switcher.changeTheme(theme: themeService.get(newPrefs));
|
||||
ref.invalidate(themeServiceProvider);
|
||||
setState(() {});
|
||||
})),
|
||||
],
|
||||
),
|
||||
).paddingSymmetric(horizontal: 24, vertical: 8),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<bool>('isInAsyncCall', isInAsyncCall));
|
||||
}
|
||||
}
|
145
lib/old_to_refactor/providers/account.dart
Normal file
145
lib/old_to_refactor/providers/account.dart
Normal file
|
@ -0,0 +1,145 @@
|
|||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../entities/local_account.dart';
|
||||
import '../../entities/user_login.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../veilid_support/veilid_support.dart';
|
||||
import '../../local_accounts/local_accounts.dart';
|
||||
import 'logins.dart';
|
||||
|
||||
part 'account.g.dart';
|
||||
|
||||
enum AccountInfoStatus {
|
||||
noAccount,
|
||||
accountInvalid,
|
||||
accountLocked,
|
||||
accountReady,
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AccountInfo {
|
||||
const AccountInfo({
|
||||
required this.status,
|
||||
required this.active,
|
||||
this.account,
|
||||
});
|
||||
|
||||
final AccountInfoStatus status;
|
||||
final bool active;
|
||||
final proto.Account? account;
|
||||
}
|
||||
|
||||
/// Get an account from the identity key and if it is logged in and we
|
||||
/// have its secret available, return the account record contents
|
||||
@riverpod
|
||||
Future<AccountInfo> fetchAccountInfo(FetchAccountInfoRef ref,
|
||||
{required TypedKey accountMasterRecordKey}) async {
|
||||
// Get which local account we want to fetch the profile for
|
||||
final localAccount = await ref.watch(
|
||||
fetchLocalAccountProvider(accountMasterRecordKey: accountMasterRecordKey)
|
||||
.future);
|
||||
if (localAccount == null) {
|
||||
// Local account does not exist
|
||||
return const AccountInfo(
|
||||
status: AccountInfoStatus.noAccount, active: false);
|
||||
}
|
||||
|
||||
// See if we've logged into this account or if it is locked
|
||||
final activeUserLogin = await ref.watch(loginsProvider.future
|
||||
.select((value) async => (await value).activeUserLogin));
|
||||
final active = activeUserLogin == accountMasterRecordKey;
|
||||
|
||||
final login = await ref.watch(
|
||||
fetchLoginProvider(accountMasterRecordKey: accountMasterRecordKey)
|
||||
.future);
|
||||
if (login == null) {
|
||||
// Account was locked
|
||||
return AccountInfo(status: AccountInfoStatus.accountLocked, active: active);
|
||||
}
|
||||
|
||||
// Pull the account DHT key, decode it and return it
|
||||
final pool = await DHTRecordPool.instance();
|
||||
final account = await (await pool.openOwned(
|
||||
login.accountRecordInfo.accountRecord,
|
||||
parent: localAccount.identityMaster.identityRecordKey))
|
||||
.scope((accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer));
|
||||
if (account == null) {
|
||||
// Account could not be read or decrypted from DHT
|
||||
ref.invalidateSelf();
|
||||
return AccountInfo(
|
||||
status: AccountInfoStatus.accountInvalid, active: active);
|
||||
}
|
||||
|
||||
// Got account, decrypted and decoded
|
||||
return AccountInfo(
|
||||
status: AccountInfoStatus.accountReady, active: active, account: account);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ActiveAccountInfo {
|
||||
const ActiveAccountInfo({
|
||||
required this.localAccount,
|
||||
required this.userLogin,
|
||||
required this.account,
|
||||
});
|
||||
//
|
||||
|
||||
KeyPair getConversationWriter() {
|
||||
final identityKey = localAccount.identityMaster.identityPublicKey;
|
||||
final identitySecret = userLogin.identitySecret;
|
||||
return KeyPair(key: identityKey, secret: identitySecret.value);
|
||||
}
|
||||
|
||||
//
|
||||
final LocalAccount localAccount;
|
||||
final UserLogin userLogin;
|
||||
final proto.Account account;
|
||||
}
|
||||
|
||||
/// Get the active account info
|
||||
@riverpod
|
||||
Future<ActiveAccountInfo?> fetchActiveAccountInfo(
|
||||
FetchActiveAccountInfoRef ref) async {
|
||||
// See if we've logged into this account or if it is locked
|
||||
final activeUserLogin = await ref.watch(loginsProvider.future
|
||||
.select((value) async => (await value).activeUserLogin));
|
||||
if (activeUserLogin == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the user login
|
||||
final userLogin = await ref.watch(
|
||||
fetchLoginProvider(accountMasterRecordKey: activeUserLogin).future);
|
||||
if (userLogin == null) {
|
||||
// Account was locked
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get which local account we want to fetch the profile for
|
||||
final localAccount = await ref.watch(
|
||||
fetchLocalAccountProvider(accountMasterRecordKey: activeUserLogin)
|
||||
.future);
|
||||
if (localAccount == null) {
|
||||
// Local account does not exist
|
||||
return null;
|
||||
}
|
||||
|
||||
// Pull the account DHT key, decode it and return it
|
||||
final pool = await DHTRecordPool.instance();
|
||||
final account = await (await pool.openOwned(
|
||||
userLogin.accountRecordInfo.accountRecord,
|
||||
parent: localAccount.identityMaster.identityRecordKey))
|
||||
.scope((accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer));
|
||||
if (account == null) {
|
||||
ref.invalidateSelf();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Got account, decrypted and decoded
|
||||
return ActiveAccountInfo(
|
||||
localAccount: localAccount,
|
||||
userLogin: userLogin,
|
||||
account: account,
|
||||
);
|
||||
}
|
118
lib/old_to_refactor/providers/chat.dart
Normal file
118
lib/old_to_refactor/providers/chat.dart
Normal file
|
@ -0,0 +1,118 @@
|
|||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
|
||||
import '../../veilid_support/veilid_support.dart';
|
||||
import 'account.dart';
|
||||
|
||||
part 'chat.g.dart';
|
||||
|
||||
/// Create a new chat (singleton for single contact chats)
|
||||
Future<void> getOrCreateChatSingleContact({
|
||||
required ActiveAccountInfo activeAccountInfo,
|
||||
required TypedKey remoteConversationRecordKey,
|
||||
}) async {
|
||||
final accountRecordKey =
|
||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
// Create conversation type Chat
|
||||
final chat = proto.Chat()
|
||||
..type = proto.ChatType.SINGLE_CONTACT
|
||||
..remoteConversationKey = remoteConversationRecordKey.toProto();
|
||||
|
||||
// Add Chat to account's list
|
||||
// if this fails, don't keep retrying, user can try again later
|
||||
await (await DHTShortArray.openOwned(
|
||||
proto.OwnedDHTRecordPointerProto.fromProto(
|
||||
activeAccountInfo.account.chatList),
|
||||
parent: accountRecordKey))
|
||||
.scope((chatList) async {
|
||||
for (var i = 0; i < chatList.length; i++) {
|
||||
final cbuf = await chatList.getItem(i);
|
||||
if (cbuf == null) {
|
||||
throw Exception('Failed to get chat');
|
||||
}
|
||||
final c = proto.Chat.fromBuffer(cbuf);
|
||||
if (c == chat) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (await chatList.tryAddItem(chat.writeToBuffer()) == false) {
|
||||
throw Exception('Failed to add chat');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Delete a chat
|
||||
Future<void> deleteChat(
|
||||
{required ActiveAccountInfo activeAccountInfo,
|
||||
required TypedKey remoteConversationRecordKey}) async {
|
||||
final accountRecordKey =
|
||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
// Create conversation type Chat
|
||||
final remoteConversationKey = remoteConversationRecordKey.toProto();
|
||||
|
||||
// Add Chat to account's list
|
||||
// if this fails, don't keep retrying, user can try again later
|
||||
await (await DHTShortArray.openOwned(
|
||||
proto.OwnedDHTRecordPointerProto.fromProto(
|
||||
activeAccountInfo.account.chatList),
|
||||
parent: accountRecordKey))
|
||||
.scope((chatList) async {
|
||||
for (var i = 0; i < chatList.length; i++) {
|
||||
final cbuf = await chatList.getItem(i);
|
||||
if (cbuf == null) {
|
||||
throw Exception('Failed to get chat');
|
||||
}
|
||||
final c = proto.Chat.fromBuffer(cbuf);
|
||||
if (c.remoteConversationKey == remoteConversationKey) {
|
||||
await chatList.tryRemoveItem(i);
|
||||
|
||||
if (activeChatState.state == remoteConversationRecordKey) {
|
||||
activeChatState.state = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the active account contact list
|
||||
@riverpod
|
||||
Future<IList<proto.Chat>?> fetchChatList(FetchChatListRef ref) async {
|
||||
// See if we've logged into this account or if it is locked
|
||||
final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
return null;
|
||||
}
|
||||
final accountRecordKey =
|
||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
// Decode the chat list from the DHT
|
||||
IList<proto.Chat> out = const IListConst([]);
|
||||
await (await DHTShortArray.openOwned(
|
||||
proto.OwnedDHTRecordPointerProto.fromProto(
|
||||
activeAccountInfo.account.chatList),
|
||||
parent: accountRecordKey))
|
||||
.scope((cList) async {
|
||||
for (var i = 0; i < cList.length; i++) {
|
||||
final cir = await cList.getItem(i);
|
||||
if (cir == null) {
|
||||
throw Exception('Failed to get chat');
|
||||
}
|
||||
out = out.add(proto.Chat.fromBuffer(cir));
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// The selected chat
|
||||
final activeChatState = StateController<TypedKey?>(null);
|
||||
final activeChatStateProvider =
|
||||
StateNotifierProvider<StateController<TypedKey?>, TypedKey?>(
|
||||
(ref) => activeChatState);
|
29
lib/old_to_refactor/providers/connection_state.dart
Normal file
29
lib/old_to_refactor/providers/connection_state.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../veilid_support/veilid_support.dart';
|
||||
|
||||
part 'connection_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class ConnectionState with _$ConnectionState {
|
||||
const factory ConnectionState({
|
||||
required VeilidStateAttachment attachment,
|
||||
}) = _ConnectionState;
|
||||
const ConnectionState._();
|
||||
|
||||
bool get isAttached => !(attachment.state == AttachmentState.detached ||
|
||||
attachment.state == AttachmentState.detaching ||
|
||||
attachment.state == AttachmentState.attaching);
|
||||
|
||||
bool get isPublicInternetReady => attachment.publicInternetReady;
|
||||
}
|
||||
|
||||
final connectionState = StateController<ConnectionState>(const ConnectionState(
|
||||
attachment: VeilidStateAttachment(
|
||||
state: AttachmentState.detached,
|
||||
publicInternetReady: false,
|
||||
localNetworkReady: false)));
|
||||
final connectionStateProvider =
|
||||
StateNotifierProvider<StateController<ConnectionState>, ConnectionState>(
|
||||
(ref) => connectionState);
|
150
lib/old_to_refactor/providers/connection_state.freezed.dart
Normal file
150
lib/old_to_refactor/providers/connection_state.freezed.dart
Normal file
|
@ -0,0 +1,150 @@
|
|||
// 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 'connection_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#custom-getters-and-methods');
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ConnectionState {
|
||||
VeilidStateAttachment get attachment => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$ConnectionStateCopyWith<ConnectionState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $ConnectionStateCopyWith<$Res> {
|
||||
factory $ConnectionStateCopyWith(
|
||||
ConnectionState value, $Res Function(ConnectionState) then) =
|
||||
_$ConnectionStateCopyWithImpl<$Res, ConnectionState>;
|
||||
@useResult
|
||||
$Res call({VeilidStateAttachment attachment});
|
||||
|
||||
$VeilidStateAttachmentCopyWith<$Res> get attachment;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ConnectionStateCopyWithImpl<$Res, $Val extends ConnectionState>
|
||||
implements $ConnectionStateCopyWith<$Res> {
|
||||
_$ConnectionStateCopyWithImpl(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? attachment = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
attachment: null == attachment
|
||||
? _value.attachment
|
||||
: attachment // ignore: cast_nullable_to_non_nullable
|
||||
as VeilidStateAttachment,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$VeilidStateAttachmentCopyWith<$Res> get attachment {
|
||||
return $VeilidStateAttachmentCopyWith<$Res>(_value.attachment, (value) {
|
||||
return _then(_value.copyWith(attachment: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$ConnectionStateImplCopyWith<$Res>
|
||||
implements $ConnectionStateCopyWith<$Res> {
|
||||
factory _$$ConnectionStateImplCopyWith(_$ConnectionStateImpl value,
|
||||
$Res Function(_$ConnectionStateImpl) then) =
|
||||
__$$ConnectionStateImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({VeilidStateAttachment attachment});
|
||||
|
||||
@override
|
||||
$VeilidStateAttachmentCopyWith<$Res> get attachment;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$ConnectionStateImplCopyWithImpl<$Res>
|
||||
extends _$ConnectionStateCopyWithImpl<$Res, _$ConnectionStateImpl>
|
||||
implements _$$ConnectionStateImplCopyWith<$Res> {
|
||||
__$$ConnectionStateImplCopyWithImpl(
|
||||
_$ConnectionStateImpl _value, $Res Function(_$ConnectionStateImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? attachment = null,
|
||||
}) {
|
||||
return _then(_$ConnectionStateImpl(
|
||||
attachment: null == attachment
|
||||
? _value.attachment
|
||||
: attachment // ignore: cast_nullable_to_non_nullable
|
||||
as VeilidStateAttachment,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$ConnectionStateImpl extends _ConnectionState {
|
||||
const _$ConnectionStateImpl({required this.attachment}) : super._();
|
||||
|
||||
@override
|
||||
final VeilidStateAttachment attachment;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConnectionState(attachment: $attachment)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$ConnectionStateImpl &&
|
||||
(identical(other.attachment, attachment) ||
|
||||
other.attachment == attachment));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, attachment);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$ConnectionStateImplCopyWith<_$ConnectionStateImpl> get copyWith =>
|
||||
__$$ConnectionStateImplCopyWithImpl<_$ConnectionStateImpl>(
|
||||
this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _ConnectionState extends ConnectionState {
|
||||
const factory _ConnectionState(
|
||||
{required final VeilidStateAttachment attachment}) =
|
||||
_$ConnectionStateImpl;
|
||||
const _ConnectionState._() : super._();
|
||||
|
||||
@override
|
||||
VeilidStateAttachment get attachment;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$ConnectionStateImplCopyWith<_$ConnectionStateImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
132
lib/old_to_refactor/providers/contact.dart
Normal file
132
lib/old_to_refactor/providers/contact.dart
Normal file
|
@ -0,0 +1,132 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
|
||||
import '../../veilid_support/veilid_support.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import 'account.dart';
|
||||
import 'chat.dart';
|
||||
|
||||
part 'contact.g.dart';
|
||||
|
||||
Future<void> createContact({
|
||||
required ActiveAccountInfo activeAccountInfo,
|
||||
required proto.Profile profile,
|
||||
required IdentityMaster remoteIdentity,
|
||||
required TypedKey remoteConversationRecordKey,
|
||||
required TypedKey localConversationRecordKey,
|
||||
}) async {
|
||||
final accountRecordKey =
|
||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
// Create Contact
|
||||
final contact = proto.Contact()
|
||||
..editedProfile = profile
|
||||
..remoteProfile = profile
|
||||
..identityMasterJson = jsonEncode(remoteIdentity.toJson())
|
||||
..identityPublicKey = TypedKey(
|
||||
kind: remoteIdentity.identityRecordKey.kind,
|
||||
value: remoteIdentity.identityPublicKey)
|
||||
.toProto()
|
||||
..remoteConversationRecordKey = remoteConversationRecordKey.toProto()
|
||||
..localConversationRecordKey = localConversationRecordKey.toProto()
|
||||
..showAvailability = false;
|
||||
|
||||
// Add Contact to account's list
|
||||
// if this fails, don't keep retrying, user can try again later
|
||||
await (await DHTShortArray.openOwned(
|
||||
proto.OwnedDHTRecordPointerProto.fromProto(
|
||||
activeAccountInfo.account.contactList),
|
||||
parent: accountRecordKey))
|
||||
.scope((contactList) async {
|
||||
if (await contactList.tryAddItem(contact.writeToBuffer()) == false) {
|
||||
throw Exception('Failed to add contact');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteContact(
|
||||
{required ActiveAccountInfo activeAccountInfo,
|
||||
required proto.Contact contact}) async {
|
||||
final pool = await DHTRecordPool.instance();
|
||||
final accountRecordKey =
|
||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
final localConversationKey =
|
||||
proto.TypedKeyProto.fromProto(contact.localConversationRecordKey);
|
||||
final remoteConversationKey =
|
||||
proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey);
|
||||
|
||||
// Remove any chats for this contact
|
||||
await deleteChat(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
remoteConversationRecordKey: remoteConversationKey);
|
||||
|
||||
// Remove Contact from account's list
|
||||
await (await DHTShortArray.openOwned(
|
||||
proto.OwnedDHTRecordPointerProto.fromProto(
|
||||
activeAccountInfo.account.contactList),
|
||||
parent: accountRecordKey))
|
||||
.scope((contactList) async {
|
||||
for (var i = 0; i < contactList.length; i++) {
|
||||
final item =
|
||||
await contactList.getItemProtobuf(proto.Contact.fromBuffer, i);
|
||||
if (item == null) {
|
||||
throw Exception('Failed to get contact');
|
||||
}
|
||||
if (item.remoteConversationRecordKey ==
|
||||
contact.remoteConversationRecordKey) {
|
||||
await contactList.tryRemoveItem(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await (await pool.openRead(localConversationKey,
|
||||
parent: accountRecordKey))
|
||||
.delete();
|
||||
} on Exception catch (e) {
|
||||
log.debug('error removing local conversation record key: $e', e);
|
||||
}
|
||||
try {
|
||||
if (localConversationKey != remoteConversationKey) {
|
||||
await (await pool.openRead(remoteConversationKey,
|
||||
parent: accountRecordKey))
|
||||
.delete();
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
log.debug('error removing remote conversation record key: $e', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the active account contact list
|
||||
@riverpod
|
||||
Future<IList<proto.Contact>?> fetchContactList(FetchContactListRef ref) async {
|
||||
// See if we've logged into this account or if it is locked
|
||||
final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
return null;
|
||||
}
|
||||
final accountRecordKey =
|
||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
// Decode the contact list from the DHT
|
||||
IList<proto.Contact> out = const IListConst([]);
|
||||
await (await DHTShortArray.openOwned(
|
||||
proto.OwnedDHTRecordPointerProto.fromProto(
|
||||
activeAccountInfo.account.contactList),
|
||||
parent: accountRecordKey))
|
||||
.scope((cList) async {
|
||||
for (var i = 0; i < cList.length; i++) {
|
||||
final cir = await cList.getItem(i);
|
||||
if (cir == null) {
|
||||
throw Exception('Failed to get contact');
|
||||
}
|
||||
out = out.add(proto.Contact.fromBuffer(cir));
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
|
@ -0,0 +1,583 @@
|
|||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
|
||||
import '../../entities/entities.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_support/veilid_support.dart';
|
||||
import 'account.dart';
|
||||
|
||||
part 'contact_invitation_list_manager.g.dart';
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
class ContactInviteInvalidKeyException implements Exception {
|
||||
const ContactInviteInvalidKeyException(this.type) : super();
|
||||
final EncryptionKeyType type;
|
||||
}
|
||||
|
||||
typedef GetEncryptionKeyCallback = Future<SecretKey?> Function(
|
||||
VeilidCryptoSystem cs,
|
||||
EncryptionKeyType encryptionKeyType,
|
||||
Uint8List encryptedSecret);
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
@immutable
|
||||
class AcceptedContact {
|
||||
const AcceptedContact({
|
||||
required this.profile,
|
||||
required this.remoteIdentity,
|
||||
required this.remoteConversationRecordKey,
|
||||
required this.localConversationRecordKey,
|
||||
});
|
||||
|
||||
final proto.Profile profile;
|
||||
final IdentityMaster remoteIdentity;
|
||||
final TypedKey remoteConversationRecordKey;
|
||||
final TypedKey localConversationRecordKey;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class InvitationStatus {
|
||||
const InvitationStatus({required this.acceptedContact});
|
||||
final AcceptedContact? acceptedContact;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// Mutable state for per-account contact invitations
|
||||
@riverpod
|
||||
class ContactInvitationListManager extends _$ContactInvitationListManager {
|
||||
ContactInvitationListManager._({
|
||||
required ActiveAccountInfo activeAccountInfo,
|
||||
required DHTShortArray dhtRecord,
|
||||
}) : _activeAccountInfo = activeAccountInfo,
|
||||
_dhtRecord = dhtRecord,
|
||||
_records = IList();
|
||||
|
||||
@override
|
||||
FutureOr<IList<proto.ContactInvitationRecord>> build(
|
||||
ActiveAccountInfo activeAccountInfo) async {
|
||||
// Load initial todo list from the remote repository
|
||||
ref.onDispose xxxx call close and pass dhtrecord through... could use a context object
|
||||
and a DHTValueChangeProvider that we watch in build that updates when dht records change
|
||||
return _open(activeAccountInfo);
|
||||
}
|
||||
|
||||
static Future<ContactInvitationListManager> _open(
|
||||
ActiveAccountInfo activeAccountInfo) async {
|
||||
final accountRecordKey =
|
||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
final dhtRecord = await DHTShortArray.openOwned(
|
||||
proto.OwnedDHTRecordPointerProto.fromProto(
|
||||
activeAccountInfo.account.contactInvitationRecords),
|
||||
parent: accountRecordKey);
|
||||
|
||||
return ContactInvitationListManager._(
|
||||
activeAccountInfo: activeAccountInfo, dhtRecord: dhtRecord);
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
state = "";
|
||||
await _dhtRecord.close();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
for (var i = 0; i < _dhtRecord.length; i++) {
|
||||
final cir = await _dhtRecord.getItem(i);
|
||||
if (cir == null) {
|
||||
throw Exception('Failed to get contact invitation record');
|
||||
}
|
||||
_records = _records.add(proto.ContactInvitationRecord.fromBuffer(cir));
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> createInvitation(
|
||||
{required EncryptionKeyType encryptionKeyType,
|
||||
required String encryptionKey,
|
||||
required String message,
|
||||
required Timestamp? expiration}) async {
|
||||
final pool = await DHTRecordPool.instance();
|
||||
final accountRecordKey =
|
||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
final identityKey =
|
||||
_activeAccountInfo.localAccount.identityMaster.identityPublicKey;
|
||||
final identitySecret = _activeAccountInfo.userLogin.identitySecret.value;
|
||||
|
||||
// Generate writer keypair to share with new contact
|
||||
final cs = await pool.veilid.bestCryptoSystem();
|
||||
final contactRequestWriter = await cs.generateKeyPair();
|
||||
final conversationWriter = _activeAccountInfo.getConversationWriter();
|
||||
|
||||
// Encrypt the writer secret with the encryption key
|
||||
final encryptedSecret = await encryptSecretToBytes(
|
||||
secret: contactRequestWriter.secret,
|
||||
cryptoKind: cs.kind(),
|
||||
encryptionKey: encryptionKey,
|
||||
encryptionKeyType: encryptionKeyType);
|
||||
|
||||
// Create local chat DHT record with the account record key as its parent
|
||||
// Do not set the encryption of this key yet as it will not yet be written
|
||||
// to and it will be eventually encrypted with the DH of the contact's
|
||||
// identity key
|
||||
late final Uint8List signedContactInvitationBytes;
|
||||
await (await pool.create(
|
||||
parent: accountRecordKey,
|
||||
schema: DHTSchema.smpl(oCnt: 0, members: [
|
||||
DHTSchemaMember(mKey: conversationWriter.key, mCnt: 1)
|
||||
])))
|
||||
.deleteScope((localConversation) async {
|
||||
// dont bother reopening localConversation with writer
|
||||
// Make ContactRequestPrivate and encrypt with the writer secret
|
||||
final crpriv = proto.ContactRequestPrivate()
|
||||
..writerKey = contactRequestWriter.key.toProto()
|
||||
..profile = _activeAccountInfo.account.profile
|
||||
..identityMasterRecordKey =
|
||||
_activeAccountInfo.userLogin.accountMasterRecordKey.toProto()
|
||||
..chatRecordKey = localConversation.key.toProto()
|
||||
..expiration = expiration?.toInt64() ?? Int64.ZERO;
|
||||
final crprivbytes = crpriv.writeToBuffer();
|
||||
final encryptedContactRequestPrivate = await cs.encryptAeadWithNonce(
|
||||
crprivbytes, contactRequestWriter.secret);
|
||||
|
||||
// Create ContactRequest and embed contactrequestprivate
|
||||
final creq = proto.ContactRequest()
|
||||
..encryptionKeyType = encryptionKeyType.toProto()
|
||||
..private = encryptedContactRequestPrivate;
|
||||
|
||||
// Create DHT unicast inbox for ContactRequest
|
||||
await (await pool.create(
|
||||
parent: accountRecordKey,
|
||||
schema: DHTSchema.smpl(oCnt: 1, members: [
|
||||
DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key)
|
||||
]),
|
||||
crypto: const DHTRecordCryptoPublic()))
|
||||
.deleteScope((contactRequestInbox) async {
|
||||
// Store ContactRequest in owner subkey
|
||||
await contactRequestInbox.eventualWriteProtobuf(creq);
|
||||
|
||||
// Create ContactInvitation and SignedContactInvitation
|
||||
final cinv = proto.ContactInvitation()
|
||||
..contactRequestInboxKey = contactRequestInbox.key.toProto()
|
||||
..writerSecret = encryptedSecret;
|
||||
final cinvbytes = cinv.writeToBuffer();
|
||||
final scinv = proto.SignedContactInvitation()
|
||||
..contactInvitation = cinvbytes
|
||||
..identitySignature =
|
||||
(await cs.sign(identityKey, identitySecret, cinvbytes)).toProto();
|
||||
signedContactInvitationBytes = scinv.writeToBuffer();
|
||||
|
||||
// Create ContactInvitationRecord
|
||||
final cinvrec = proto.ContactInvitationRecord()
|
||||
..contactRequestInbox =
|
||||
contactRequestInbox.ownedDHTRecordPointer.toProto()
|
||||
..writerKey = contactRequestWriter.key.toProto()
|
||||
..writerSecret = contactRequestWriter.secret.toProto()
|
||||
..localConversationRecordKey = localConversation.key.toProto()
|
||||
..expiration = expiration?.toInt64() ?? Int64.ZERO
|
||||
..invitation = signedContactInvitationBytes
|
||||
..message = message;
|
||||
|
||||
// Add ContactInvitationRecord to account's list
|
||||
// if this fails, don't keep retrying, user can try again later
|
||||
await (await DHTShortArray.openOwned(
|
||||
proto.OwnedDHTRecordPointerProto.fromProto(
|
||||
_activeAccountInfo.account.contactInvitationRecords),
|
||||
parent: accountRecordKey))
|
||||
.scope((cirList) async {
|
||||
if (await cirList.tryAddItem(cinvrec.writeToBuffer()) == false) {
|
||||
throw Exception('Failed to add contact invitation record');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return signedContactInvitationBytes;
|
||||
}
|
||||
|
||||
Future<void> deleteInvitation(
|
||||
{required bool accepted,
|
||||
required proto.ContactInvitationRecord contactInvitationRecord}) async {
|
||||
final pool = await DHTRecordPool.instance();
|
||||
final accountRecordKey =
|
||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
// Remove ContactInvitationRecord from account's list
|
||||
await (await DHTShortArray.openOwned(
|
||||
proto.OwnedDHTRecordPointerProto.fromProto(
|
||||
_activeAccountInfo.account.contactInvitationRecords),
|
||||
parent: accountRecordKey))
|
||||
.scope((cirList) async {
|
||||
for (var i = 0; i < cirList.length; i++) {
|
||||
final item = await cirList.getItemProtobuf(
|
||||
proto.ContactInvitationRecord.fromBuffer, i);
|
||||
if (item == null) {
|
||||
throw Exception('Failed to get contact invitation record');
|
||||
}
|
||||
if (item.contactRequestInbox.recordKey ==
|
||||
contactInvitationRecord.contactRequestInbox.recordKey) {
|
||||
await cirList.tryRemoveItem(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
await (await pool.openOwned(
|
||||
proto.OwnedDHTRecordPointerProto.fromProto(
|
||||
contactInvitationRecord.contactRequestInbox),
|
||||
parent: accountRecordKey))
|
||||
.scope((contactRequestInbox) async {
|
||||
// Wipe out old invitation so it shows up as invalid
|
||||
await contactRequestInbox.tryWriteBytes(Uint8List(0));
|
||||
await contactRequestInbox.delete();
|
||||
});
|
||||
if (!accepted) {
|
||||
await (await pool.openRead(
|
||||
proto.TypedKeyProto.fromProto(
|
||||
contactInvitationRecord.localConversationRecordKey),
|
||||
parent: accountRecordKey))
|
||||
.delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<ValidContactInvitation?> validateInvitation(
|
||||
{required Uint8List inviteData,
|
||||
required GetEncryptionKeyCallback getEncryptionKeyCallback}) async {
|
||||
final accountRecordKey =
|
||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
final signedContactInvitation =
|
||||
proto.SignedContactInvitation.fromBuffer(inviteData);
|
||||
|
||||
final contactInvitationBytes =
|
||||
Uint8List.fromList(signedContactInvitation.contactInvitation);
|
||||
final contactInvitation =
|
||||
proto.ContactInvitation.fromBuffer(contactInvitationBytes);
|
||||
|
||||
final contactRequestInboxKey =
|
||||
proto.TypedKeyProto.fromProto(contactInvitation.contactRequestInboxKey);
|
||||
|
||||
ValidContactInvitation? out;
|
||||
|
||||
final pool = await DHTRecordPool.instance();
|
||||
final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
|
||||
|
||||
// See if we're chatting to ourselves, if so, don't delete it here
|
||||
final isSelf = _records.indexWhere((cir) =>
|
||||
proto.TypedKeyProto.fromProto(cir.contactRequestInbox.recordKey) ==
|
||||
contactRequestInboxKey) !=
|
||||
-1;
|
||||
|
||||
await (await pool.openRead(contactRequestInboxKey,
|
||||
parent: accountRecordKey))
|
||||
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
||||
//
|
||||
final contactRequest = await contactRequestInbox
|
||||
.getProtobuf(proto.ContactRequest.fromBuffer);
|
||||
|
||||
// Decrypt contact request private
|
||||
final encryptionKeyType =
|
||||
EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType);
|
||||
late final SharedSecret? writerSecret;
|
||||
try {
|
||||
writerSecret = await getEncryptionKeyCallback(cs, encryptionKeyType,
|
||||
Uint8List.fromList(contactInvitation.writerSecret));
|
||||
} on Exception catch (_) {
|
||||
throw ContactInviteInvalidKeyException(encryptionKeyType);
|
||||
}
|
||||
if (writerSecret == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final contactRequestPrivateBytes = await cs.decryptAeadWithNonce(
|
||||
Uint8List.fromList(contactRequest.private), writerSecret);
|
||||
|
||||
final contactRequestPrivate =
|
||||
proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes);
|
||||
final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto(
|
||||
contactRequestPrivate.identityMasterRecordKey);
|
||||
|
||||
// Fetch the account master
|
||||
final contactIdentityMaster = await openIdentityMaster(
|
||||
identityMasterRecordKey: contactIdentityMasterRecordKey);
|
||||
|
||||
// Verify
|
||||
final signature = proto.SignatureProto.fromProto(
|
||||
signedContactInvitation.identitySignature);
|
||||
await cs.verify(contactIdentityMaster.identityPublicKey,
|
||||
contactInvitationBytes, signature);
|
||||
|
||||
final writer = KeyPair(
|
||||
key: proto.CryptoKeyProto.fromProto(contactRequestPrivate.writerKey),
|
||||
secret: writerSecret);
|
||||
|
||||
out = ValidContactInvitation._(
|
||||
contactInvitationManager: this,
|
||||
signedContactInvitation: signedContactInvitation,
|
||||
contactInvitation: contactInvitation,
|
||||
contactRequestInboxKey: contactRequestInboxKey,
|
||||
contactRequest: contactRequest,
|
||||
contactRequestPrivate: contactRequestPrivate,
|
||||
contactIdentityMaster: contactIdentityMaster,
|
||||
writer: writer);
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Future<InvitationStatus?> checkInvitationStatus(
|
||||
{required ActiveAccountInfo activeAccountInfo,
|
||||
required proto.ContactInvitationRecord contactInvitationRecord}) async {
|
||||
// Open the contact request inbox
|
||||
try {
|
||||
final pool = await DHTRecordPool.instance();
|
||||
final accountRecordKey =
|
||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
final writerKey =
|
||||
proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerKey);
|
||||
final writerSecret =
|
||||
proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerSecret);
|
||||
final recordKey = proto.TypedKeyProto.fromProto(
|
||||
contactInvitationRecord.contactRequestInbox.recordKey);
|
||||
final writer = TypedKeyPair(
|
||||
kind: recordKey.kind, key: writerKey, secret: writerSecret);
|
||||
final acceptReject = await (await pool.openRead(recordKey,
|
||||
crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer),
|
||||
parent: accountRecordKey,
|
||||
defaultSubkey: 1))
|
||||
.scope((contactRequestInbox) async {
|
||||
//
|
||||
final signedContactResponse = await contactRequestInbox.getProtobuf(
|
||||
proto.SignedContactResponse.fromBuffer,
|
||||
forceRefresh: true);
|
||||
if (signedContactResponse == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final contactResponseBytes =
|
||||
Uint8List.fromList(signedContactResponse.contactResponse);
|
||||
final contactResponse =
|
||||
proto.ContactResponse.fromBuffer(contactResponseBytes);
|
||||
final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto(
|
||||
contactResponse.identityMasterRecordKey);
|
||||
final cs = await pool.veilid.getCryptoSystem(recordKey.kind);
|
||||
|
||||
// Fetch the remote contact's account master
|
||||
final contactIdentityMaster = await openIdentityMaster(
|
||||
identityMasterRecordKey: contactIdentityMasterRecordKey);
|
||||
|
||||
// Verify
|
||||
final signature = proto.SignatureProto.fromProto(
|
||||
signedContactResponse.identitySignature);
|
||||
await cs.verify(contactIdentityMaster.identityPublicKey,
|
||||
contactResponseBytes, signature);
|
||||
|
||||
// Check for rejection
|
||||
if (!contactResponse.accept) {
|
||||
return const InvitationStatus(acceptedContact: null);
|
||||
}
|
||||
|
||||
// Pull profile from remote conversation key
|
||||
final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(
|
||||
contactResponse.remoteConversationRecordKey);
|
||||
final remoteConversation = await readRemoteConversation(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
remoteIdentityPublicKey:
|
||||
contactIdentityMaster.identityPublicTypedKey(),
|
||||
remoteConversationRecordKey: remoteConversationRecordKey);
|
||||
if (remoteConversation == null) {
|
||||
log.info('Remote conversation could not be read. Waiting...');
|
||||
return null;
|
||||
}
|
||||
// Complete the local conversation now that we have the remote profile
|
||||
final localConversationRecordKey = proto.TypedKeyProto.fromProto(
|
||||
contactInvitationRecord.localConversationRecordKey);
|
||||
return createConversation(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
remoteIdentityPublicKey:
|
||||
contactIdentityMaster.identityPublicTypedKey(),
|
||||
existingConversationRecordKey: localConversationRecordKey,
|
||||
// ignore: prefer_expression_function_bodies
|
||||
callback: (localConversation) async {
|
||||
return InvitationStatus(
|
||||
acceptedContact: AcceptedContact(
|
||||
profile: remoteConversation.profile,
|
||||
remoteIdentity: contactIdentityMaster,
|
||||
remoteConversationRecordKey: remoteConversationRecordKey,
|
||||
localConversationRecordKey: localConversationRecordKey));
|
||||
});
|
||||
});
|
||||
|
||||
if (acceptReject == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Delete invitation and return the accepted or rejected contact
|
||||
await deleteInvitation(
|
||||
accepted: acceptReject.acceptedContact != null,
|
||||
contactInvitationRecord: contactInvitationRecord);
|
||||
|
||||
return acceptReject;
|
||||
} on Exception catch (e) {
|
||||
log.error('Exception in checkAcceptRejectContact: $e', e);
|
||||
|
||||
// Attempt to clean up. All this needs better lifetime management
|
||||
await deleteInvitation(
|
||||
accepted: false, contactInvitationRecord: contactInvitationRecord);
|
||||
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
final ActiveAccountInfo _activeAccountInfo;
|
||||
final DHTShortArray _dhtRecord;
|
||||
IList<proto.ContactInvitationRecord> _records;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
///
|
||||
|
||||
class ValidContactInvitation {
|
||||
ValidContactInvitation._(
|
||||
{required ContactInvitationListManager contactInvitationManager,
|
||||
required proto.SignedContactInvitation signedContactInvitation,
|
||||
required proto.ContactInvitation contactInvitation,
|
||||
required TypedKey contactRequestInboxKey,
|
||||
required proto.ContactRequest contactRequest,
|
||||
required proto.ContactRequestPrivate contactRequestPrivate,
|
||||
required IdentityMaster contactIdentityMaster,
|
||||
required KeyPair writer})
|
||||
: _contactInvitationManager = contactInvitationManager,
|
||||
_signedContactInvitation = signedContactInvitation,
|
||||
_contactInvitation = contactInvitation,
|
||||
_contactRequestInboxKey = contactRequestInboxKey,
|
||||
_contactRequest = contactRequest,
|
||||
_contactRequestPrivate = contactRequestPrivate,
|
||||
_contactIdentityMaster = contactIdentityMaster,
|
||||
_writer = writer;
|
||||
|
||||
Future<AcceptedContact?> accept() async {
|
||||
final pool = await DHTRecordPool.instance();
|
||||
final activeAccountInfo = _contactInvitationManager._activeAccountInfo;
|
||||
try {
|
||||
// Ensure we don't delete this if we're trying to chat to self
|
||||
final isSelf = _contactIdentityMaster.identityPublicKey ==
|
||||
activeAccountInfo.localAccount.identityMaster.identityPublicKey;
|
||||
final accountRecordKey =
|
||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
return (await pool.openWrite(_contactRequestInboxKey, _writer,
|
||||
parent: accountRecordKey))
|
||||
// ignore: prefer_expression_function_bodies
|
||||
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
||||
// Create local conversation key for this
|
||||
// contact and send via contact response
|
||||
return createConversation(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
remoteIdentityPublicKey:
|
||||
_contactIdentityMaster.identityPublicTypedKey(),
|
||||
callback: (localConversation) async {
|
||||
final contactResponse = proto.ContactResponse()
|
||||
..accept = true
|
||||
..remoteConversationRecordKey = localConversation.key.toProto()
|
||||
..identityMasterRecordKey = activeAccountInfo
|
||||
.localAccount.identityMaster.masterRecordKey
|
||||
.toProto();
|
||||
final contactResponseBytes = contactResponse.writeToBuffer();
|
||||
|
||||
final cs = await pool.veilid
|
||||
.getCryptoSystem(_contactRequestInboxKey.kind);
|
||||
|
||||
final identitySignature = await cs.sign(
|
||||
activeAccountInfo
|
||||
.localAccount.identityMaster.identityPublicKey,
|
||||
activeAccountInfo.userLogin.identitySecret.value,
|
||||
contactResponseBytes);
|
||||
|
||||
final signedContactResponse = proto.SignedContactResponse()
|
||||
..contactResponse = contactResponseBytes
|
||||
..identitySignature = identitySignature.toProto();
|
||||
|
||||
// Write the acceptance to the inbox
|
||||
if (await contactRequestInbox.tryWriteProtobuf(
|
||||
proto.SignedContactResponse.fromBuffer,
|
||||
signedContactResponse,
|
||||
subkey: 1) !=
|
||||
null) {
|
||||
throw Exception('failed to accept contact invitation');
|
||||
}
|
||||
return AcceptedContact(
|
||||
profile: _contactRequestPrivate.profile,
|
||||
remoteIdentity: _contactIdentityMaster,
|
||||
remoteConversationRecordKey: proto.TypedKeyProto.fromProto(
|
||||
_contactRequestPrivate.chatRecordKey),
|
||||
localConversationRecordKey: localConversation.key,
|
||||
);
|
||||
});
|
||||
});
|
||||
} on Exception catch (e) {
|
||||
log.debug('exception: $e', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> reject() async {
|
||||
final pool = await DHTRecordPool.instance();
|
||||
final activeAccountInfo = _contactInvitationManager._activeAccountInfo;
|
||||
|
||||
// Ensure we don't delete this if we're trying to chat to self
|
||||
final isSelf = _contactIdentityMaster.identityPublicKey ==
|
||||
activeAccountInfo.localAccount.identityMaster.identityPublicKey;
|
||||
final accountRecordKey =
|
||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
return (await pool.openWrite(_contactRequestInboxKey, _writer,
|
||||
parent: accountRecordKey))
|
||||
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
||||
final cs =
|
||||
await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind);
|
||||
|
||||
final contactResponse = proto.ContactResponse()
|
||||
..accept = false
|
||||
..identityMasterRecordKey = activeAccountInfo
|
||||
.localAccount.identityMaster.masterRecordKey
|
||||
.toProto();
|
||||
final contactResponseBytes = contactResponse.writeToBuffer();
|
||||
|
||||
final identitySignature = await cs.sign(
|
||||
activeAccountInfo.localAccount.identityMaster.identityPublicKey,
|
||||
activeAccountInfo.userLogin.identitySecret.value,
|
||||
contactResponseBytes);
|
||||
|
||||
final signedContactResponse = proto.SignedContactResponse()
|
||||
..contactResponse = contactResponseBytes
|
||||
..identitySignature = identitySignature.toProto();
|
||||
|
||||
// Write the rejection to the inbox
|
||||
if (await contactRequestInbox.tryWriteProtobuf(
|
||||
proto.SignedContactResponse.fromBuffer, signedContactResponse,
|
||||
subkey: 1) !=
|
||||
null) {
|
||||
log.error('failed to reject contact invitation');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
ContactInvitationListManager _contactInvitationManager;
|
||||
proto.SignedContactInvitation _signedContactInvitation;
|
||||
proto.ContactInvitation _contactInvitation;
|
||||
TypedKey _contactRequestInboxKey;
|
||||
proto.ContactRequest _contactRequest;
|
||||
proto.ContactRequestPrivate _contactRequestPrivate;
|
||||
IdentityMaster _contactIdentityMaster;
|
||||
KeyPair _writer;
|
||||
}
|
56
lib/old_to_refactor/providers/contact_invite.dart
Normal file
56
lib/old_to_refactor/providers/contact_invite.dart
Normal file
|
@ -0,0 +1,56 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../entities/local_account.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_support/veilid_support.dart';
|
||||
import 'account.dart';
|
||||
import 'conversation.dart';
|
||||
|
||||
part 'contact_invite.g.dart';
|
||||
|
||||
/// Get the active account contact invitation list
|
||||
@riverpod
|
||||
Future<IList<proto.ContactInvitationRecord>?> fetchContactInvitationRecords(
|
||||
FetchContactInvitationRecordsRef ref) async {
|
||||
// See if we've logged into this account or if it is locked
|
||||
final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
return null;
|
||||
}
|
||||
final accountRecordKey =
|
||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
// Decode the contact invitation list from the DHT
|
||||
IList<proto.ContactInvitationRecord> out = const IListConst([]);
|
||||
|
||||
try {
|
||||
await (await DHTShortArray.openOwned(
|
||||
proto.OwnedDHTRecordPointerProto.fromProto(
|
||||
activeAccountInfo.account.contactInvitationRecords),
|
||||
parent: accountRecordKey))
|
||||
.scope((cirList) async {
|
||||
for (var i = 0; i < cirList.length; i++) {
|
||||
final cir = await cirList.getItem(i);
|
||||
if (cir == null) {
|
||||
throw Exception('Failed to get contact invitation record');
|
||||
}
|
||||
out = out.add(proto.ContactInvitationRecord.fromBuffer(cir));
|
||||
}
|
||||
});
|
||||
} on VeilidAPIExceptionTryAgain catch (_) {
|
||||
// Try again later
|
||||
ref.invalidateSelf();
|
||||
return null;
|
||||
} on Exception catch (_) {
|
||||
// Try again later
|
||||
ref.invalidateSelf();
|
||||
rethrow;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
355
lib/old_to_refactor/providers/conversation.dart
Normal file
355
lib/old_to_refactor/providers/conversation.dart
Normal file
|
@ -0,0 +1,355 @@
|
|||
// A Conversation is a type of Chat that is 1:1 between two Contacts only
|
||||
// Each Contact in the ContactList has at most one Conversation between the
|
||||
// remote contact and the local account
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_init.dart';
|
||||
import '../../veilid_support/veilid_support.dart';
|
||||
import 'account.dart';
|
||||
import 'chat.dart';
|
||||
import 'contact.dart';
|
||||
|
||||
part 'conversation.g.dart';
|
||||
|
||||
class Conversation {
|
||||
Conversation._(
|
||||
{required ActiveAccountInfo activeAccountInfo,
|
||||
required TypedKey localConversationRecordKey,
|
||||
required TypedKey remoteIdentityPublicKey,
|
||||
required TypedKey remoteConversationRecordKey})
|
||||
: _activeAccountInfo = activeAccountInfo,
|
||||
_localConversationRecordKey = localConversationRecordKey,
|
||||
_remoteIdentityPublicKey = remoteIdentityPublicKey,
|
||||
_remoteConversationRecordKey = remoteConversationRecordKey;
|
||||
|
||||
Future<Conversation> open() async {}
|
||||
|
||||
Future<void> close() async {
|
||||
//
|
||||
}
|
||||
|
||||
Future<proto.Conversation?> readRemoteConversation() async {
|
||||
final accountRecordKey =
|
||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
final pool = await DHTRecordPool.instance();
|
||||
|
||||
final crypto = await getConversationCrypto();
|
||||
return (await pool.openRead(_remoteConversationRecordKey,
|
||||
parent: accountRecordKey, crypto: crypto))
|
||||
.scope((remoteConversation) async {
|
||||
//
|
||||
final conversation =
|
||||
await remoteConversation.getProtobuf(proto.Conversation.fromBuffer);
|
||||
return conversation;
|
||||
});
|
||||
}
|
||||
|
||||
Future<proto.Conversation?> readLocalConversation() async {
|
||||
final accountRecordKey =
|
||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
final pool = await DHTRecordPool.instance();
|
||||
|
||||
final crypto = await getConversationCrypto();
|
||||
return (await pool.openRead(_localConversationRecordKey,
|
||||
parent: accountRecordKey, crypto: crypto))
|
||||
.scope((localConversation) async {
|
||||
//
|
||||
final update =
|
||||
await localConversation.getProtobuf(proto.Conversation.fromBuffer);
|
||||
if (update != null) {
|
||||
return update;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<proto.Conversation?> writeLocalConversation({
|
||||
required proto.Conversation conversation,
|
||||
}) async {
|
||||
final accountRecordKey =
|
||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
final pool = await DHTRecordPool.instance();
|
||||
|
||||
final crypto = await getConversationCrypto();
|
||||
final writer = _activeAccountInfo.getConversationWriter();
|
||||
|
||||
return (await pool.openWrite(_localConversationRecordKey, writer,
|
||||
parent: accountRecordKey, crypto: crypto))
|
||||
.scope((localConversation) async {
|
||||
//
|
||||
final update = await localConversation.tryWriteProtobuf(
|
||||
proto.Conversation.fromBuffer, conversation);
|
||||
if (update != null) {
|
||||
return update;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addLocalConversationMessage(
|
||||
{required proto.Message message}) async {
|
||||
final conversation = await readLocalConversation();
|
||||
if (conversation == null) {
|
||||
return;
|
||||
}
|
||||
final messagesRecordKey =
|
||||
proto.TypedKeyProto.fromProto(conversation.messages);
|
||||
final crypto = await getConversationCrypto();
|
||||
final writer = _activeAccountInfo.getConversationWriter();
|
||||
|
||||
await (await DHTShortArray.openWrite(messagesRecordKey, writer,
|
||||
parent: _localConversationRecordKey, crypto: crypto))
|
||||
.scope((messages) async {
|
||||
await messages.tryAddItem(message.writeToBuffer());
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> mergeLocalConversationMessages(
|
||||
{required IList<proto.Message> newMessages}) async {
|
||||
final conversation = await readLocalConversation();
|
||||
if (conversation == null) {
|
||||
return false;
|
||||
}
|
||||
var changed = false;
|
||||
final messagesRecordKey =
|
||||
proto.TypedKeyProto.fromProto(conversation.messages);
|
||||
final crypto = await getConversationCrypto();
|
||||
final writer = _activeAccountInfo.getConversationWriter();
|
||||
|
||||
newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp)
|
||||
.compareTo(Timestamp.fromInt64(b.timestamp)));
|
||||
|
||||
await (await DHTShortArray.openWrite(messagesRecordKey, writer,
|
||||
parent: _localConversationRecordKey, crypto: crypto))
|
||||
.scope((messages) async {
|
||||
// Ensure newMessages is sorted by timestamp
|
||||
newMessages =
|
||||
newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
// Existing messages will always be sorted by timestamp so merging is easy
|
||||
var pos = 0;
|
||||
outer:
|
||||
for (final newMessage in newMessages) {
|
||||
var skip = false;
|
||||
while (pos < messages.length) {
|
||||
final m =
|
||||
await messages.getItemProtobuf(proto.Message.fromBuffer, pos);
|
||||
if (m == null) {
|
||||
log.error('unable to get message #$pos');
|
||||
break outer;
|
||||
}
|
||||
|
||||
// If timestamp to insert is less than
|
||||
// the current position, insert it here
|
||||
final newTs = Timestamp.fromInt64(newMessage.timestamp);
|
||||
final curTs = Timestamp.fromInt64(m.timestamp);
|
||||
final cmp = newTs.compareTo(curTs);
|
||||
if (cmp < 0) {
|
||||
break;
|
||||
} else if (cmp == 0) {
|
||||
skip = true;
|
||||
break;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
// Insert at this position
|
||||
if (!skip) {
|
||||
await messages.tryInsertItem(pos, newMessage.writeToBuffer());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
return changed;
|
||||
}
|
||||
|
||||
Future<IList<proto.Message>?> getRemoteConversationMessages() async {
|
||||
final conversation = await readRemoteConversation();
|
||||
if (conversation == null) {
|
||||
return null;
|
||||
}
|
||||
final messagesRecordKey =
|
||||
proto.TypedKeyProto.fromProto(conversation.messages);
|
||||
final crypto = await getConversationCrypto();
|
||||
|
||||
return (await DHTShortArray.openRead(messagesRecordKey,
|
||||
parent: _remoteConversationRecordKey, crypto: crypto))
|
||||
.scope((messages) async {
|
||||
var out = IList<proto.Message>();
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i);
|
||||
if (msg == null) {
|
||||
throw Exception('Failed to get message');
|
||||
}
|
||||
out = out.add(msg);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
Future<DHTRecordCrypto> getConversationCrypto() async {
|
||||
var conversationCrypto = _conversationCrypto;
|
||||
if (conversationCrypto != null) {
|
||||
return conversationCrypto;
|
||||
}
|
||||
final veilid = await eventualVeilid.future;
|
||||
final identitySecret = _activeAccountInfo.userLogin.identitySecret;
|
||||
final cs = await veilid.getCryptoSystem(identitySecret.kind);
|
||||
final sharedSecret =
|
||||
await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value);
|
||||
|
||||
conversationCrypto = await DHTRecordCryptoPrivate.fromSecret(
|
||||
identitySecret.kind, sharedSecret);
|
||||
_conversationCrypto = conversationCrypto;
|
||||
return conversationCrypto;
|
||||
}
|
||||
|
||||
Future<IList<proto.Message>?> getLocalConversationMessages() async {
|
||||
final conversation = await readLocalConversation();
|
||||
if (conversation == null) {
|
||||
return null;
|
||||
}
|
||||
final messagesRecordKey =
|
||||
proto.TypedKeyProto.fromProto(conversation.messages);
|
||||
final crypto = await getConversationCrypto();
|
||||
|
||||
return (await DHTShortArray.openRead(messagesRecordKey,
|
||||
parent: _localConversationRecordKey, crypto: crypto))
|
||||
.scope((messages) async {
|
||||
var out = IList<proto.Message>();
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i);
|
||||
if (msg == null) {
|
||||
throw Exception('Failed to get message');
|
||||
}
|
||||
out = out.add(msg);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
final ActiveAccountInfo _activeAccountInfo;
|
||||
final TypedKey _localConversationRecordKey;
|
||||
final TypedKey _remoteIdentityPublicKey;
|
||||
final TypedKey _remoteConversationRecordKey;
|
||||
//
|
||||
DHTRecordCrypto? _conversationCrypto;
|
||||
}
|
||||
|
||||
// Create a conversation
|
||||
// If we were the initiator of the conversation there may be an
|
||||
// incomplete 'existingConversationRecord' that we need to fill
|
||||
// in now that we have the remote identity key
|
||||
Future<T> createConversation<T>(
|
||||
{required ActiveAccountInfo activeAccountInfo,
|
||||
required TypedKey remoteIdentityPublicKey,
|
||||
required FutureOr<T> Function(DHTRecord) callback,
|
||||
TypedKey? existingConversationRecordKey}) async {
|
||||
final pool = await DHTRecordPool.instance();
|
||||
final accountRecordKey =
|
||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||
|
||||
final crypto = await getConversationCrypto(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
remoteIdentityPublicKey: remoteIdentityPublicKey);
|
||||
final writer = activeAccountInfo.getConversationWriter();
|
||||
|
||||
// Open with SMPL scheme for identity writer
|
||||
late final DHTRecord localConversationRecord;
|
||||
if (existingConversationRecordKey != null) {
|
||||
localConversationRecord = await pool.openWrite(
|
||||
existingConversationRecordKey, writer,
|
||||
parent: accountRecordKey, crypto: crypto);
|
||||
} else {
|
||||
final localConversationRecordCreate = await pool.create(
|
||||
parent: accountRecordKey,
|
||||
crypto: crypto,
|
||||
schema: DHTSchema.smpl(
|
||||
oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)]));
|
||||
await localConversationRecordCreate.close();
|
||||
localConversationRecord = await pool.openWrite(
|
||||
localConversationRecordCreate.key, writer,
|
||||
parent: accountRecordKey, crypto: crypto);
|
||||
}
|
||||
return localConversationRecord
|
||||
// ignore: prefer_expression_function_bodies
|
||||
.deleteScope((localConversation) async {
|
||||
// Make messages log
|
||||
return (await DHTShortArray.create(
|
||||
parent: localConversation.key, crypto: crypto, smplWriter: writer))
|
||||
.deleteScope((messages) async {
|
||||
// Write local conversation key
|
||||
final conversation = proto.Conversation()
|
||||
..profile = activeAccountInfo.account.profile
|
||||
..identityMasterJson =
|
||||
jsonEncode(activeAccountInfo.localAccount.identityMaster.toJson())
|
||||
..messages = messages.record.key.toProto();
|
||||
|
||||
//
|
||||
final update = await localConversation.tryWriteProtobuf(
|
||||
proto.Conversation.fromBuffer, conversation);
|
||||
if (update != null) {
|
||||
throw Exception('Failed to write local conversation');
|
||||
}
|
||||
return await callback(localConversation);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
@riverpod
|
||||
class ActiveConversationMessages extends _$ActiveConversationMessages {
|
||||
/// Get message for active conversation
|
||||
@override
|
||||
FutureOr<IList<proto.Message>?> build() async {
|
||||
await eventualVeilid.future;
|
||||
|
||||
final activeChat = ref.watch(activeChatStateProvider);
|
||||
if (activeChat == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final activeAccountInfo =
|
||||
await ref.watch(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
|
||||
const IListConst([]);
|
||||
|
||||
final activeChatContactIdx = contactList.indexWhere(
|
||||
(c) =>
|
||||
proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) ==
|
||||
activeChat,
|
||||
);
|
||||
if (activeChatContactIdx == -1) {
|
||||
return null;
|
||||
}
|
||||
final activeChatContact = contactList[activeChatContactIdx];
|
||||
final remoteIdentityPublicKey =
|
||||
proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey);
|
||||
// final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(
|
||||
// activeChatContact.remoteConversationRecordKey);
|
||||
final localConversationRecordKey = proto.TypedKeyProto.fromProto(
|
||||
activeChatContact.localConversationRecordKey);
|
||||
|
||||
return await getLocalConversationMessages(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
localConversationRecordKey: localConversationRecordKey,
|
||||
remoteIdentityPublicKey: remoteIdentityPublicKey,
|
||||
);
|
||||
}
|
||||
}
|
83
lib/old_to_refactor/providers/window_control.dart
Normal file
83
lib/old_to_refactor/providers/window_control.dart
Normal file
|
@ -0,0 +1,83 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../../tools/responsive.dart';
|
||||
|
||||
export 'package:window_manager/window_manager.dart' show TitleBarStyle;
|
||||
|
||||
part 'window_control.g.dart';
|
||||
|
||||
enum OrientationCapability {
|
||||
normal,
|
||||
portraitOnly,
|
||||
landscapeOnly,
|
||||
}
|
||||
|
||||
// Window Control
|
||||
@riverpod
|
||||
class WindowControl extends _$WindowControl {
|
||||
/// Change window control
|
||||
@override
|
||||
FutureOr<bool> build() async {
|
||||
await _doWindowSetup(TitleBarStyle.hidden, OrientationCapability.normal);
|
||||
return true;
|
||||
}
|
||||
|
||||
static Future<void> initialize() async {
|
||||
if (isDesktop) {
|
||||
await windowManager.ensureInitialized();
|
||||
|
||||
const windowOptions = WindowOptions(
|
||||
size: Size(768, 1024),
|
||||
//minimumSize: Size(480, 480),
|
||||
center: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
skipTaskbar: false,
|
||||
);
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doWindowSetup(TitleBarStyle titleBarStyle,
|
||||
OrientationCapability orientationCapability) async {
|
||||
if (isDesktop) {
|
||||
await windowManager.setTitleBarStyle(titleBarStyle);
|
||||
} else {
|
||||
switch (orientationCapability) {
|
||||
case OrientationCapability.normal:
|
||||
await SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
case OrientationCapability.portraitOnly:
|
||||
await SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
]);
|
||||
case OrientationCapability.landscapeOnly:
|
||||
await SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// Mutators and Selectors
|
||||
|
||||
/// Reorder accounts
|
||||
Future<void> changeWindowSetup(TitleBarStyle titleBarStyle,
|
||||
OrientationCapability orientationCapability) async {
|
||||
state = const AsyncValue.loading();
|
||||
await _doWindowSetup(titleBarStyle, orientationCapability);
|
||||
state = const AsyncValue.data(true);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue