Files
buzz/mobile/lib/shared/community/community_icon_artwork.dart
klopez4212 d2da104d94 feat(mobile): add animated community switcher (#8076)
## Summary

Replace the community switch sheet with a two-column picker. Selection
enlarges the avatar, waits with the voice-note loader for destination
channel content and layout to settle, then follows a curved path into
the header.

Cache raster icons at a maximum 512-pixel size and accept only
resource-free emoji SVGs. Cancel timed-out or disposed downloads without
delaying content readiness. Animate Edit badges in place, and open Add
Community on the pairing welcome page. Community removal lives only in
the switcher’s Edit mode. Keep the flow shared across Android/iOS, with
native iOS controls and removal confirmation, and clear navigation
backgrounds at rest.

### Related issue

Related to #8070 (community icon refresh/caching).

### Testing

- 122 focused Flutter regression tests and seven native iOS tests passed
after merging the latest navigation changes from `main`. Earlier feature
build installed and launched on a physical iPhone.
- Full mobile pre-push checks passed; regression coverage includes
gradient rendering, rejected/oversized icons, bounded persisted raster
decoding, SVG resource rejection and cache migration, timeout/disposal
cancellation, disconnected destination channels, degraded unread-history
settlement, and switching while icons refresh. Full `just ci` stopped
during Rust checks for low disk space. Hosted CI remains required.
- [Grid and Edit
previews](https://github.com/block/buzz/pull/8076#issuecomment-5977838957).

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-04 14:11:39 +01:00

82 lines
2.8 KiB
Dart

import 'dart:convert';
import 'dart:ui' as ui;
import '../emoji/emoji_avatar.dart';
const _maximumSourceEdge = 4096;
const _maximumSourcePixels = 4 * 1024 * 1024;
const _maximumCachedEdge = 512;
const _maximumEncodedBytes = 256 * 1024;
// Only the product's fixed emoji template is safe to retain as SVG. Arbitrary
// SVG can reference external or embedded rasters that bypass the limits below.
// Accept current desktop artwork and its two legacy rounded backgrounds.
final _emojiSvg = RegExp(
r'^<svg xmlns="http://www\.w3\.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><rect width="512" height="512"(?: rx="(?:112|256)")? fill="#[0-9a-fA-F]{6}"/><text x="50%" y="56%" dominant-baseline="middle" text-anchor="middle" font-size="258">[^<>]{1,64}</text></svg>$',
);
/// Rejects pathological raster dimensions before decoding and persists only a
/// small first-frame PNG or a regenerated, resource-free emoji SVG.
Future<String?> prepareCommunityIconArtwork(String artwork) async {
try {
final data = UriData.parse(artwork);
final bytes = data.contentAsBytes();
if (bytes.length > _maximumEncodedBytes) return null;
if (data.mimeType == 'image/svg+xml') {
final svg = utf8.decode(bytes);
if (!_emojiSvg.hasMatch(svg)) return null;
final emoji = parseEmojiAvatarSvg(svg);
if (emoji == null) return null;
return emojiAvatarDataUrl(emoji.emoji, emoji.colorValue);
}
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
try {
final descriptor = await ui.ImageDescriptor.encoded(buffer);
try {
final width = descriptor.width;
final height = descriptor.height;
if (width <= 0 ||
height <= 0 ||
width > _maximumSourceEdge ||
height > _maximumSourceEdge ||
width * height > _maximumSourcePixels) {
return null;
}
final codec = await descriptor.instantiateCodec(
targetWidth: width >= height
? width.clamp(1, _maximumCachedEdge)
: null,
targetHeight: height > width
? height.clamp(1, _maximumCachedEdge)
: null,
);
try {
final frame = await codec.getNextFrame();
try {
final png = await frame.image.toByteData(
format: ui.ImageByteFormat.png,
);
if (png == null || png.lengthInBytes > _maximumEncodedBytes) {
return null;
}
return Uri.dataFromBytes(
png.buffer.asUint8List(),
mimeType: 'image/png',
).toString();
} finally {
frame.image.dispose();
}
} finally {
codec.dispose();
}
} finally {
descriptor.dispose();
}
} finally {
buffer.dispose();
}
} catch (_) {
return null;
}
}