Files
m8chat-app2/lib/features/calls/presentation/call_controller.dart
help4bis b941cdfe4b refactor: /simplify — 22 fixes from 3-agent code review
Critical:
- Fix MXC URI resolution: all avatars/images now resolve mxc:// to HTTP
- Sync persistence: only write changed rooms, batch message upserts
- lastActivityAt uses room.lastEvent.originServerTs, not creation time

High:
- Shared MatrixAvatar widget replaces 6 duplicate implementations
- CallScreen decodes roomId before LiveKit JWT fetch
- Decline button actually dismisses incoming call overlay
- EventTypes constants replace raw string literals
- LiveKitService uses lazy auth reads, onDispose disconnects

Medium:
- CallController is keepAlive with timer/room cleanup
- authRepository is keepAlive (used from keepAlive notifier)
- StreamController not closed in stopListening (crash fix)
- Index on messages.roomId for query performance
- 400ms debounce on user search
- Static DateFormat in MessageBubble
- Hardcoded strings replaced with AppConfig refs
- Duplicate isDirectMessage field removed from RoomModel
- E2EE profile claim corrected to Phase 3

Shared utilities:
- lib/shared/widgets/matrix_avatar.dart
- lib/shared/utils/mxc_url.dart
- lib/shared/utils/room_preview.dart
- lib/shared/utils/matrix_id.dart

rawJson column removed (unused, caused main-thread jsonEncode)
Schema migrated to v2 with roomId index.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:19:22 +10:00

117 lines
3.6 KiB
Dart

// Version: 1.2.0 | Created: 2026-04-01 | Updated: 2026-04-02
// 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: true)
class CallController extends _$CallController {
Timer? _durationTimer;
Duration _elapsed = Duration.zero;
@override
CallState build() {
ref.onDispose(() {
_durationTimer?.cancel();
_durationTimer = null;
ref.read(liveKitServiceProvider).disconnect();
});
return const CallState.idle();
}
/// 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);
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);
}
});
}
}