mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-08-11 23:50:19 -04:00
contact invitation algorithm
This commit is contained in:
parent
c35056f687
commit
f52094c105
43 changed files with 1319 additions and 451 deletions
67
lib/components/bottom_sheet_action_button.dart
Normal file
67
lib/components/bottom_sheet_action_button.dart
Normal file
|
@ -0,0 +1,67 @@
|
|||
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(
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
|||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../tools/theme_service.dart';
|
||||
|
||||
class ChatComponent extends ConsumerStatefulWidget {
|
||||
const ChatComponent({super.key});
|
||||
|
||||
|
@ -63,56 +65,68 @@ class ChatComponentState extends ConsumerState<ChatComponent> {
|
|||
_addMessage(textMessage);
|
||||
}
|
||||
|
||||
void _handleAttachmentPressed() {
|
||||
//
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final chatTheme = scale.toChatTheme();
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
//
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
return DefaultTextStyle(
|
||||
style: textTheme.bodySmall!,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: scale.primaryScale.appBackground,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0),
|
||||
child: Text("current contact",
|
||||
textAlign: TextAlign.start,
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: scale.primaryScale.subtleBackground,
|
||||
),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.fromSTEB(
|
||||
16, 0, 16, 0),
|
||||
child: Text("current contact",
|
||||
textAlign: TextAlign.start,
|
||||
style: textTheme.titleMedium),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(),
|
||||
child: Chat(
|
||||
//theme: _chatTheme,
|
||||
messages: _messages,
|
||||
//onAttachmentPressed: _handleAttachmentPressed,
|
||||
//onMessageTap: _handleMessageTap,
|
||||
//onPreviewDataFetched: _handlePreviewDataFetched,
|
||||
onSendPressed: _handleSendPressed,
|
||||
showUserAvatars: true,
|
||||
showUserNames: true,
|
||||
user: _user,
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(),
|
||||
child: Chat(
|
||||
theme: chatTheme,
|
||||
messages: _messages,
|
||||
//onAttachmentPressed: _handleAttachmentPressed,
|
||||
//onMessageTap: _handleMessageTap,
|
||||
//onPreviewDataFetched: _handlePreviewDataFetched,
|
||||
|
||||
onSendPressed: _handleSendPressed,
|
||||
showUserAvatars: true,
|
||||
showUserNames: true,
|
||||
user: _user,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,74 @@
|
|||
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_animate/flutter_animate.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
class ContactInvitationDisplay extends ConsumerWidget {
|
||||
const ContactInvitationDisplay({super.key});
|
||||
//final LocalAccount account;
|
||||
import '../tools/tools.dart';
|
||||
|
||||
class ContactInvitationDisplayDialog extends ConsumerStatefulWidget {
|
||||
const ContactInvitationDisplayDialog({
|
||||
super.key,
|
||||
});
|
||||
|
||||
// EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
|
||||
// _encryptionKey = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
//final logins = ref.watch(loginsProvider);
|
||||
ContactInvitationDisplayDialogState createState() =>
|
||||
ContactInvitationDisplayDialogState();
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [const Expanded(child: Text('Contact Invitation'))]));
|
||||
class ContactInvitationDisplayDialogState
|
||||
extends ConsumerState<ContactInvitationDisplayDialog> {
|
||||
final focusNode = FocusNode();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
Future<void>? _generateFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_generateFuture == null) {
|
||||
_generateFuture = _generate();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _generate() async {
|
||||
// Generate invitation
|
||||
|
||||
setState(() {
|
||||
_generateFuture = null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
//properties.add(DiagnosticsProperty<LocalAccount>('account', account));
|
||||
void 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 cardsize = MediaQuery.of(context).size.shortestSide - 24;
|
||||
//
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.white,
|
||||
child: SizedBox(
|
||||
width: cardsize,
|
||||
height: cardsize,
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [Text("Contact Invitation")]))
|
||||
.withModalHUD(context, _generateFuture != null)));
|
||||
}
|
||||
}
|
||||
|
|
162
lib/components/enter_pin.dart
Normal file
162
lib/components/enter_pin.dart
Normal file
|
@ -0,0 +1,162 @@
|
|||
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({
|
||||
this.matchPin,
|
||||
this.description,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String? matchPin;
|
||||
final String? description;
|
||||
|
||||
@override
|
||||
EnterPinDialogState createState() => EnterPinDialogState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('matchPin', matchPin))
|
||||
..add(StringProperty('description', description));
|
||||
}
|
||||
}
|
||||
|
||||
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.matchPin == null
|
||||
? 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
|
||||
],
|
||||
// validator: (widget.matchPin != null)
|
||||
// ? (value) => value == widget.matchPin
|
||||
// ? null
|
||||
// : translate('enter_pin_dialog.pin_does_not_match')
|
||||
// : null,
|
||||
// onClipboardFound: (value) {
|
||||
// debugPrint('onClipboardFound: $value');
|
||||
// pinController.setText(value);
|
||||
// },
|
||||
hapticFeedbackType: HapticFeedbackType.lightImpact,
|
||||
onCompleted: (pin) {
|
||||
debugPrint('onCompleted: $pin');
|
||||
Navigator.pop(context, pin);
|
||||
},
|
||||
onChanged: (value) {
|
||||
debugPrint('onChanged: $value');
|
||||
},
|
||||
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),
|
||||
),
|
||||
),
|
||||
errorText: '',
|
||||
errorPinTheme: defaultPinTheme.copyWith(
|
||||
decoration: BoxDecoration(
|
||||
color: scale.errorScale.border,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
).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));
|
||||
}
|
||||
}
|
162
lib/components/send_invite_dialog.dart
Normal file
162
lib/components/send_invite_dialog.dart
Normal file
|
@ -0,0 +1,162 @@
|
|||
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 'package:quickalert/quickalert.dart';
|
||||
|
||||
import '../entities/local_account.dart';
|
||||
import '../tools/tools.dart';
|
||||
import 'contact_invitation_display.dart';
|
||||
import 'enter_pin.dart';
|
||||
|
||||
class SendInviteDialog extends ConsumerStatefulWidget {
|
||||
const SendInviteDialog({super.key});
|
||||
|
||||
@override
|
||||
SendInviteDialogState createState() => SendInviteDialogState();
|
||||
}
|
||||
|
||||
class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
|
||||
final messageTextController = TextEditingController(
|
||||
text: translate('send_invite_dialog.connect_with_me'));
|
||||
|
||||
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
|
||||
String _encryptionKey = '';
|
||||
|
||||
@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(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(
|
||||
matchPin: pin,
|
||||
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 {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_encryptionKeyType = EncryptionKeyType.password;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onGenerateButtonPressed() async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => ContactInvitationDisplayDialog());
|
||||
// if (ret == null) {
|
||||
// return;
|
||||
// }
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
//final scale = theme.extension<ScaleScheme>()!;
|
||||
final textTheme = theme.textTheme;
|
||||
return SizedBox(
|
||||
height: 400,
|
||||
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,
|
||||
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.numeric_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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue