create_group_screen.dart
1 import 'dart:typed_data'; 2 3 import 'package:flutter/material.dart'; 4 import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 import 'package:go_router/go_router.dart'; 6 import 'package:iconify_flutter/icons/majesticons.dart'; 7 8 import '../../core/router.dart'; 9 import '../../core/theme/app_theme.dart'; 10 import '../../providers/contacts_provider.dart'; 11 import '../../providers/groups_provider.dart'; 12 import '../../providers/messages_provider.dart' show bytesToHex; 13 import '../../services/dead_drop_service.dart'; 14 import '../../widgets/themed_icon.dart'; 15 16 class CreateGroupScreen extends ConsumerStatefulWidget { 17 const CreateGroupScreen({super.key}); 18 19 @override 20 ConsumerState<CreateGroupScreen> createState() => _CreateGroupScreenState(); 21 } 22 23 class _CreateGroupScreenState extends ConsumerState<CreateGroupScreen> { 24 final _nameController = TextEditingController(); 25 final _selectedContactIds = <String>{}; 26 bool _isCreating = false; 27 28 @override 29 void dispose() { 30 _nameController.dispose(); 31 super.dispose(); 32 } 33 34 Future<void> _createGroup() async { 35 final name = _nameController.text.trim(); 36 if (name.isEmpty || _selectedContactIds.isEmpty) return; 37 38 setState(() => _isCreating = true); 39 40 try { 41 final memberIds = _selectedContactIds 42 .map((hex) { 43 final bytes = <int>[]; 44 for (var i = 0; i < hex.length; i += 2) { 45 bytes.add(int.parse(hex.substring(i, i + 2), radix: 16)); 46 } 47 return Uint8List.fromList(bytes); 48 }) 49 .toList(); 50 51 final groupId = await ref 52 .read(groupsProvider.notifier) 53 .createGroup(name, memberIds); 54 55 if (mounted) { 56 final groupIdHex = bytesToHex(groupId); 57 context.push(AppRoutes.groupChatPath(groupIdHex)); 58 } 59 } catch (e) { 60 if (mounted) { 61 ScaffoldMessenger.of(context).showSnackBar( 62 SnackBar(content: Text('Failed to create group: $e')), 63 ); 64 } 65 } finally { 66 if (mounted) setState(() => _isCreating = false); 67 } 68 } 69 70 @override 71 Widget build(BuildContext context) { 72 final colors = AppColors.of(context); 73 final contactsAsync = ref.watch(contactsProvider); 74 75 return Scaffold( 76 appBar: AppBar( 77 title: const Text('New Group'), 78 leading: !Navigator.of(context).canPop() && AppResponsive.isWideScreen(context) 79 ? IconButton( 80 icon: const Icon(Icons.close), 81 onPressed: () => ref.read(routerProvider).go(AppRoutes.contacts), 82 ) 83 : null, 84 ), 85 body: Column( 86 children: [ 87 // Group name input 88 Padding( 89 padding: const EdgeInsets.all(AppSpacing.md), 90 child: TextField( 91 controller: _nameController, 92 autofocus: true, 93 decoration: InputDecoration( 94 labelText: 'Group name', 95 hintText: 'Enter a name for the group', 96 border: OutlineInputBorder( 97 borderRadius: BorderRadius.circular(12), 98 ), 99 prefixIcon: Padding( 100 padding: const EdgeInsets.all(12), 101 child: ThemedIcon(Majesticons.user_group_line, 102 color: colors.primary), 103 ), 104 ), 105 textCapitalization: TextCapitalization.words, 106 onChanged: (_) => setState(() {}), 107 ), 108 ), 109 110 // Member selection label 111 Padding( 112 padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), 113 child: Row( 114 children: [ 115 Text( 116 'Select members', 117 style: Theme.of(context).textTheme.titleSmall?.copyWith( 118 fontWeight: FontWeight.w600, 119 ), 120 ), 121 const Spacer(), 122 Text( 123 '${_selectedContactIds.length} selected', 124 style: Theme.of(context).textTheme.bodySmall?.copyWith( 125 color: colors.onBackgroundSecondary, 126 ), 127 ), 128 ], 129 ), 130 ), 131 const SizedBox(height: AppSpacing.sm), 132 133 // Contact list 134 Expanded( 135 child: contactsAsync.when( 136 loading: () => 137 const Center(child: CircularProgressIndicator()), 138 error: (e, _) => Center(child: Text('Error: $e')), 139 data: (contacts) { 140 if (contacts.isEmpty) { 141 return Center( 142 child: Text( 143 'No contacts to add', 144 style: Theme.of(context).textTheme.bodyMedium?.copyWith( 145 color: colors.onBackgroundSecondary, 146 ), 147 ), 148 ); 149 } 150 151 return ListView.builder( 152 itemCount: contacts.length, 153 itemBuilder: (context, index) { 154 final contact = contacts[index]; 155 final contactIdHex = contact.contactId 156 .map( 157 (b) => b.toRadixString(16).padLeft(2, '0')) 158 .join(); 159 final isSelected = 160 _selectedContactIds.contains(contactIdHex); 161 162 return CheckboxListTile( 163 value: isSelected, 164 onChanged: (checked) { 165 setState(() { 166 if (checked == true) { 167 _selectedContactIds.add(contactIdHex); 168 } else { 169 _selectedContactIds.remove(contactIdHex); 170 } 171 }); 172 }, 173 secondary: CircleAvatar( 174 backgroundColor: colors.primary, 175 child: Text( 176 contact.displayName[0].toUpperCase(), 177 style: TextStyle( 178 color: colors.onPrimary, 179 fontWeight: FontWeight.w600, 180 ), 181 ), 182 ), 183 title: Text(contact.displayName), 184 activeColor: colors.primary, 185 ); 186 }, 187 ); 188 }, 189 ), 190 ), 191 ], 192 ), 193 bottomNavigationBar: SafeArea( 194 child: Padding( 195 padding: const EdgeInsets.all(AppSpacing.md), 196 child: FilledButton( 197 onPressed: _nameController.text.trim().isNotEmpty && 198 _selectedContactIds.isNotEmpty && 199 !_isCreating 200 ? _createGroup 201 : null, 202 child: _isCreating 203 ? const SizedBox( 204 width: 20, 205 height: 20, 206 child: CircularProgressIndicator( 207 strokeWidth: 2, 208 color: Colors.white, 209 ), 210 ) 211 : const Text('Create Group'), 212 ), 213 ), 214 ), 215 ); 216 } 217 }