// Version: 1.2.0 | Created: 2026-04-01 | Updated: 2026-04-02 // Spaces screen — list of Matrix spaces with expandable child room lists. import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../../shared/widgets/matrix_avatar.dart'; import '../data/spaces_repository.dart'; import '../domain/space_model.dart'; part 'spaces_screen.g.dart'; // --------------------------------------------------------------------------- // Providers // --------------------------------------------------------------------------- @riverpod Stream> spacesStream(Ref ref) { return ref.watch(spacesRepositoryProvider).watchSpaces(); } @riverpod List spaceRooms(Ref ref, String spaceId) { return ref.watch(spacesRepositoryProvider).getRoomsInSpace(spaceId); } // --------------------------------------------------------------------------- // Screen // --------------------------------------------------------------------------- class SpacesScreen extends ConsumerWidget { const SpacesScreen({super.key, this.embedded = false}); final bool embedded; @override Widget build(BuildContext context, WidgetRef ref) { final spacesAsync = ref.watch(spacesStreamProvider); final body = spacesAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (err, _) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Text('Could not load spaces: $err'), ), ), data: (spaces) { if (spaces.isEmpty) { return _EmptySpacesState(); } return ListView.builder( itemCount: spaces.length, itemBuilder: (context, index) { return _SpaceTile(space: spaces[index]); }, ); }, ); if (embedded) return body; return Scaffold( appBar: AppBar(title: const Text('Spaces')), body: body, ); } } // --------------------------------------------------------------------------- // Space tile with expandable child room list // --------------------------------------------------------------------------- class _SpaceTile extends ConsumerStatefulWidget { const _SpaceTile({required this.space}); final SpaceModel space; @override ConsumerState<_SpaceTile> createState() => _SpaceTileState(); } class _SpaceTileState extends ConsumerState<_SpaceTile> { bool _expanded = false; @override Widget build(BuildContext context) { final theme = Theme.of(context); final space = widget.space; return Column( children: [ // Space header row ListTile( leading: MatrixAvatar( name: space.displayName, avatarUrl: space.avatarUrl, radius: 22, ), title: Text( space.displayName, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), subtitle: Text( '${space.roomCount} ${space.roomCount == 1 ? 'room' : 'rooms'}', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withAlpha(153), ), ), trailing: space.roomCount > 0 ? IconButton( icon: Icon( _expanded ? Icons.expand_less : Icons.expand_more, color: theme.colorScheme.onSurface.withAlpha(153), ), onPressed: () => setState(() => _expanded = !_expanded), ) : null, onTap: () => setState(() => _expanded = !_expanded), ), // Child rooms — shown when expanded if (_expanded) _ChildRoomList(spaceId: space.id), const Divider(height: 1), ], ); } } // --------------------------------------------------------------------------- // Child room list // --------------------------------------------------------------------------- class _ChildRoomList extends ConsumerWidget { const _ChildRoomList({required this.spaceId}); final String spaceId; @override Widget build(BuildContext context, WidgetRef ref) { final rooms = ref.watch(spaceRoomsProvider(spaceId)); final theme = Theme.of(context); if (rooms.isEmpty) { return Padding( padding: const EdgeInsets.only(left: 56, right: 16, bottom: 8), child: Text( 'No joined rooms in this space.', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withAlpha(102), ), ), ); } return Column( children: rooms.map((room) { return ListTile( contentPadding: const EdgeInsets.only(left: 56, right: 16), leading: MatrixAvatar( name: room.displayName, avatarUrl: room.avatarUrl, radius: 18, ), title: Text(room.displayName, style: theme.textTheme.bodyMedium), trailing: room.isDirect ? Icon( Icons.person_outline, size: 16, color: theme.colorScheme.onSurface.withAlpha(102), ) : null, onTap: () => context.push('/rooms/${Uri.encodeComponent(room.id)}'), ); }).toList(), ); } } // --------------------------------------------------------------------------- // Empty state // --------------------------------------------------------------------------- class _EmptySpacesState extends StatelessWidget { @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.dashboard_outlined, size: 64, color: theme.colorScheme.onSurface.withAlpha(77), ), const SizedBox(height: 16), Text( 'No spaces yet', style: theme.textTheme.titleMedium?.copyWith( color: theme.colorScheme.onSurface.withAlpha(153), ), ), const SizedBox(height: 8), Text( 'Spaces you join will appear here.', textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withAlpha(102), ), ), ], ), ), ); } }