mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-18 14:22:53 +00:00
298 lines
9.7 KiB
Dart
298 lines
9.7 KiB
Dart
// ignore_for_file: parameter_assignments
|
|
|
|
import 'dart:async';
|
|
import 'dart:collection';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
import 'package:twonly/globals.dart';
|
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
|
import 'package:twonly/src/database/twonly.db.dart';
|
|
import 'package:twonly/src/utils/misc.dart';
|
|
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
|
import 'package:twonly/src/views/components/flame.dart';
|
|
import 'package:twonly/src/views/components/user_context_menu.component.dart';
|
|
import 'package:twonly/src/views/groups/group_create_select_group_name.view.dart';
|
|
|
|
class GroupCreateSelectMembersView extends StatefulWidget {
|
|
const GroupCreateSelectMembersView({this.groupId, super.key});
|
|
final String? groupId;
|
|
@override
|
|
State<GroupCreateSelectMembersView> createState() => _StartNewChatView();
|
|
}
|
|
|
|
class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
|
List<Contact> contacts = [];
|
|
List<Contact> allContacts = [];
|
|
final TextEditingController searchUserName = TextEditingController();
|
|
late StreamSubscription<List<Contact>> contactSub;
|
|
|
|
final HashSet<int> selectedUsers = HashSet();
|
|
final HashSet<int> alreadyInGroup = HashSet();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
final stream = twonlyDB.contactsDao.watchAllAcceptedContacts();
|
|
|
|
contactSub = stream.listen((update) async {
|
|
update.sort(
|
|
(a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)),
|
|
);
|
|
setState(() {
|
|
allContacts = update;
|
|
});
|
|
await filterUsers();
|
|
});
|
|
initAsync();
|
|
}
|
|
|
|
Future<void> initAsync() async {
|
|
if (widget.groupId != null) {
|
|
final members = await twonlyDB.groupsDao.getGroupContact(widget.groupId!);
|
|
for (final member in members) {
|
|
alreadyInGroup.add(member.userId);
|
|
}
|
|
if (mounted) setState(() {});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
unawaited(contactSub.cancel());
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> filterUsers() async {
|
|
if (searchUserName.value.text.isEmpty) {
|
|
setState(() {
|
|
contacts = allContacts;
|
|
});
|
|
return;
|
|
}
|
|
final usersFiltered = allContacts
|
|
.where(
|
|
(user) => getContactDisplayName(
|
|
user,
|
|
).toLowerCase().contains(searchUserName.value.text.toLowerCase()),
|
|
)
|
|
.toList();
|
|
setState(() {
|
|
contacts = usersFiltered;
|
|
});
|
|
}
|
|
|
|
void toggleSelectedUser(int userId) {
|
|
if (alreadyInGroup.contains(userId)) return;
|
|
if (!selectedUsers.contains(userId)) {
|
|
if (selectedUsers.length + alreadyInGroup.length > 256) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(context.lang.groupSizeLimitError(256)),
|
|
duration: const Duration(seconds: 3),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
selectedUsers.add(userId);
|
|
} else {
|
|
selectedUsers.remove(userId);
|
|
}
|
|
setState(() {});
|
|
}
|
|
|
|
Future<void> submitChanges() async {
|
|
if (widget.groupId != null) {
|
|
Navigator.pop(context, selectedUsers.toList());
|
|
return;
|
|
}
|
|
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => GroupCreateSelectGroupNameView(
|
|
selectedUsers: allContacts
|
|
.where((t) => selectedUsers.contains(t.userId))
|
|
.toList(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: () => FocusScope.of(context).unfocus(),
|
|
child: Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(
|
|
widget.groupId == null
|
|
? context.lang.selectMembers
|
|
: context.lang.addMember,
|
|
),
|
|
),
|
|
floatingActionButton: FilledButton.icon(
|
|
onPressed: selectedUsers.isEmpty ? null : submitChanges,
|
|
label: Text(
|
|
widget.groupId == null
|
|
? context.lang.next
|
|
: context.lang.updateGroup,
|
|
),
|
|
icon: const FaIcon(FontAwesomeIcons.penToSquare),
|
|
),
|
|
body: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
bottom: 40,
|
|
left: 10,
|
|
top: 20,
|
|
right: 10,
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
child: TextField(
|
|
onChanged: (_) async {
|
|
await filterUsers();
|
|
},
|
|
controller: searchUserName,
|
|
decoration: getInputDecoration(
|
|
context,
|
|
context.lang.shareImageSearchAllContacts,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
restorationId: 'new_message_users_list',
|
|
itemCount:
|
|
contacts.length + (selectedUsers.isEmpty ? 0 : 2),
|
|
itemBuilder: (context, i) {
|
|
if (selectedUsers.isNotEmpty) {
|
|
final selected = selectedUsers.toList();
|
|
if (i == 0) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 18),
|
|
constraints: const BoxConstraints(
|
|
maxHeight: 150,
|
|
),
|
|
child: SingleChildScrollView(
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
// Wrap will use the available width from constraints.maxWidth
|
|
return Wrap(
|
|
spacing: 8,
|
|
children: selected.map((w) {
|
|
return _Chip(
|
|
contact: allContacts.firstWhere(
|
|
(t) => t.userId == w,
|
|
),
|
|
onTap: toggleSelectedUser,
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
if (i == 1) {
|
|
return const Divider();
|
|
}
|
|
i -= 2;
|
|
}
|
|
final user = contacts[i];
|
|
return UserContextMenu(
|
|
key: ValueKey(user.userId),
|
|
contact: user,
|
|
child: ListTile(
|
|
title: Row(
|
|
children: [
|
|
Text(getContactDisplayName(user)),
|
|
FlameCounterWidget(
|
|
contactId: user.userId,
|
|
prefix: true,
|
|
),
|
|
],
|
|
),
|
|
subtitle: (alreadyInGroup.contains(user.userId))
|
|
? Text(context.lang.alreadyInGroup)
|
|
: null,
|
|
leading: AvatarIcon(
|
|
contactId: user.userId,
|
|
fontSize: 13,
|
|
),
|
|
trailing: Checkbox(
|
|
value:
|
|
selectedUsers.contains(user.userId) |
|
|
alreadyInGroup.contains(user.userId),
|
|
side: WidgetStateBorderSide.resolveWith(
|
|
(states) {
|
|
if (states.contains(WidgetState.selected)) {
|
|
return const BorderSide(width: 0);
|
|
}
|
|
return BorderSide(
|
|
color: Theme.of(context).colorScheme.outline,
|
|
);
|
|
},
|
|
),
|
|
onChanged: (value) {
|
|
toggleSelectedUser(user.userId);
|
|
},
|
|
),
|
|
onTap: () {
|
|
toggleSelectedUser(user.userId);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Chip extends StatelessWidget {
|
|
const _Chip({
|
|
required this.contact,
|
|
required this.onTap,
|
|
});
|
|
final Contact contact;
|
|
final void Function(int) onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: () => onTap(contact.userId),
|
|
child: Chip(
|
|
avatar: AvatarIcon(
|
|
contactId: contact.userId,
|
|
fontSize: 10,
|
|
),
|
|
label: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
getContactDisplayName(contact),
|
|
style: const TextStyle(fontSize: 14),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(width: 15),
|
|
const FaIcon(
|
|
FontAwesomeIcons.xmark,
|
|
color: Colors.grey,
|
|
size: 12,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|