Files
Warp/apps/mobile/lib/app.dart
T
tajniak81andClaude Opus 5.5 3356089c13 Android app (apps/mobile) with Flutter
apps/mobile is a Matrix client for Android on the famedly Matrix Dart SDK,
with vodozemac (compiled from Rust during the build) for end-to-end
encryption. It speaks the plain Client-Server API and uses the web
client's design tokens, fonts and symbol.

- Sign in by server name (.well-known), homeserver address or full user ID;
  sign up with the dummy and registration token stages. Sessions survive
  restarts: SQLite in private storage, credentials also in the Keystore,
  both excluded from backups and device transfers.
- Room list with unread and mention counts, invites, search and filters.
  Timeline with grouping, day separators, notices, history on scroll,
  links, images and files (authenticated media). Sending text, photos,
  camera shots and files, with Sending/Sent/Read/Not sent and Retry.
  Typing, receipts, deleting. New rooms and DMs, encrypted by default;
  joining by address; room info with members, invite, rename, topic,
  notification level, leave.
- Encryption: one-step setup behind a recovery key, verifying a new session
  with the recovery key (restoring the backup) or another session, emoji
  verification of people and sessions, reset. Undecryptable messages say
  why. Session list with verify and sign out.
- Notifications while running, and push through UnifiedPush (no Firebase):
  the distributor's endpoint becomes an event_id_only pusher, the gateway
  is discovered, and a push starts the app headless to fetch, decrypt and
  show the event.

Make targets: mobile (release APKs per ABI into bin/), mobile-run,
mobile-test. Kotlin incremental compilation is off (Windows, project and
pub cache on different drives), and Tink's JVM artifact is excluded in
favour of tink-android.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:45:06 +02:00

117 lines
3.6 KiB
Dart

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/notifications.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<NavigatorState>();
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,
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 = <StreamSubscription>[];
@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));
Notifications.launchRoomId().then((roomId) {
if (roomId != null) _openRoom(roomId);
});
Notifications.requestPermission();
}
@override
void dispose() {
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));
}
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();
}