feat: room search, room options sheet, .htaccess for E2EE

- Rooms screen: inline search filter with toggle — replaces the no-op button
- Chat screen: room options sheet — member list, invite, leave room
- rooms_repository: remove debug prints
- web/.htaccess: COOP+COEP headers for SharedArrayBuffer / LiveKit E2EE worker
  (was on production but missing from source; adds it to build output automatically)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 05:49:10 +10:00
parent 7e16b825c5
commit c1b5b31d48
5 changed files with 276 additions and 29 deletions

View File

@@ -1,8 +1,7 @@
// Version: 1.2.0 | Created: 2026-04-01 | Updated: 2026-04-02
// Version: 1.3.0 | Created: 2026-04-01 | Updated: 2026-04-27
// Rooms repository. Reads room list from the Matrix SDK client.
import 'package:matrix/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../core/network/matrix_client.dart';
@@ -25,10 +24,6 @@ class RoomsRepository {
/// Returns the current room list, sorted unread-first then by last activity.
List<RoomModel> getRooms() {
final rooms = _client.rooms;
debugPrint('[Rooms] client.rooms count: ${rooms.length}');
for (final r in rooms) {
debugPrint('[Rooms] ${r.id} "${r.getLocalizedDisplayname()}" isSpace=${r.isSpace}');
}
final models = rooms.map(_toModel).toList();
models.sort((a, b) {

View File

@@ -1,4 +1,4 @@
// Version: 1.3.0 | Created: 2026-04-01 | Updated: 2026-04-11
// Version: 1.4.0 | Created: 2026-04-01 | Updated: 2026-04-27
// Main rooms list screen with bottom navigation.
import 'package:flutter/material.dart';
@@ -22,6 +22,25 @@ class RoomsScreen extends ConsumerStatefulWidget {
class _RoomsScreenState extends ConsumerState<RoomsScreen> {
int _selectedIndex = 0;
bool _searchActive = false;
String _searchQuery = '';
final _searchController = TextEditingController();
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _toggleSearch() {
setState(() {
_searchActive = !_searchActive;
if (!_searchActive) {
_searchQuery = '';
_searchController.clear();
}
});
}
static const _destinations = [
NavigationDestination(
@@ -53,7 +72,7 @@ class _RoomsScreenState extends ConsumerState<RoomsScreen> {
Widget _buildBody() {
return switch (_selectedIndex) {
0 => const _RoomListBody(),
0 => _RoomListBody(searchQuery: _searchQuery),
1 => const SpacesScreen(embedded: true),
2 => const ProfileScreen(embedded: true),
3 => const ConferenceTab(),
@@ -67,20 +86,29 @@ class _RoomsScreenState extends ConsumerState<RoomsScreen> {
return Scaffold(
appBar: _selectedIndex == 0
? AppBar(
title: const Text('M8Chat'),
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: [
IconButton(
icon: const Icon(Icons.search),
tooltip: 'Search rooms',
onPressed: () {
// Phase 2: room search
},
),
IconButton(
icon: const Icon(Icons.edit_square),
tooltip: 'New message',
onPressed: () => showUserSearchDialog(context),
icon: Icon(_searchActive ? Icons.close : Icons.search),
tooltip: _searchActive ? 'Cancel search' : 'Search rooms',
onPressed: _toggleSearch,
),
if (!_searchActive)
IconButton(
icon: const Icon(Icons.edit_square),
tooltip: 'New message',
onPressed: () => showUserSearchDialog(context),
),
],
)
: null,
@@ -96,7 +124,9 @@ class _RoomsScreenState extends ConsumerState<RoomsScreen> {
}
class _RoomListBody extends ConsumerWidget {
const _RoomListBody();
const _RoomListBody({this.searchQuery = ''});
final String searchQuery;
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -132,14 +162,39 @@ class _RoomListBody extends ConsumerWidget {
),
),
data: (rooms) {
if (rooms.isEmpty) {
return const _EmptyRoomsState();
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: rooms.length,
itemCount: filtered.length,
separatorBuilder: (_, __) => const Divider(height: 1, indent: 72),
itemBuilder: (context, index) {
final room = rooms[index];
final room = filtered[index];
return RoomTile(
room: room,
onTap: () =>