mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-12-24 07:09:29 -05:00
ui improvements for invitations
This commit is contained in:
parent
d962f98786
commit
01c6490ec4
@ -122,7 +122,9 @@
|
||||
},
|
||||
"create_invitation_dialog": {
|
||||
"title": "Create Contact Invitation",
|
||||
"connect_with_me": "Connect with me on VeilidChat!",
|
||||
"me": "me",
|
||||
"fingerprint": "Fingerprint:",
|
||||
"connect_with_me": "Connect with {name} on VeilidChat!",
|
||||
"enter_message_hint": "Enter message for contact (optional)",
|
||||
"message_to_contact": "Message to send with invitation (not encrypted)",
|
||||
"generate": "Generate Invitation",
|
||||
@ -148,6 +150,7 @@
|
||||
"failed_to_reject": "Failed to reject contact invitation",
|
||||
"invalid_invitation": "Invalid invitation",
|
||||
"try_again_online": "Invitation could not be reached, try again when online",
|
||||
"key_not_found": "Invitation could not be found, it may not be on the network yet",
|
||||
"protected_with_pin": "Contact invitation is protected with a PIN",
|
||||
"protected_with_password": "Contact invitation is protected with a password",
|
||||
"invalid_pin": "Invalid PIN",
|
||||
@ -155,7 +158,7 @@
|
||||
},
|
||||
"waiting_invitation": {
|
||||
"accepted": "Contact invitation accepted from {name}",
|
||||
"reject": "Contact invitation was rejected"
|
||||
"rejected": "Contact invitation was rejected"
|
||||
},
|
||||
"paste_invitation_dialog": {
|
||||
"title": "Paste Contact Invite",
|
||||
@ -225,6 +228,10 @@
|
||||
"in_app": "In-app",
|
||||
"push": "Push",
|
||||
"in_app_or_push": "In-app or Push",
|
||||
"notifications": "Notifications",
|
||||
"event": "Event",
|
||||
"sound": "Sound",
|
||||
"delivery": "Delivery",
|
||||
"enable_badge": "Enable icon 'badge' bubble",
|
||||
"enable_notifications": "Enable notifications",
|
||||
"message_notification_content": "Message notification content",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
@ -214,9 +215,11 @@ class ContactInvitationListCubit
|
||||
}
|
||||
}
|
||||
|
||||
Future<ValidContactInvitation?> validateInvitation(
|
||||
{required Uint8List inviteData,
|
||||
required GetEncryptionKeyCallback getEncryptionKeyCallback}) async {
|
||||
Future<ValidContactInvitation?> validateInvitation({
|
||||
required Uint8List inviteData,
|
||||
required GetEncryptionKeyCallback getEncryptionKeyCallback,
|
||||
required CancelRequest cancelRequest,
|
||||
}) async {
|
||||
final pool = DHTRecordPool.instance;
|
||||
|
||||
// Get contact request inbox from invitation
|
||||
@ -245,15 +248,18 @@ class ContactInvitationListCubit
|
||||
contactRequestInboxKey) !=
|
||||
-1;
|
||||
|
||||
await (await pool.openRecordRead(contactRequestInboxKey,
|
||||
debugName: 'ContactInvitationListCubit::validateInvitation::'
|
||||
'ContactRequestInbox',
|
||||
parent: pool.getParentRecordKey(contactRequestInboxKey) ??
|
||||
_accountInfo.accountRecordKey))
|
||||
await (await pool
|
||||
.openRecordRead(contactRequestInboxKey,
|
||||
debugName: 'ContactInvitationListCubit::validateInvitation::'
|
||||
'ContactRequestInbox',
|
||||
parent: pool.getParentRecordKey(contactRequestInboxKey) ??
|
||||
_accountInfo.accountRecordKey)
|
||||
.withCancel(cancelRequest))
|
||||
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
||||
//
|
||||
final contactRequest = await contactRequestInbox
|
||||
.getProtobuf(proto.ContactRequest.fromBuffer);
|
||||
.getProtobuf(proto.ContactRequest.fromBuffer)
|
||||
.withCancel(cancelRequest);
|
||||
|
||||
final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
|
||||
|
||||
@ -281,7 +287,8 @@ class ContactInvitationListCubit
|
||||
|
||||
// Fetch the account master
|
||||
final contactSuperIdentity = await SuperIdentity.open(
|
||||
superRecordKey: contactSuperIdentityRecordKey);
|
||||
superRecordKey: contactSuperIdentityRecordKey)
|
||||
.withCancel(cancelRequest);
|
||||
|
||||
// Verify
|
||||
final idcs = await contactSuperIdentity.currentInstance.cryptoSystem;
|
||||
|
@ -11,6 +11,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../account_manager/account_manager.dart';
|
||||
import '../../notifications/notifications.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
@ -20,17 +21,20 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
const ContactInvitationDisplayDialog._({
|
||||
required this.locator,
|
||||
required this.message,
|
||||
required this.fingerprint,
|
||||
});
|
||||
|
||||
final Locator locator;
|
||||
final String message;
|
||||
final String fingerprint;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(StringProperty('message', message))
|
||||
..add(DiagnosticsProperty<Locator>('locator', locator));
|
||||
..add(DiagnosticsProperty<Locator>('locator', locator))
|
||||
..add(StringProperty('fingerprint', fingerprint));
|
||||
}
|
||||
|
||||
String makeTextInvite(String message, Uint8List data) {
|
||||
@ -38,10 +42,12 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
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';
|
||||
'---- END VEILIDCHAT CONTACT INVITE -----\n'
|
||||
'Fingerprint:\n$fingerprint\n';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -97,18 +103,27 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
.copyWith(color: Colors.black)))
|
||||
.paddingAll(8),
|
||||
FittedBox(
|
||||
child: QrImageView.withQr(
|
||||
size: 300,
|
||||
qr: QrCode.fromUint8List(
|
||||
data: data.$1,
|
||||
errorCorrectLevel:
|
||||
QrErrorCorrectLevel.L)))
|
||||
.expanded(),
|
||||
child: QrImageView.withQr(
|
||||
size: 300,
|
||||
qr: QrCode.fromUint8List(
|
||||
data: data.$1,
|
||||
errorCorrectLevel:
|
||||
QrErrorCorrectLevel.L)),
|
||||
).expanded(),
|
||||
Text(message,
|
||||
softWrap: true,
|
||||
style: textTheme.labelLarge!
|
||||
.copyWith(color: Colors.black))
|
||||
.paddingAll(8),
|
||||
Text(
|
||||
'${translate('create_invitation_dialog.fingerprint')}\n'
|
||||
'$fingerprint',
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(
|
||||
color: Colors.black,
|
||||
fontFamily: 'Source Code Pro'))
|
||||
.paddingAll(2),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.copy),
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -129,11 +144,15 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
error: errorPage)))));
|
||||
}
|
||||
|
||||
static Future<void> show(
|
||||
{required BuildContext context,
|
||||
required Locator locator,
|
||||
required InvitationGeneratorCubit Function(BuildContext) create,
|
||||
required String message}) async {
|
||||
static Future<void> show({
|
||||
required BuildContext context,
|
||||
required Locator locator,
|
||||
required InvitationGeneratorCubit Function(BuildContext) create,
|
||||
required String message,
|
||||
}) async {
|
||||
final fingerprint =
|
||||
locator<AccountInfoCubit>().state.identityPublicKey.toString();
|
||||
|
||||
await showPopControlDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => BlocProvider(
|
||||
@ -141,6 +160,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||
child: ContactInvitationDisplayDialog._(
|
||||
locator: locator,
|
||||
message: message,
|
||||
fingerprint: fingerprint,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
@ -37,8 +37,7 @@ class CreateInvitationDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
final _messageTextController = TextEditingController(
|
||||
text: translate('create_invitation_dialog.connect_with_me'));
|
||||
late final TextEditingController _messageTextController;
|
||||
|
||||
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
|
||||
String _encryptionKey = '';
|
||||
@ -46,6 +45,12 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final accountInfo = widget.locator<AccountRecordCubit>().state;
|
||||
final name = accountInfo.asData?.value.profile.name ??
|
||||
translate('create_invitation_dialog.me');
|
||||
_messageTextController = TextEditingController(
|
||||
text: translate('create_invitation_dialog.connect_with_me',
|
||||
args: {'name': name}));
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -152,13 +157,13 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||
message: _messageTextController.text,
|
||||
expiration: _expiration);
|
||||
|
||||
navigator.pop();
|
||||
|
||||
await ContactInvitationDisplayDialog.show(
|
||||
context: context,
|
||||
locator: widget.locator,
|
||||
message: _messageTextController.text,
|
||||
create: (context) => InvitationGeneratorCubit(generator));
|
||||
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -61,17 +62,19 @@ class InvitationDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class InvitationDialogState extends State<InvitationDialog> {
|
||||
ValidContactInvitation? _validInvitation;
|
||||
bool _isValidating = false;
|
||||
bool _isAccepting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool get isValidating => _isValidating;
|
||||
bool get isAccepting => _isAccepting;
|
||||
Future<void> _onCancel() async {
|
||||
final navigator = Navigator.of(context);
|
||||
_cancelRequest.cancel();
|
||||
setState(() {
|
||||
_isAccepting = false;
|
||||
});
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
Future<void> _onAccept() async {
|
||||
final navigator = Navigator.of(context);
|
||||
@ -153,6 +156,7 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||
final validatedContactInvitation =
|
||||
await contactInvitationListCubit.validateInvitation(
|
||||
inviteData: inviteData,
|
||||
cancelRequest: _cancelRequest,
|
||||
getEncryptionKeyCallback:
|
||||
(cs, encryptionKeyType, encryptedSecret) async {
|
||||
String encryptionKey;
|
||||
@ -234,6 +238,9 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||
late final String errorText;
|
||||
if (e is VeilidAPIExceptionTryAgain) {
|
||||
errorText = translate('invitation_dialog.try_again_online');
|
||||
}
|
||||
if (e is VeilidAPIExceptionKeyNotFound) {
|
||||
errorText = translate('invitation_dialog.key_not_found');
|
||||
} else {
|
||||
errorText = translate('invitation_dialog.invalid_invitation');
|
||||
}
|
||||
@ -245,6 +252,12 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||
_validInvitation = null;
|
||||
widget.onValidationFailed();
|
||||
});
|
||||
} on CancelException {
|
||||
setState(() {
|
||||
_isValidating = false;
|
||||
_validInvitation = null;
|
||||
widget.onValidationCancelled();
|
||||
});
|
||||
} on Exception catch (e) {
|
||||
log.debug('exception: $e', e);
|
||||
setState(() {
|
||||
@ -264,6 +277,11 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||
Text(translate('invitation_dialog.validating'))
|
||||
.paddingLTRB(0, 0, 0, 16),
|
||||
buildProgressIndicator().paddingAll(16),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.cancel),
|
||||
label: Text(translate('button.cancel')),
|
||||
onPressed: _onCancel,
|
||||
).paddingAll(16),
|
||||
]).toCenter(),
|
||||
if (_validInvitation == null &&
|
||||
!_isValidating &&
|
||||
@ -315,13 +333,25 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _isAccepting
|
||||
? [buildProgressIndicator().paddingAll(16)]
|
||||
? [
|
||||
buildProgressIndicator().paddingAll(16),
|
||||
]
|
||||
: _buildPreAccept()),
|
||||
),
|
||||
);
|
||||
return PopControl(dismissible: dismissible, child: dialog);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
ValidContactInvitation? _validInvitation;
|
||||
bool _isValidating = false;
|
||||
bool _isAccepting = false;
|
||||
final _cancelRequest = CancelRequest();
|
||||
|
||||
bool get isValidating => _isValidating;
|
||||
bool get isAccepting => _isAccepting;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
|
@ -52,7 +52,11 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
out.add(DropdownMenuItem(
|
||||
value: x.$1,
|
||||
enabled: x.$2,
|
||||
child: Text(x.$3, style: textTheme.labelSmall)));
|
||||
child: Text(
|
||||
x.$3,
|
||||
style: textTheme.labelSmall,
|
||||
textAlign: TextAlign.center,
|
||||
)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@ -71,7 +75,11 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
out.add(DropdownMenuItem(
|
||||
value: x.$1,
|
||||
enabled: x.$2,
|
||||
child: Text(x.$3, style: textTheme.labelSmall)));
|
||||
child: Text(
|
||||
x.$3,
|
||||
style: textTheme.labelSmall,
|
||||
textAlign: TextAlign.center,
|
||||
)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@ -100,17 +108,23 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
out.add(DropdownMenuItem(
|
||||
value: x.$1,
|
||||
enabled: x.$2,
|
||||
child: Text(x.$3, style: textTheme.labelSmall)));
|
||||
child: Text(
|
||||
x.$3,
|
||||
style: textTheme.labelSmall,
|
||||
textAlign: TextAlign.center,
|
||||
)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(width: 2, color: scale.primaryScale.border),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8 * scaleConfig.borderRadiusScale))),
|
||||
return InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('settings_page.notifications'),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8 * scaleConfig.borderRadiusScale),
|
||||
borderSide: BorderSide(width: 2, color: scale.primaryScale.border),
|
||||
),
|
||||
),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
// Display Beta Warning
|
||||
FormBuilderCheckbox(
|
||||
@ -175,12 +189,35 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
await updatePreferences(newNotificationsPreference);
|
||||
},
|
||||
items: messageNotificationContentItems(),
|
||||
).paddingAll(8),
|
||||
).paddingLTRB(0, 4, 0, 4),
|
||||
|
||||
// Notifications
|
||||
Table(
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
children: [
|
||||
TableRow(children: [
|
||||
Text(translate('settings_page.event'),
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.titleMedium!.copyWith(
|
||||
color: scale.primaryScale.border,
|
||||
decorationColor: scale.primaryScale.border,
|
||||
decoration: TextDecoration.underline))
|
||||
.paddingAll(8),
|
||||
Text(translate('settings_page.delivery'),
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.titleMedium!.copyWith(
|
||||
color: scale.primaryScale.border,
|
||||
decorationColor: scale.primaryScale.border,
|
||||
decoration: TextDecoration.underline))
|
||||
.paddingAll(8),
|
||||
Text(translate('settings_page.sound'),
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.titleMedium!.copyWith(
|
||||
color: scale.primaryScale.border,
|
||||
decorationColor: scale.primaryScale.border,
|
||||
decoration: TextDecoration.underline))
|
||||
.paddingAll(8),
|
||||
]),
|
||||
TableRow(children: [
|
||||
// Invitation accepted
|
||||
Text(
|
||||
@ -216,7 +253,7 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
await updatePreferences(newNotificationsPreference);
|
||||
},
|
||||
items: soundEffectItems(),
|
||||
).paddingAll(4)
|
||||
).paddingLTRB(4, 4, 0, 4)
|
||||
]),
|
||||
// Message received
|
||||
TableRow(children: [
|
||||
@ -253,7 +290,7 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
await updatePreferences(newNotificationsPreference);
|
||||
},
|
||||
items: soundEffectItems(),
|
||||
).paddingAll(4)
|
||||
).paddingLTRB(4, 4, 0, 4)
|
||||
]),
|
||||
|
||||
// Message sent
|
||||
@ -277,9 +314,9 @@ Widget buildSettingsPageNotificationPreferences(
|
||||
await updatePreferences(newNotificationsPreference);
|
||||
},
|
||||
items: soundEffectItems(),
|
||||
).paddingAll(4)
|
||||
).paddingLTRB(4, 4, 0, 4)
|
||||
]),
|
||||
]).paddingAll(8)
|
||||
])
|
||||
]).paddingAll(8),
|
||||
);
|
||||
}
|
||||
|
@ -36,10 +36,9 @@ packages:
|
||||
async_tools:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: async_tools
|
||||
sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "../../../dart_async_tools"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.4"
|
||||
bloc:
|
||||
dependency: "direct main"
|
||||
@ -52,10 +51,9 @@ packages:
|
||||
bloc_advanced_tools:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bloc_advanced_tools
|
||||
sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "../../../bloc_advanced_tools"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.4"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
|
@ -26,11 +26,11 @@ dependencies:
|
||||
# veilid: ^0.0.1
|
||||
path: ../../../veilid/veilid-flutter
|
||||
|
||||
# dependency_overrides:
|
||||
# async_tools:
|
||||
# path: ../../../dart_async_tools
|
||||
# bloc_advanced_tools:
|
||||
# path: ../../../bloc_advanced_tools
|
||||
dependency_overrides:
|
||||
async_tools:
|
||||
path: ../../../dart_async_tools
|
||||
bloc_advanced_tools:
|
||||
path: ../../../bloc_advanced_tools
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.10
|
||||
|
14
pubspec.lock
14
pubspec.lock
@ -84,10 +84,9 @@ packages:
|
||||
async_tools:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: async_tools
|
||||
sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "../dart_async_tools"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.4"
|
||||
awesome_extensions:
|
||||
dependency: "direct main"
|
||||
@ -140,10 +139,9 @@ packages:
|
||||
bloc_advanced_tools:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bloc_advanced_tools
|
||||
sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "../bloc_advanced_tools"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.4"
|
||||
blurry_modal_progress_hud:
|
||||
dependency: "direct main"
|
||||
|
10
pubspec.yaml
10
pubspec.yaml
@ -107,11 +107,11 @@ dependencies:
|
||||
xterm: ^4.0.0
|
||||
zxing2: ^0.2.3
|
||||
|
||||
# dependency_overrides:
|
||||
# async_tools:
|
||||
# path: ../dart_async_tools
|
||||
# bloc_advanced_tools:
|
||||
# path: ../bloc_advanced_tools
|
||||
dependency_overrides:
|
||||
async_tools:
|
||||
path: ../dart_async_tools
|
||||
bloc_advanced_tools:
|
||||
path: ../bloc_advanced_tools
|
||||
# flutter_chat_ui:
|
||||
# path: ../flutter_chat_ui
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user