fix: calls — correct MSC4143 JWT flow + message order

Calls:
- JWT fetch now uses correct MSC4143 flow: get OpenID token from
  Synapse, then POST to /_matrix/livekit/jwt/sfu/get (was using
  GET with Bearer token to wrong path — returned 301→404)
- Error messages now visible for 3 seconds before popping screen
  (was flashing away instantly — user couldn't see failure reason)
- Voice vs video calls differentiated via ?video=0/1 query param
- Debug logging added to JWT flow for troubleshooting

Messages:
- Chat timeline now shows newest at bottom (standard behaviour).
  Was reversed twice: SDK returns newest-first, code reversed to
  oldest-first, then ListView(reverse:true) put oldest at bottom.
  Removed the extra .reversed — newest-first + reverse:true = correct.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-03 06:21:08 +10:00
parent b941cdfe4b
commit 1f58c9e21d
5 changed files with 79 additions and 32 deletions

View File

@@ -1,4 +1,4 @@
// Version: 1.2.0 | Created: 2026-04-01 | Updated: 2026-04-02
// Version: 1.2.1 | Created: 2026-04-01 | Updated: 2026-04-03
// Full call screen with LiveKit video/audio.
// - Remote video: full screen background
// - Local video: picture-in-picture overlay (bottom right)
@@ -16,9 +16,14 @@ import '../domain/call_state.dart';
import 'call_controller.dart';
class CallScreen extends ConsumerStatefulWidget {
const CallScreen({super.key, required this.roomId});
const CallScreen({
super.key,
required this.roomId,
this.isVideo = true,
});
final String roomId;
final bool isVideo;
@override
ConsumerState<CallScreen> createState() => _CallScreenState();
@@ -28,12 +33,11 @@ class _CallScreenState extends ConsumerState<CallScreen> {
@override
void initState() {
super.initState();
// Start the call as soon as the screen opens.
// Using addPostFrameCallback so the provider is ready.
WidgetsBinding.instance.addPostFrameCallback((_) {
ref
.read(callControllerProvider.notifier)
.joinCall(Uri.decodeComponent(widget.roomId), withVideo: true);
ref.read(callControllerProvider.notifier).joinCall(
Uri.decodeComponent(widget.roomId),
withVideo: widget.isVideo,
);
});
}
@@ -41,10 +45,25 @@ class _CallScreenState extends ConsumerState<CallScreen> {
Widget build(BuildContext context) {
final callState = ref.watch(callControllerProvider);
// Pop back automatically when call ends.
// On call ended: show error reason for 3 seconds before popping,
// so the user can see WHY the call failed.
ref.listen<CallState>(callControllerProvider, (_, next) {
if (next is CallEnded && context.canPop()) {
context.pop();
if (next is CallEnded && mounted) {
final reason = next.reason;
if (reason != null && reason.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(reason),
backgroundColor: Theme.of(context).colorScheme.error,
duration: const Duration(seconds: 3),
),
);
}
// Delay pop so the user sees the error.
final nav = GoRouter.of(context);
Future.delayed(const Duration(seconds: 3), () {
if (mounted) nav.pop();
});
}
});