simplified code across the app and consolidated duplicated helpers
This commit is contained in:
+37
-32
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -76,13 +77,16 @@ final Map<String, Future<AvatarPayload?>> _pendingAvatars = {};
|
||||
final ValueNotifier<int> _avatarCacheGeneration = ValueNotifier<int>(0);
|
||||
|
||||
String avatarUrl({required String id, required bool isGroup, int size = 512}) {
|
||||
final host = EndpointData().nextcloud().full();
|
||||
if (isGroup) {
|
||||
final host = EndpointData().nextcloud().full();
|
||||
return 'https://$host/ocs/v2.php/apps/spreed/api/v1/room/$id/avatar';
|
||||
}
|
||||
return 'https://$host/avatar/$id/$size';
|
||||
return '${_userAvatarUrlPrefix(id)}$size';
|
||||
}
|
||||
|
||||
String _userAvatarUrlPrefix(String id) =>
|
||||
'https://${EndpointData().nextcloud().full()}/avatar/$id/';
|
||||
|
||||
/// Resolves an avatar through the shared caches: in-memory first, then disk,
|
||||
/// then the network — the fetch being deduped across every concurrent caller
|
||||
/// and written back to both caches.
|
||||
@@ -109,26 +113,32 @@ Future<AvatarPayload?> loadAvatarPayload({
|
||||
return payload;
|
||||
}
|
||||
|
||||
final payload = await _fetchDeduped(url);
|
||||
_commitAvatar(url, id: id, isGroup: isGroup, size: size, payload: payload);
|
||||
return payload;
|
||||
return _fetchDeduped(url, id: id, isGroup: isGroup, size: size);
|
||||
}
|
||||
|
||||
/// Shares one in-flight request per URL across all callers.
|
||||
Future<AvatarPayload?> _fetchDeduped(String url) =>
|
||||
_pendingAvatars.putIfAbsent(url, () {
|
||||
final future = _fetchAvatarPayload(url);
|
||||
// Cleanup hangs off an error-neutralised copy: whenComplete on `future`
|
||||
// itself returns a second future that forwards the error unawaited.
|
||||
unawaited(
|
||||
future.then<void>((_) {}, onError: (_) {}).whenComplete(() {
|
||||
if (identical(_pendingAvatars[url], future)) {
|
||||
_pendingAvatars.remove(url);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return future;
|
||||
});
|
||||
/// Shares one in-flight request per URL across all callers; the result is
|
||||
/// committed to the caches once, not once per waiting caller.
|
||||
Future<AvatarPayload?> _fetchDeduped(
|
||||
String url, {
|
||||
required String id,
|
||||
required bool isGroup,
|
||||
required int size,
|
||||
}) => _pendingAvatars.putIfAbsent(url, () {
|
||||
final future = _fetchAvatarPayload(url).then((payload) {
|
||||
_commitAvatar(url, id: id, isGroup: isGroup, size: size, payload: payload);
|
||||
return payload;
|
||||
});
|
||||
// Cleanup hangs off an error-neutralised copy: whenComplete on `future`
|
||||
// itself returns a second future that forwards the error unawaited.
|
||||
unawaited(
|
||||
future.then<void>((_) {}, onError: (_) {}).whenComplete(() {
|
||||
if (identical(_pendingAvatars[url], future)) {
|
||||
_pendingAvatars.remove(url);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return future;
|
||||
});
|
||||
|
||||
/// Persists a resolved result to the in-memory and disk caches.
|
||||
void _commitAvatar(
|
||||
@@ -189,10 +199,7 @@ AvatarPayload _payloadFromBytes(Uint8List bytes) =>
|
||||
|
||||
bool _looksLikeSvg(Uint8List bytes) {
|
||||
final head = utf8
|
||||
.decode(
|
||||
bytes.sublist(0, bytes.length < 256 ? bytes.length : 256),
|
||||
allowMalformed: true,
|
||||
)
|
||||
.decode(bytes.sublist(0, min(bytes.length, 256)), allowMalformed: true)
|
||||
.trimLeft();
|
||||
return head.startsWith('<?xml') || head.startsWith('<svg');
|
||||
}
|
||||
@@ -220,8 +227,7 @@ void invalidateAvatarCache({String? id, bool? isGroup}) {
|
||||
unawaited(PushAvatarStore.evict(id));
|
||||
} else {
|
||||
// User avatars include the rendered size in the URL — drop every variant.
|
||||
final host = EndpointData().nextcloud().full();
|
||||
final prefix = 'https://$host/avatar/$id/';
|
||||
final prefix = _userAvatarUrlPrefix(id);
|
||||
_resolvedAvatars.removeWhere((url, _) => url.startsWith(prefix));
|
||||
_pendingAvatars.removeWhere((url, _) => url.startsWith(prefix));
|
||||
unawaited(AvatarDiskCache.instance.evict(id: id, isGroup: false));
|
||||
@@ -285,14 +291,15 @@ class _UserAvatarState extends State<UserAvatar> {
|
||||
int _resolvedRequestSize() =>
|
||||
widget.requestSize ?? (widget.size * 4).clamp(64, 1024);
|
||||
|
||||
String _url() => avatarUrl(
|
||||
String _url([int? size]) => avatarUrl(
|
||||
id: widget.id,
|
||||
isGroup: widget.isGroup,
|
||||
size: _resolvedRequestSize(),
|
||||
size: size ?? _resolvedRequestSize(),
|
||||
);
|
||||
|
||||
void _attach() {
|
||||
final url = _url();
|
||||
final size = _resolvedRequestSize();
|
||||
final url = _url(size);
|
||||
final cached = _readAvatarCache(url);
|
||||
if (cached != null) {
|
||||
_payload = cached.payload;
|
||||
@@ -302,7 +309,6 @@ class _UserAvatarState extends State<UserAvatar> {
|
||||
// the widget may have been recycled onto a different id by then.
|
||||
final id = widget.id;
|
||||
final isGroup = widget.isGroup;
|
||||
final size = _resolvedRequestSize();
|
||||
|
||||
// Persistent disk cache: on a warm session (cache directory already known)
|
||||
// this hits synchronously, so a cold app start paints the last-known
|
||||
@@ -348,14 +354,13 @@ class _UserAvatarState extends State<UserAvatar> {
|
||||
|
||||
AvatarPayload? fresh;
|
||||
try {
|
||||
fresh = await _fetchDeduped(url);
|
||||
fresh = await _fetchDeduped(url, id: id, isGroup: isGroup, size: size);
|
||||
} on Object {
|
||||
// Transient failure (offline, 5xx). Keep showing the cached picture; the
|
||||
// next mount retries. Deliberately no null-cache so we don't mask it.
|
||||
return;
|
||||
}
|
||||
|
||||
_commitAvatar(url, id: id, isGroup: isGroup, size: size, payload: fresh);
|
||||
if (!mounted || _url() != url) return;
|
||||
if (fresh == null) {
|
||||
// HTTP 404 — the avatar was removed server-side. Fall back to the icon.
|
||||
|
||||
Reference in New Issue
Block a user