mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-08-02 11:16:10 -04:00
theming work, revamp contact invitation
This commit is contained in:
parent
3c95c9d1a3
commit
ae841ec42a
26 changed files with 504 additions and 507 deletions
|
@ -17,19 +17,25 @@ import 'contact_item_widget.dart';
|
|||
import 'empty_contact_list_widget.dart';
|
||||
|
||||
enum ContactsBrowserElementKind {
|
||||
invitation,
|
||||
contact,
|
||||
invitation,
|
||||
}
|
||||
|
||||
class ContactsBrowserElement {
|
||||
ContactsBrowserElement.invitation(proto.ContactInvitationRecord i)
|
||||
: kind = ContactsBrowserElementKind.invitation,
|
||||
contact = null,
|
||||
invitation = i;
|
||||
ContactsBrowserElement.contact(proto.Contact c)
|
||||
: kind = ContactsBrowserElementKind.contact,
|
||||
invitation = null,
|
||||
contact = c;
|
||||
ContactsBrowserElement.invitation(proto.ContactInvitationRecord i)
|
||||
: kind = ContactsBrowserElementKind.invitation,
|
||||
contact = null,
|
||||
invitation = i;
|
||||
|
||||
String get sortKey => switch (kind) {
|
||||
ContactsBrowserElementKind.contact => contact!.displayName,
|
||||
ContactsBrowserElementKind.invitation =>
|
||||
invitation!.recipient + invitation!.message
|
||||
};
|
||||
|
||||
final ContactsBrowserElementKind kind;
|
||||
final proto.ContactInvitationRecord? invitation;
|
||||
|
@ -66,27 +72,25 @@ class ContactsBrowser extends StatefulWidget {
|
|||
|
||||
class _ContactsBrowserState extends State<ContactsBrowser>
|
||||
with SingleTickerProviderStateMixin {
|
||||
Widget buildInvitationBar(BuildContext context) {
|
||||
Widget buildInvitationButton(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final scaleScheme = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
final menuIconColor = scaleConfig.preferBorders
|
||||
? scale.primaryScale.hoverBorder
|
||||
: scale.primaryScale.hoverBorder;
|
||||
? scaleScheme.primaryScale.hoverBorder
|
||||
: scaleScheme.primaryScale.hoverBorder;
|
||||
final menuBackgroundColor = scaleConfig.preferBorders
|
||||
? scale.primaryScale.elementBackground
|
||||
: scale.primaryScale.elementBackground;
|
||||
? scaleScheme.primaryScale.activeElementBackground
|
||||
: scaleScheme.primaryScale.activeElementBackground;
|
||||
|
||||
final menuBorderColor = scale.primaryScale.hoverBorder;
|
||||
final menuBorderColor = scaleScheme.primaryScale.hoverBorder;
|
||||
|
||||
final menuParams = StarMenuParameters(
|
||||
shape: MenuShape.grid,
|
||||
checkItemsScreenBoundaries: true,
|
||||
shape: MenuShape.linear,
|
||||
centerOffset: const Offset(0, 64),
|
||||
backgroundParams:
|
||||
BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)),
|
||||
// backgroundParams:
|
||||
// BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)),
|
||||
boundaryBackground: BoundaryBackground(
|
||||
color: menuBackgroundColor,
|
||||
decoration: ShapeDecoration(
|
||||
|
@ -99,89 +103,64 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||
borderRadius: BorderRadius.circular(
|
||||
8 * scaleConfig.borderRadiusScale)))));
|
||||
|
||||
final receiveInviteMenuItems = [
|
||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
_receiveInviteMenuController.closeMenu!();
|
||||
await ScanInvitationDialog.show(context);
|
||||
},
|
||||
iconSize: 32,
|
||||
icon: Icon(
|
||||
Icons.qr_code_scanner,
|
||||
size: 32,
|
||||
color: menuIconColor,
|
||||
),
|
||||
),
|
||||
Text(translate('add_contact_sheet.scan_invite'),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
|
||||
]).paddingAll(4),
|
||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
_receiveInviteMenuController.closeMenu!();
|
||||
await PasteInvitationDialog.show(context);
|
||||
},
|
||||
iconSize: 32,
|
||||
icon: Icon(
|
||||
Icons.paste,
|
||||
size: 32,
|
||||
color: menuIconColor,
|
||||
),
|
||||
),
|
||||
Text(translate('add_contact_sheet.paste_invite'),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
|
||||
]).paddingAll(4)
|
||||
];
|
||||
|
||||
return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
|
||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await CreateInvitationDialog.show(context);
|
||||
},
|
||||
iconSize: 32,
|
||||
icon: const Icon(Icons.contact_page),
|
||||
color: menuIconColor,
|
||||
),
|
||||
Text(translate('add_contact_sheet.create_invite'),
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
|
||||
]),
|
||||
StarMenu(
|
||||
items: receiveInviteMenuItems,
|
||||
onItemTapped: (_index, controller) {
|
||||
controller.closeMenu!();
|
||||
},
|
||||
controller: _receiveInviteMenuController,
|
||||
params: menuParams,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
iconSize: 32,
|
||||
icon: ImageIcon(
|
||||
const AssetImage('assets/images/handshake.png'),
|
||||
size: 32,
|
||||
color: menuIconColor,
|
||||
)),
|
||||
Text(translate('add_contact_sheet.receive_invite'),
|
||||
ElevatedButton makeMenuButton(
|
||||
{required IconData iconData,
|
||||
required String text,
|
||||
required void Function()? onPressed}) =>
|
||||
ElevatedButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(
|
||||
iconData,
|
||||
size: 32,
|
||||
).paddingSTEB(0, 8, 0, 8),
|
||||
label: Text(
|
||||
text,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
|
||||
]),
|
||||
),
|
||||
]).paddingAll(16);
|
||||
).paddingSTEB(0, 8, 0, 8));
|
||||
|
||||
final inviteMenuItems = [
|
||||
makeMenuButton(
|
||||
iconData: Icons.paste,
|
||||
text: translate('add_contact_sheet.paste_invite'),
|
||||
onPressed: () async {
|
||||
_invitationMenuController.closeMenu!();
|
||||
await PasteInvitationDialog.show(context);
|
||||
}),
|
||||
makeMenuButton(
|
||||
iconData: Icons.qr_code_scanner,
|
||||
text: translate('add_contact_sheet.scan_invite'),
|
||||
onPressed: () async {
|
||||
_invitationMenuController.closeMenu!();
|
||||
await ScanInvitationDialog.show(context);
|
||||
}).paddingLTRB(0, 0, 0, 8),
|
||||
makeMenuButton(
|
||||
iconData: Icons.contact_page,
|
||||
text: translate('add_contact_sheet.create_invite'),
|
||||
onPressed: () async {
|
||||
_invitationMenuController.closeMenu!();
|
||||
await CreateInvitationDialog.show(context);
|
||||
}).paddingLTRB(0, 0, 0, 8),
|
||||
];
|
||||
|
||||
return StarMenu(
|
||||
items: inviteMenuItems,
|
||||
onItemTapped: (_index, controller) {
|
||||
controller.closeMenu!();
|
||||
},
|
||||
controller: _invitationMenuController,
|
||||
params: menuParams,
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
iconSize: 24,
|
||||
icon: Icon(Icons.person_add, color: menuIconColor),
|
||||
tooltip: translate('add_contact_sheet.add_contact')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
//final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
|
||||
|
@ -196,100 +175,89 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||
final contactList =
|
||||
ciState.state.asData?.value.map((x) => x.value).toIList();
|
||||
|
||||
final expansionListData =
|
||||
<ContactsBrowserElementKind, List<ContactsBrowserElement>>{};
|
||||
if (contactInvitationRecordList.isNotEmpty) {
|
||||
expansionListData[ContactsBrowserElementKind.invitation] =
|
||||
contactInvitationRecordList
|
||||
.toList()
|
||||
.map(ContactsBrowserElement.invitation)
|
||||
.toList();
|
||||
}
|
||||
final initialList = <ContactsBrowserElement>[];
|
||||
if (contactList != null) {
|
||||
expansionListData[ContactsBrowserElementKind.contact] =
|
||||
contactList.toList().map(ContactsBrowserElement.contact).toList();
|
||||
initialList
|
||||
.addAll(contactList.toList().map(ContactsBrowserElement.contact));
|
||||
}
|
||||
if (contactInvitationRecordList.isNotEmpty) {
|
||||
initialList.addAll(contactInvitationRecordList
|
||||
.toList()
|
||||
.map(ContactsBrowserElement.invitation));
|
||||
}
|
||||
|
||||
initialList.sort((a, b) => a.sortKey.compareTo(b.sortKey));
|
||||
|
||||
return Column(children: [
|
||||
buildInvitationBar(context),
|
||||
SearchableList<ContactsBrowserElement>.expansion(
|
||||
expansionListData: expansionListData,
|
||||
expansionTitleBuilder: (k) {
|
||||
final kind = k as ContactsBrowserElementKind;
|
||||
late final String title;
|
||||
switch (kind) {
|
||||
case ContactsBrowserElementKind.contact:
|
||||
title = translate('contacts_dialog.contacts');
|
||||
case ContactsBrowserElementKind.invitation:
|
||||
title = translate('contacts_dialog.invitations');
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Text(title, style: textTheme.titleSmall),
|
||||
);
|
||||
},
|
||||
expansionInitiallyExpanded: (k) => true,
|
||||
expansionListBuilder: (_index, element) {
|
||||
switch (element.kind) {
|
||||
case ContactsBrowserElementKind.contact:
|
||||
final contact = element.contact!;
|
||||
return ContactItemWidget(
|
||||
contact: contact,
|
||||
selected: widget.selectedContactRecordKey ==
|
||||
contact.localConversationRecordKey.toVeilid(),
|
||||
disabled: false,
|
||||
onDoubleTap: _onStartChat,
|
||||
onTap: _onSelectContact,
|
||||
onDelete: _onDeleteContact)
|
||||
.paddingLTRB(0, 4, 0, 0);
|
||||
case ContactsBrowserElementKind.invitation:
|
||||
final invitation = element.invitation!;
|
||||
return ContactInvitationItemWidget(
|
||||
contactInvitationRecord: invitation, disabled: false)
|
||||
.paddingLTRB(0, 4, 0, 0);
|
||||
}
|
||||
},
|
||||
filterExpansionData: (value) {
|
||||
final lowerValue = value.toLowerCase();
|
||||
final filteredMap = {
|
||||
for (final entry in expansionListData.entries)
|
||||
entry.key: (expansionListData[entry.key] ?? []).where((element) {
|
||||
SearchableList<ContactsBrowserElement>(
|
||||
initialList: initialList,
|
||||
itemBuilder: (element) {
|
||||
switch (element.kind) {
|
||||
case ContactsBrowserElementKind.contact:
|
||||
final contact = element.contact!;
|
||||
return contact.nickname
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
contact.profile.name
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
contact.profile.pronouns
|
||||
.toLowerCase()
|
||||
.contains(lowerValue);
|
||||
return ContactItemWidget(
|
||||
contact: contact,
|
||||
selected: widget.selectedContactRecordKey ==
|
||||
contact.localConversationRecordKey.toVeilid(),
|
||||
disabled: false,
|
||||
onDoubleTap: _onStartChat,
|
||||
onTap: _onSelectContact,
|
||||
onDelete: _onDeleteContact)
|
||||
.paddingLTRB(0, 4, 0, 0);
|
||||
case ContactsBrowserElementKind.invitation:
|
||||
final invitation = element.invitation!;
|
||||
return invitation.message
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
invitation.recipient.toLowerCase().contains(lowerValue);
|
||||
return ContactInvitationItemWidget(
|
||||
contactInvitationRecord: invitation,
|
||||
disabled: false)
|
||||
.paddingLTRB(0, 4, 0, 0);
|
||||
}
|
||||
}).toList()
|
||||
};
|
||||
return filteredMap;
|
||||
},
|
||||
hideEmptyExpansionItems: true,
|
||||
searchFieldHeight: 40,
|
||||
listViewPadding: const EdgeInsets.all(4),
|
||||
spaceBetweenSearchAndList: 4,
|
||||
emptyWidget: contactList == null
|
||||
? waitingPage(text: translate('contact_list.loading_contacts'))
|
||||
: const EmptyContactListWidget(),
|
||||
defaultSuffixIconColor: scale.primaryScale.border,
|
||||
closeKeyboardWhenScrolling: true,
|
||||
searchFieldEnabled: contactList != null,
|
||||
inputDecoration:
|
||||
InputDecoration(labelText: translate('contact_list.search')),
|
||||
).expanded()
|
||||
},
|
||||
filter: (value) {
|
||||
final lowerValue = value.toLowerCase();
|
||||
|
||||
final filtered = <ContactsBrowserElement>[];
|
||||
for (final element in initialList) {
|
||||
switch (element.kind) {
|
||||
case ContactsBrowserElementKind.contact:
|
||||
final contact = element.contact!;
|
||||
if (contact.nickname.toLowerCase().contains(lowerValue) ||
|
||||
contact.profile.name
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
contact.profile.pronouns
|
||||
.toLowerCase()
|
||||
.contains(lowerValue)) {
|
||||
filtered.add(element);
|
||||
}
|
||||
case ContactsBrowserElementKind.invitation:
|
||||
final invitation = element.invitation!;
|
||||
if (invitation.message
|
||||
.toLowerCase()
|
||||
.contains(lowerValue) ||
|
||||
invitation.recipient
|
||||
.toLowerCase()
|
||||
.contains(lowerValue)) {
|
||||
filtered.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
searchFieldHeight: 40,
|
||||
listViewPadding: const EdgeInsets.fromLTRB(4, 0, 4, 4),
|
||||
searchFieldPadding: const EdgeInsets.fromLTRB(4, 8, 4, 4),
|
||||
emptyWidget: contactList == null
|
||||
? waitingPage(
|
||||
text: translate('contact_list.loading_contacts'))
|
||||
: const EmptyContactListWidget(),
|
||||
defaultSuffixIconColor: scale.primaryScale.border,
|
||||
closeKeyboardWhenScrolling: true,
|
||||
searchFieldEnabled: contactList != null,
|
||||
inputDecoration:
|
||||
InputDecoration(labelText: translate('contact_list.search')),
|
||||
secondaryWidget:
|
||||
buildInvitationButton(context).paddingLTRB(4, 0, 0, 0))
|
||||
.expanded()
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -318,5 +286,5 @@ class _ContactsBrowserState extends State<ContactsBrowser>
|
|||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
final _receiveInviteMenuController = StarMenuController();
|
||||
final _invitationMenuController = StarMenuController();
|
||||
}
|
||||
|
|
|
@ -1,196 +0,0 @@
|
|||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../chat/chat.dart';
|
||||
import '../../chat_list/chat_list.dart';
|
||||
import '../../layout/layout.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../contacts.dart';
|
||||
|
||||
const _kDoBackArrow = 'doBackArrow';
|
||||
|
||||
class ContactsDialog extends StatefulWidget {
|
||||
const ContactsDialog._({required this.modalContext});
|
||||
|
||||
@override
|
||||
State<ContactsDialog> createState() => _ContactsDialogState();
|
||||
|
||||
static Future<void> show(BuildContext modalContext) async {
|
||||
await showDialog<void>(
|
||||
context: modalContext,
|
||||
barrierDismissible: false,
|
||||
useRootNavigator: false,
|
||||
builder: (context) => ContactsDialog._(modalContext: modalContext));
|
||||
}
|
||||
|
||||
final BuildContext modalContext;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
.add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
|
||||
}
|
||||
}
|
||||
|
||||
class _ContactsDialogState extends State<ContactsDialog> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final appBarIconColor = scale.primaryScale.borderText;
|
||||
|
||||
final enableSplit = !isMobileWidth(context);
|
||||
final enableLeft = enableSplit || _selectedContact == null;
|
||||
final enableRight = enableSplit || _selectedContact != null;
|
||||
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: StyledScaffold(
|
||||
appBar: DefaultAppBar(
|
||||
title: Text(!enableSplit && enableRight
|
||||
? translate('contacts_dialog.edit_contact')
|
||||
: translate('contacts_dialog.contacts')),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
singleFuture((this, _kDoBackArrow), () async {
|
||||
final confirmed = await _onContactSelected(null);
|
||||
if (!enableSplit && enableRight) {
|
||||
} else {
|
||||
if (confirmed) {
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
if (_selectedContact != null)
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child:
|
||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chat_bubble),
|
||||
color: appBarIconColor,
|
||||
tooltip: translate('contacts_dialog.new_chat'),
|
||||
onPressed: () async {
|
||||
await _onChatStarted(_selectedContact!);
|
||||
}),
|
||||
Text(translate('contacts_dialog.new_chat'),
|
||||
style: theme.textTheme.labelSmall!
|
||||
.copyWith(color: appBarIconColor)),
|
||||
])).paddingLTRB(8, 0, 8, 0),
|
||||
if (enableSplit && _selectedContact != null)
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child:
|
||||
Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
color: appBarIconColor,
|
||||
tooltip:
|
||||
translate('contacts_dialog.close_contact'),
|
||||
onPressed: () async {
|
||||
await _onContactSelected(null);
|
||||
}),
|
||||
Text(translate('contacts_dialog.close_contact'),
|
||||
style: theme.textTheme.labelSmall!
|
||||
.copyWith(color: appBarIconColor)),
|
||||
])).paddingLTRB(8, 0, 8, 0),
|
||||
]),
|
||||
body: LayoutBuilder(builder: (context, constraint) {
|
||||
final maxWidth = constraint.maxWidth;
|
||||
|
||||
return ColoredBox(
|
||||
color: scale.primaryScale.appBackground,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: !enableLeft,
|
||||
child: SizedBox(
|
||||
width: enableLeft && !enableRight
|
||||
? maxWidth
|
||||
: (maxWidth / 3).clamp(200, 500),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: scale
|
||||
.primaryScale.subtleBackground),
|
||||
child: ContactsBrowser(
|
||||
selectedContactRecordKey: _selectedContact
|
||||
?.localConversationRecordKey
|
||||
.toVeilid(),
|
||||
onContactSelected: _onContactSelected,
|
||||
onStartChat: _onChatStarted,
|
||||
).paddingLTRB(8, 0, 8, 8)))),
|
||||
if (enableRight && enableLeft)
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 1, maxWidth: 1),
|
||||
color: scale.primaryScale.subtleBorder),
|
||||
if (enableRight)
|
||||
if (_selectedContact == null)
|
||||
const NoContactWidget().expanded()
|
||||
else
|
||||
ContactDetailsWidget(
|
||||
contact: _selectedContact!,
|
||||
onModifiedState: _onModifiedState)
|
||||
.paddingLTRB(16, 16, 16, 16)
|
||||
.expanded(),
|
||||
]));
|
||||
})));
|
||||
}
|
||||
|
||||
void _onModifiedState(bool isModified) {
|
||||
setState(() {
|
||||
_isModified = isModified;
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> _onContactSelected(proto.Contact? contact) async {
|
||||
if (contact != _selectedContact && _isModified) {
|
||||
final ok = await showConfirmModal(
|
||||
context: context,
|
||||
title: translate('confirmation.discard_changes'),
|
||||
text: translate('confirmation.are_you_sure_discard'));
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_selectedContact = contact;
|
||||
_isModified = false;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _onChatStarted(proto.Contact contact) async {
|
||||
final chatListCubit = context.read<ChatListCubit>();
|
||||
await chatListCubit.getOrCreateChatSingleContact(contact: contact);
|
||||
|
||||
if (mounted) {
|
||||
context
|
||||
.read<ActiveChatCubit>()
|
||||
.setActiveChat(contact.localConversationRecordKey.toVeilid());
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
proto.Contact? _selectedContact;
|
||||
bool _isModified = false;
|
||||
}
|
171
lib/contacts/views/contacts_page.dart
Normal file
171
lib/contacts/views/contacts_page.dart
Normal file
|
@ -0,0 +1,171 @@
|
|||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../chat/chat.dart';
|
||||
import '../../chat_list/chat_list.dart';
|
||||
import '../../layout/layout.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
import '../contacts.dart';
|
||||
|
||||
const _kDoBackArrow = 'doBackArrow';
|
||||
|
||||
class ContactsPage extends StatefulWidget {
|
||||
const ContactsPage({super.key});
|
||||
|
||||
@override
|
||||
State<ContactsPage> createState() => _ContactsPageState();
|
||||
}
|
||||
|
||||
class _ContactsPageState extends State<ContactsPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scale = theme.extension<ScaleScheme>()!;
|
||||
final appBarIconColor = scale.primaryScale.borderText;
|
||||
|
||||
final enableSplit = !isMobileSize(context);
|
||||
final enableLeft = enableSplit || _selectedContact == null;
|
||||
final enableRight = enableSplit || _selectedContact != null;
|
||||
|
||||
return StyledScaffold(
|
||||
appBar: DefaultAppBar(
|
||||
title: Text(!enableSplit && enableRight
|
||||
? translate('contacts_dialog.edit_contact')
|
||||
: translate('contacts_dialog.contacts')),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
singleFuture((this, _kDoBackArrow), () async {
|
||||
final confirmed = await _onContactSelected(null);
|
||||
if (!enableSplit && enableRight) {
|
||||
} else {
|
||||
if (confirmed) {
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
if (_selectedContact != null)
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chat_bubble),
|
||||
color: appBarIconColor,
|
||||
tooltip: translate('contacts_dialog.new_chat'),
|
||||
onPressed: () async {
|
||||
await _onChatStarted(_selectedContact!);
|
||||
}),
|
||||
Text(translate('contacts_dialog.new_chat'),
|
||||
style: theme.textTheme.labelSmall!
|
||||
.copyWith(color: appBarIconColor)),
|
||||
])).paddingLTRB(8, 0, 8, 0),
|
||||
if (enableSplit && _selectedContact != null)
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
color: appBarIconColor,
|
||||
tooltip: translate('contacts_dialog.close_contact'),
|
||||
onPressed: () async {
|
||||
await _onContactSelected(null);
|
||||
}),
|
||||
Text(translate('contacts_dialog.close_contact'),
|
||||
style: theme.textTheme.labelSmall!
|
||||
.copyWith(color: appBarIconColor)),
|
||||
])).paddingLTRB(8, 0, 8, 0),
|
||||
]),
|
||||
body: LayoutBuilder(builder: (context, constraint) {
|
||||
final maxWidth = constraint.maxWidth;
|
||||
|
||||
return ColoredBox(
|
||||
color: scale.primaryScale.appBackground,
|
||||
child:
|
||||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Offstage(
|
||||
offstage: !enableLeft,
|
||||
child: SizedBox(
|
||||
width: enableLeft && !enableRight
|
||||
? maxWidth
|
||||
: (maxWidth / 3).clamp(200, 500),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: scale.primaryScale.subtleBackground),
|
||||
child: ContactsBrowser(
|
||||
selectedContactRecordKey: _selectedContact
|
||||
?.localConversationRecordKey
|
||||
.toVeilid(),
|
||||
onContactSelected: _onContactSelected,
|
||||
onStartChat: _onChatStarted,
|
||||
).paddingLTRB(8, 0, 8, 8)))),
|
||||
if (enableRight && enableLeft)
|
||||
Container(
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: 1, maxWidth: 1),
|
||||
color: scale.primaryScale.subtleBorder),
|
||||
if (enableRight)
|
||||
if (_selectedContact == null)
|
||||
const NoContactWidget().expanded()
|
||||
else
|
||||
ContactDetailsWidget(
|
||||
contact: _selectedContact!,
|
||||
onModifiedState: _onModifiedState)
|
||||
.paddingLTRB(16, 16, 16, 16)
|
||||
.expanded(),
|
||||
]));
|
||||
}));
|
||||
}
|
||||
|
||||
void _onModifiedState(bool isModified) {
|
||||
setState(() {
|
||||
_isModified = isModified;
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> _onContactSelected(proto.Contact? contact) async {
|
||||
if (contact != _selectedContact && _isModified) {
|
||||
final ok = await showConfirmModal(
|
||||
context: context,
|
||||
title: translate('confirmation.discard_changes'),
|
||||
text: translate('confirmation.are_you_sure_discard'));
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_selectedContact = contact;
|
||||
_isModified = false;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _onChatStarted(proto.Contact contact) async {
|
||||
final chatListCubit = context.read<ChatListCubit>();
|
||||
await chatListCubit.getOrCreateChatSingleContact(contact: contact);
|
||||
|
||||
if (mounted) {
|
||||
context
|
||||
.read<ActiveChatCubit>()
|
||||
.setActiveChat(contact.localConversationRecordKey.toVeilid());
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
proto.Contact? _selectedContact;
|
||||
bool _isModified = false;
|
||||
}
|
|
@ -2,7 +2,7 @@ export 'availability_widget.dart';
|
|||
export 'contact_details_widget.dart';
|
||||
export 'contact_item_widget.dart';
|
||||
export 'contacts_browser.dart';
|
||||
export 'contacts_dialog.dart';
|
||||
export 'contacts_page.dart';
|
||||
export 'edit_contact_form.dart';
|
||||
export 'empty_contact_list_widget.dart';
|
||||
export 'no_contact_widget.dart';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue