import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; import 'matrix/calls.dart'; import 'matrix/notifications.dart'; import 'screens/call_screen.dart'; import 'screens/home_screen.dart'; import 'screens/login_screen.dart'; import 'screens/room_screen.dart'; import 'state.dart'; import 'theme.dart'; import 'widgets/uia.dart'; import 'widgets/verification_sheet.dart'; final navigatorKey = GlobalKey(); final messengerKey = GlobalKey(); class WarpApp extends ConsumerWidget { const WarpApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final loginState = ref.watch(loginStateProvider); final signedIn = loginState.value == LoginState.loggedIn; // Signing in from the sign-up screen, or out from settings, leaves those // screens on top of the new home: close them. ref.listen(loginStateProvider, (prev, next) { if (prev?.value != next.value) { navigatorKey.currentState?.popUntil((route) => route.isFirst); } }); return MaterialApp( title: 'Warp', navigatorKey: navigatorKey, scaffoldMessengerKey: messengerKey, debugShowCheckedModeBanner: false, theme: warpTheme(Brightness.light), darkTheme: warpTheme(Brightness.dark), themeMode: ref.watch(themeModeProvider), home: loginState.isLoading ? const Scaffold() : signedIn ? const _SignedIn(key: ValueKey('signed-in')) : const LoginScreen(key: ValueKey('signed-out')), ); } } /// Everything that runs only while signed in: the notification listener, /// prompts the SDK raises (a password for an account change, an incoming /// verification request) and opening rooms from notifications. class _SignedIn extends ConsumerStatefulWidget { const _SignedIn({super.key}); @override ConsumerState<_SignedIn> createState() => _SignedInState(); } class _SignedInState extends ConsumerState<_SignedIn> with WidgetsBindingObserver { final _subs = []; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); final client = ref.read(clientProvider); _subs ..add(Notifications.watch(client)) ..add(Notifications.opened.listen(_openRoom)) ..add(client.onUiaRequest.stream.listen(_onUia)) ..add(client.onKeyVerificationRequest.stream.listen(_onVerification)); final calls = ref.read(callsProvider); calls.addListener(_onCalls); _subs.add(calls.openRequests.listen((_) => _openCall())); _subs.add( calls.errors.stream.listen( (message) => messengerKey.currentState?.showSnackBar( SnackBar(content: Text(message)), ), ), ); Notifications.launchRoomId().then((roomId) { if (roomId != null) _openRoom(roomId); }); Notifications.requestPermission(); } IncomingCall? _shownCall; /// Shows the incoming call card when a call starts ringing. void _onCalls() { final calls = ref.read(callsProvider); final call = calls.incoming; if (call == null || call == _shownCall) return; _shownCall = call; final context = navigatorKey.currentContext; final navigator = navigatorKey.currentState; if (context == null || navigator == null) return; if (!Notifications.foreground) { Notifications.show( roomId: call.roomId, title: call.callerName, body: call.video ? 'Video call' : 'Voice call', ); } showIncomingCall(context, calls, navigator); } @override void dispose() { ref.read(callsProvider).removeListener(_onCalls); WidgetsBinding.instance.removeObserver(this); for (final sub in _subs) { sub.cancel(); } super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { // The sync loop keeps running in the background for as long as Android // lets the process live; pushes cover the time after that. Notifications.foreground = state == AppLifecycleState.resumed; } void _openRoom(String roomId) { final nav = navigatorKey.currentState; if (nav == null) return; nav.popUntil((route) => route.isFirst); nav.push(RoomScreen.route(roomId)); } /// Shows the call screen, unless it is already on top. void _openCall() { final nav = navigatorKey.currentState; if (nav == null || !ref.read(callsProvider).inCall) return; var onTop = false; nav.popUntil((route) { onTop = route.settings.name == 'call'; return true; }); if (!onTop) nav.push(CallScreen.route()); } void _onUia(UiaRequest request) { final context = navigatorKey.currentContext; if (context == null) return request.cancel(); handleUia(context, ref.read(clientProvider), request); } void _onVerification(KeyVerification request) { final context = navigatorKey.currentContext; if (context == null) return; showVerificationSheet(context, request); } @override Widget build(BuildContext context) => const HomeScreen(); }