/ app / lib / screens / groups / create_group_screen.dart
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  }