Mobile: - The room list's title picks a space: All chats, or one space, which narrows the list to its rooms (and its subspaces' rooms). - A space browser over /hierarchy: the rooms of a space and of its subspaces, joined or joinable, with Join; for those who may, adding an existing room or a new one to the space. - New space in the new-chat sheet; invites to spaces show with the other invites and open the space when joined. - Fixes several setState calls that were handed a Future (reloading the members, sessions, push distributor, threads), which Flutter rejects. Server: /hierarchy left out children_state for a room without children. The spec requires it on every room (an empty list), and SpaceRoom's omitempty dropped empty lists too, despite its comment. The famedly Dart SDK fails to parse such a response. The client API now wraps each room so the field is always written; the federation response, which wants it left out of the children it lists, is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
2.4 KiB
Dart
75 lines
2.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:matrix/matrix.dart';
|
|
|
|
/// The one Matrix client, created in main() before the app starts.
|
|
final clientProvider = Provider<Client>(
|
|
(ref) => throw UnimplementedError('overridden in main()'),
|
|
);
|
|
|
|
/// Signed in or not. Starts from the restored session.
|
|
final loginStateProvider = StreamProvider<LoginState>((ref) async* {
|
|
final client = ref.watch(clientProvider);
|
|
yield client.isLogged() ? LoginState.loggedIn : LoginState.loggedOut;
|
|
yield* client.onLoginStateChanged.stream;
|
|
});
|
|
|
|
/// Bumps on every sync. The SDK's objects change in place, so screens that
|
|
/// show rooms watch this and read the client again; that is the whole
|
|
/// trick that keeps them current.
|
|
final syncProvider = StreamProvider<int>((ref) async* {
|
|
final client = ref.watch(clientProvider);
|
|
var n = 0;
|
|
yield n;
|
|
await for (final _ in client.onSync.stream) {
|
|
yield ++n;
|
|
}
|
|
});
|
|
|
|
/// Whether the connection to the homeserver is working, from the sync
|
|
/// loop's status. Null until the first sync after start finishes.
|
|
final syncErrorProvider = StreamProvider<bool>((ref) async* {
|
|
final client = ref.watch(clientProvider);
|
|
await for (final status in client.onSyncStatus.stream) {
|
|
if (status.status == SyncStatus.error) yield true;
|
|
if (status.status == SyncStatus.finished) yield false;
|
|
}
|
|
});
|
|
|
|
/// The theme the user picked: the system's, light or dark.
|
|
class ThemeModeNotifier extends Notifier<ThemeMode> {
|
|
static const _key = 'warp.theme';
|
|
static const _storage = FlutterSecureStorage();
|
|
|
|
@override
|
|
ThemeMode build() {
|
|
_storage.read(key: _key).then((value) {
|
|
final mode = ThemeMode.values.where((m) => m.name == value).firstOrNull;
|
|
if (mode != null) state = mode;
|
|
});
|
|
return ThemeMode.system;
|
|
}
|
|
|
|
void set(ThemeMode mode) {
|
|
state = mode;
|
|
_storage.write(key: _key, value: mode.name);
|
|
}
|
|
}
|
|
|
|
final themeModeProvider = NotifierProvider<ThemeModeNotifier, ThemeMode>(
|
|
ThemeModeNotifier.new,
|
|
);
|
|
|
|
/// The space the room list shows, or null for all chats.
|
|
class SelectedSpaceNotifier extends Notifier<String?> {
|
|
@override
|
|
String? build() => null;
|
|
|
|
void select(String? spaceId) => state = spaceId;
|
|
}
|
|
|
|
final selectedSpaceProvider = NotifierProvider<SelectedSpaceNotifier, String?>(
|
|
SelectedSpaceNotifier.new,
|
|
);
|