implemented chat long-polling and optimistic updates, centralized notification management, optimized avatar caching

This commit is contained in:
2026-05-10 15:47:55 +02:00
parent 6ae396e605
commit 1458d8ce49
15 changed files with 712 additions and 146 deletions
+57 -47
View File
@@ -29,15 +29,18 @@ class _AvatarPayload {
_AvatarPayload(this.bytes, this.isSvg);
}
final Map<String, Future<_AvatarPayload?>> _avatarCache = {};
// Resolved payloads are cached so re-mounts render synchronously; in-flight
// requests are deduped so concurrent mounts share one HTTP call.
final Map<String, _AvatarPayload?> _resolvedAvatars = {};
final Map<String, Future<_AvatarPayload?>> _pendingAvatars = {};
class _UserAvatarState extends State<UserAvatar> {
late Future<_AvatarPayload?> _payload;
_AvatarPayload? _payload;
@override
void initState() {
super.initState();
_payload = _load();
_attach();
}
@override
@@ -46,7 +49,7 @@ class _UserAvatarState extends State<UserAvatar> {
if (oldWidget.id != widget.id ||
oldWidget.isGroup != widget.isGroup ||
oldWidget.size != widget.size) {
_payload = _load();
_attach();
}
}
@@ -58,9 +61,20 @@ class _UserAvatarState extends State<UserAvatar> {
return 'https://$host/avatar/${widget.id}/${widget.size}';
}
Future<_AvatarPayload?> _load() {
void _attach() {
final url = _url();
return _avatarCache.putIfAbsent(url, () => _fetch(url));
if (_resolvedAvatars.containsKey(url)) {
_payload = _resolvedAvatars[url];
return;
}
_payload = null;
final pending = _pendingAvatars.putIfAbsent(url, () => _fetch(url));
pending.then((p) {
_resolvedAvatars[url] = p;
_pendingAvatars.remove(url);
if (!mounted || _url() != url) return;
setState(() => _payload = p);
});
}
Future<_AvatarPayload?> _fetch(String url) async {
@@ -97,49 +111,45 @@ class _UserAvatarState extends State<UserAvatar> {
Widget build(BuildContext context) {
final radius = widget.size.toDouble();
final theme = Theme.of(context);
final payload = _payload;
return FutureBuilder<_AvatarPayload?>(
future: _payload,
builder: (context, snapshot) {
final payload = snapshot.data;
Widget content;
if (payload == null) {
content = Icon(
widget.isGroup ? Icons.group : Icons.person,
size: radius,
color: Colors.white,
);
} else if (payload.isSvg) {
content = SvgPicture.memory(
payload.bytes,
width: radius * 2,
height: radius * 2,
fit: BoxFit.cover,
);
} else {
content = Image.memory(
payload.bytes,
width: radius * 2,
height: radius * 2,
fit: BoxFit.cover,
gaplessPlayback: true,
);
}
return CircleAvatar(
radius: radius,
backgroundColor: theme.primaryColor,
foregroundColor: Colors.white,
child: ClipOval(
child: SizedBox(
width: radius * 2,
height: radius * 2,
child: content,
),
),
Widget content;
if (payload != null) {
if (payload.isSvg) {
content = SvgPicture.memory(
payload.bytes,
width: radius * 2,
height: radius * 2,
fit: BoxFit.cover,
);
},
} else {
content = Image.memory(
payload.bytes,
width: radius * 2,
height: radius * 2,
fit: BoxFit.cover,
gaplessPlayback: true,
);
}
} else {
content = Icon(
widget.isGroup ? Icons.group : Icons.person,
size: radius,
color: Colors.white,
);
}
return CircleAvatar(
radius: radius,
backgroundColor: theme.primaryColor,
foregroundColor: Colors.white,
child: ClipOval(
child: SizedBox(
width: radius * 2,
height: radius * 2,
child: content,
),
),
);
}
}