Files
Warp/apps/mobile/lib/matrix/media.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

88 lines
2.8 KiB
Dart

import 'dart:typed_data';
import 'package:matrix/matrix.dart';
/// Loads media for display: avatars by their `mxc://` URI, attachments by
/// their event.
///
/// Media is authenticated (Matrix 1.11), so it can't be handed to an image
/// widget as a URL: it is fetched with the access token here. Attachments go
/// through the SDK, which decrypts files from encrypted rooms and keeps them
/// in its file store; avatars are cached in that store too. A small
/// in-memory cache in front keeps scrolling from refetching.
class Media {
Media._();
static const _maxEntries = 300;
static final _cache = <String, Future<Uint8List?>>{};
static Future<Uint8List?> _remember(
String key,
Future<Uint8List?> Function() load,
) {
final hit = _cache.remove(key);
if (hit != null) {
_cache[key] = hit;
return hit;
}
final future = load().catchError((Object e) {
Logs().v('Could not load $key', e);
_cache.remove(key);
return null;
});
_cache[key] = future;
while (_cache.length > _maxEntries) {
_cache.remove(_cache.keys.first);
}
return future;
}
/// A square thumbnail of [mxc], [size] logical pixels (fetched at 2x).
static Future<Uint8List?> thumbnail(Client client, Uri mxc, int size) {
final px = size * 2 < 96 ? 96 : size * 2;
return _remember('$mxc@$px', () async {
final stored = await client.database.getFile(_storeKey(mxc, px));
if (stored != null) return stored;
final uri = await mxc.getThumbnailUri(
client,
width: px,
height: px,
method: ThumbnailMethod.crop,
);
final response = await client.httpClient.get(
uri,
headers: {'authorization': 'Bearer ${client.accessToken}'},
);
if (response.statusCode != 200) return null;
final bytes = response.bodyBytes;
await client.database.storeFile(
_storeKey(mxc, px),
bytes,
DateTime.now().millisecondsSinceEpoch,
);
return bytes;
});
}
static Uri _storeKey(Uri mxc, int px) =>
Uri(scheme: 'cache', host: 'avatar', path: '${mxc.host}${mxc.path}@$px');
/// The attachment of [event], or its thumbnail when it has one and
/// [thumbnail] is set. Decrypted when the room is encrypted.
static Future<Uint8List?> attachment(Event event, {bool thumbnail = false}) {
final useThumb = thumbnail && event.hasThumbnail;
return _remember(
'${event.eventId}${useThumb ? '#thumb' : ''}',
() async => (await event.downloadAndDecryptAttachment(
getThumbnail: useThumb,
)).bytes,
);
}
/// Forgets an event's cached attachment, for a retry after a failure.
static void evict(Event event) {
_cache.remove(event.eventId);
_cache.remove('${event.eventId}#thumb');
}
}