feat: Phase 2 complete — calls, media, spaces, persistence, chat improvements
- LiveKit/MatrixRTC voice+video calls with full call screen UI - Incoming call overlay (accept/decline) - Media upload/download — file picker, image rendering, file download - Spaces navigation — space list + expandable child rooms - Drift persistence — rooms + messages written on every sync - Sync persistence auto-starts on login and session restore - Chat: typing indicators, long-press menu, reply, emoji reactions - User search dialog + start DM from rooms screen - Android: INTERNET + CAMERA + RECORD_AUDIO permissions in main manifest - Emoji picker for reactions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,109 @@
|
||||
// Version: 1.0.0 | Created: 2026-04-01
|
||||
// Call controller stub. LiveKit integration deferred to Phase 2.
|
||||
// Version: 1.1.0 | Created: 2026-04-01
|
||||
// Call controller — manages LiveKit room connection lifecycle.
|
||||
// Transitions through idle → connecting → active → ended states.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../data/livekit_service.dart';
|
||||
import '../domain/call_state.dart';
|
||||
|
||||
part 'call_controller.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: false)
|
||||
class CallController extends _$CallController {
|
||||
Timer? _durationTimer;
|
||||
Duration _elapsed = Duration.zero;
|
||||
|
||||
@override
|
||||
CallState build() => const CallState.idle();
|
||||
|
||||
/// Phase 2: join a LiveKit room via MatrixRTC JWT endpoint.
|
||||
Future<void> joinCall(String roomId) async {
|
||||
/// Join a LiveKit room via MatrixRTC JWT endpoint.
|
||||
///
|
||||
/// On success, starts a timer to track call duration and transitions to
|
||||
/// [CallActive]. On failure, transitions to [CallEnded] with a reason.
|
||||
Future<void> joinCall(String roomId, {bool withVideo = true}) async {
|
||||
state = CallState.connecting(roomId: roomId);
|
||||
// TODO(phase2): fetch JWT from AppConfig.livekitJwtUrl and connect LiveKit client.
|
||||
state = const CallState.ended(reason: 'Calls not yet implemented.');
|
||||
|
||||
final service = ref.read(liveKitServiceProvider);
|
||||
final result = await service.connect(roomId);
|
||||
|
||||
switch (result) {
|
||||
case LiveKitConnected(:final room):
|
||||
// Enable camera and microphone on the local participant.
|
||||
final local = room.localParticipant;
|
||||
if (local != null) {
|
||||
if (withVideo) {
|
||||
await local.setCameraEnabled(true);
|
||||
}
|
||||
await local.setMicrophoneEnabled(true);
|
||||
}
|
||||
_startTimer(roomId, room, isVideo: withVideo);
|
||||
case LiveKitFailed(:final failure):
|
||||
state = CallEnded(
|
||||
reason: switch (failure) {
|
||||
LiveKitNotAuthenticated() => 'Not authenticated.',
|
||||
LiveKitJwtFetchFailed(:final message) =>
|
||||
'Could not connect: $message',
|
||||
LiveKitConnectFailed(:final message) => 'Call failed: $message',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle microphone on/off during an active call.
|
||||
Future<void> toggleAudio() async {
|
||||
final current = state;
|
||||
if (current is! CallActive) return;
|
||||
|
||||
final room = ref.read(liveKitServiceProvider).activeRoom;
|
||||
final local = room?.localParticipant;
|
||||
if (local == null) return;
|
||||
|
||||
final newEnabled = !current.isAudioEnabled;
|
||||
await local.setMicrophoneEnabled(newEnabled);
|
||||
state = current.copyWith(isAudioEnabled: newEnabled);
|
||||
}
|
||||
|
||||
/// Toggle camera on/off during an active call.
|
||||
Future<void> toggleVideo() async {
|
||||
final current = state;
|
||||
if (current is! CallActive) return;
|
||||
|
||||
final room = ref.read(liveKitServiceProvider).activeRoom;
|
||||
final local = room?.localParticipant;
|
||||
if (local == null) return;
|
||||
|
||||
final newEnabled = !current.isVideoEnabled;
|
||||
await local.setCameraEnabled(newEnabled);
|
||||
state = current.copyWith(isVideoEnabled: newEnabled);
|
||||
}
|
||||
|
||||
/// Hang up — disconnect from LiveKit and transition to [CallEnded].
|
||||
Future<void> endCall() async {
|
||||
_durationTimer?.cancel();
|
||||
_durationTimer = null;
|
||||
await ref.read(liveKitServiceProvider).disconnect();
|
||||
state = const CallState.ended();
|
||||
}
|
||||
|
||||
void _startTimer(String roomId, Room room, {required bool isVideo}) {
|
||||
_elapsed = Duration.zero;
|
||||
state = CallActive(
|
||||
roomId: roomId,
|
||||
duration: _elapsed,
|
||||
isVideoEnabled: isVideo,
|
||||
isAudioEnabled: true,
|
||||
);
|
||||
|
||||
_durationTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
_elapsed += const Duration(seconds: 1);
|
||||
final current = state;
|
||||
if (current is CallActive) {
|
||||
state = current.copyWith(duration: _elapsed);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user