Calls the way the web client and Element Call do them, with the same events, so the three can share a call. The famedly SDK's own group calls use com.famedly.call.member events, which the others don't read, so this is written against the matrix-js-sdk wire format instead (lib/matrix/ calls.dart): - Membership: org.matrix.msc3401.call.member state, key _@user:server_DEVICE_m.call, with the legacy session fields the js-sdk sends (application m.call, call_id "", membershipID, expires, focus_active, foci_preferred). An empty delayed copy (MSC4140) is scheduled first and pushed back every 5 s; leaving sends the empty state and cancels it. - Media: a LiveKit token from the JWT service (the call's, else the homeserver's rtc/transports or .well-known rtc_foci) for an OpenID token; livekit_client connects. - Encrypted rooms: a random 16-byte key per device, sent to every other device in the call over encrypted to-device io.element.call.encryption_keys, keys from others applied per LiveKit identity (@user:server:DEVICE); per-participant keys, no ratcheting, HKDF, like the web client's livekit-client. - Ringing: a call started in a room of two sends a ring notification (MSC4075); the other side gets an incoming-call card and an insistent ringtone notification (full screen on a locked phone), declines with MSC4310 or answers by joining. The caller gives up after a minute or on a decline. - UI: voice and video buttons in the room header, a call screen (the other person large and you in the corner, or a grid; mute, camera, flip, leave), a Join banner and a room-list marker for calls going on. Not tested end to end yet: it needs a LiveKit SFU and JWT service. README: replies, reactions, threads, spaces, QR verification, sliding sync and calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
2.7 KiB
Dart
85 lines
2.7 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';
|
|
|
|
import 'matrix/calls.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,
|
|
);
|
|
|
|
/// Calls: one manager for the signed-in client. Screens listen to it
|
|
/// directly (it is a ChangeNotifier).
|
|
final callsProvider = Provider<Calls>((ref) {
|
|
final calls = Calls(ref.watch(clientProvider));
|
|
ref.onDispose(calls.dispose);
|
|
return calls;
|
|
});
|