// Version: 1.7.0 | Created: 2026-04-01 | Updated: 2026-07-04 // Main rooms list screen with bottom navigation. import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'dart:async'; import '../../../app/theme.dart'; import '../../../core/network/matrix_client.dart'; import '../../help/presentation/help_tab.dart'; import '../../panic/presentation/panic_button.dart'; import '../../jitsi/presentation/conference_tab.dart'; import '../../profile/presentation/key_restore_prompt.dart'; import '../../profile/presentation/profile_screen.dart'; import '../../spaces/presentation/spaces_screen.dart'; import 'public_rooms_dialog.dart'; import 'room_tile.dart'; import 'rooms_controller.dart'; import 'user_search_dialog.dart'; class RoomsScreen extends ConsumerStatefulWidget { const RoomsScreen({super.key}); @override ConsumerState createState() => _RoomsScreenState(); } class _RoomsScreenState extends ConsumerState { int _selectedIndex = 0; bool _searchActive = false; String _searchQuery = ''; final _searchController = TextEditingController(); @override void initState() { super.initState(); // Offer encrypted-history restore right after login (fires once). WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final client = ref.read(matrixClientProvider); unawaited(maybePromptKeyRestore(context, client)); }); } @override void dispose() { _searchController.dispose(); super.dispose(); } void _toggleSearch() { setState(() { _searchActive = !_searchActive; if (!_searchActive) { _searchQuery = ''; _searchController.clear(); } }); } static const _destinations = [ NavigationDestination( icon: Icon(Icons.chat_bubble_outline), selectedIcon: Icon(Icons.chat_bubble), label: 'Rooms', ), NavigationDestination( icon: Icon(Icons.dashboard_outlined), selectedIcon: Icon(Icons.dashboard), label: 'Spaces', ), NavigationDestination( icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Profile', ), NavigationDestination( icon: Icon(Icons.videocam_outlined), selectedIcon: Icon(Icons.videocam), label: 'Conference', ), NavigationDestination( icon: Icon(Icons.help_outline), selectedIcon: Icon(Icons.help), label: 'Help', ), ]; Widget _buildBody() { return switch (_selectedIndex) { 0 => _RoomListBody(searchQuery: _searchQuery), 1 => const SpacesScreen(embedded: true), 2 => const ProfileScreen(embedded: true), 3 => const ConferenceTab(), 4 => const HelpTab(), _ => const _RoomListBody(), }; } @override Widget build(BuildContext context) { return Scaffold( appBar: _selectedIndex == 0 ? AppBar( title: _searchActive ? TextField( controller: _searchController, autofocus: true, decoration: const InputDecoration( hintText: 'Search rooms...', border: InputBorder.none, ), onChanged: (v) => setState(() => _searchQuery = v), ) : const Text('M8Chat'), actions: [ const PanicButton(), IconButton( icon: Icon(_searchActive ? Icons.close : Icons.search), tooltip: _searchActive ? 'Cancel search' : 'Search rooms', onPressed: _toggleSearch, ), if (!_searchActive) IconButton( icon: const Icon(Icons.explore_outlined), tooltip: 'Find rooms', onPressed: () => showPublicRoomsDialog(context), ), ], ) : null, body: _buildBody(), floatingActionButton: _selectedIndex == 0 && !_searchActive ? FloatingActionButton.extended( onPressed: () => showUserSearchDialog(context), tooltip: 'New message', icon: const Icon(Icons.edit_square), label: const Text('New message'), ) : null, bottomNavigationBar: NavigationBar( selectedIndex: _selectedIndex, onDestinationSelected: (index) => setState(() => _selectedIndex = index), destinations: _destinations, ), ); } } class _RoomListBody extends ConsumerWidget { const _RoomListBody({this.searchQuery = ''}); final String searchQuery; @override Widget build(BuildContext context, WidgetRef ref) { final roomsAsync = ref.watch(roomsListProvider); return roomsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 48), const SizedBox(height: 16), Text( 'Could not load rooms.', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Text( error.toString(), textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 24), ElevatedButton( onPressed: () => ref.refresh(roomsListProvider), child: const Text('Retry'), ), ], ), ), ), data: (rooms) { final filtered = searchQuery.trim().isEmpty ? rooms : rooms .where((r) => r.displayName .toLowerCase() .contains(searchQuery.trim().toLowerCase())) .toList(); if (filtered.isEmpty) { return searchQuery.isNotEmpty ? Center( child: Padding( padding: const EdgeInsets.all(32), child: Text( 'No rooms match "$searchQuery".', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context) .colorScheme .onSurface .withAlpha(153), ), ), ), ) : const _EmptyRoomsState(); } return ListView.separated( itemCount: filtered.length, separatorBuilder: (_, __) => const Divider(height: 1, indent: 72), itemBuilder: (context, index) { final room = filtered[index]; return RoomTile( room: room, onTap: () => context.push('/rooms/${Uri.encodeComponent(room.id)}'), ); }, ); }, ); } } class _EmptyRoomsState extends StatelessWidget { const _EmptyRoomsState(); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.chat_bubble_outline, size: 64, color: theme.colorScheme.onSurface.withAlpha(77), ), const SizedBox(height: 16), Text( 'No rooms yet', style: theme.textTheme.titleMedium?.copyWith( color: M8Colours.subtleText, ), ), const SizedBox(height: 8), Text( 'Tap the New message button to message someone, or find ' 'a public room to join.', textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith( color: M8Colours.subtleText, ), ), const SizedBox(height: 20), OutlinedButton.icon( onPressed: () => showPublicRoomsDialog(context), icon: const Icon(Icons.explore_outlined), label: const Text('Find rooms'), ), ], ), ), ); } }