fix: Settings crash, photo preview before sending, incoming-content hardening
Settings crashed with a TypeError: the profile card passed a SepotFingerprint object where the localized string formatter expects a String (hidden behind a `dynamic` parameter). Also fixes RangeError in avatar initials (empty names, double spaces) and switches initials to grapheme clusters so emoji don't get split. New photo preview screen: a captured or picked photo no longer sends immediately — it opens in a preview with freehand drawing (7 colours, undo), draggable text overlay, 90° rotation and a caption field. Nothing leaves the device until Send is tapped. Incoming-content hardening: decryption and content processing are now separated in the delivery path. Once an envelope is decrypted the ratchet has advanced, so redelivery can never help — permanently malformed content (FormatException/TypeError) is acked and dropped instead of poisoning the queue on every reconnect. Attachment metadata now validates base64 of the crypto fields up front, and a corrupt inline thumbnail no longer breaks bubble rendering. PNG without real transparency (screenshots, edited photos) is now re-encoded as JPEG — several times smaller on the wire. Tests: 116 (new widget tests for Settings and the photo preview, AttachmentMeta wire validation).
This commit is contained in:
@@ -72,9 +72,11 @@ class ImageProcessor {
|
||||
: img.copyResize(image, height: maxDimension);
|
||||
}
|
||||
|
||||
// PNG s průhledností zůstává PNG (JPEG alfa kanál neumí, a re-encode
|
||||
// PNG→PNG metadata stejně zahodí); vše ostatní JPEG q80.
|
||||
final keepPng = mimeHint == 'image/png' && decoded.numChannels == 4;
|
||||
// PNG s REÁLNOU průhledností zůstává PNG (JPEG alfa kanál neumí,
|
||||
// a re-encode PNG→PNG metadata stejně zahodí); vše ostatní JPEG q80.
|
||||
// Samotný alfa kanál nestačí — plně krytý RGBA PNG (screenshot,
|
||||
// canvas render z editoru fotek) je jako JPEG několikanásobně menší.
|
||||
final keepPng = mimeHint == 'image/png' && _hasTransparency(decoded);
|
||||
final bytes = keepPng
|
||||
? Uint8List.fromList(img.encodePng(image))
|
||||
: Uint8List.fromList(img.encodeJpg(image, quality: jpegQuality));
|
||||
@@ -89,6 +91,16 @@ class ImageProcessor {
|
||||
});
|
||||
}
|
||||
|
||||
/// Má obraz aspoň jeden ne-plně-krytý pixel? Běží v izolátu spolu se
|
||||
/// zbytkem zpracování, O(n) sken je proti dekódu/enkódu zanedbatelný.
|
||||
static bool _hasTransparency(img.Image image) {
|
||||
if (image.numChannels < 4) return false;
|
||||
for (final pixel in image) {
|
||||
if (pixel.a < pixel.maxChannelValue) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Uint8List _thumbnail(img.Image source) {
|
||||
var thumb = max(source.width, source.height) > thumbDimension
|
||||
? (source.width >= source.height
|
||||
|
||||
@@ -1045,13 +1045,24 @@ class MessagingService {
|
||||
final trustRoot = _trustRoot;
|
||||
if (crypto == null || trustRoot == null) return;
|
||||
|
||||
final SepotSealedMessage opened;
|
||||
try {
|
||||
final opened = await crypto.sealedDecrypt(
|
||||
opened = await crypto.sealedDecrypt(
|
||||
ciphertext: Uint8List.fromList(envelope.payload),
|
||||
trustRoot: trustRoot,
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
} catch (_) {
|
||||
// Nedešifrovatelná/cizí obálka: NEACKovat, ať ji fronta podrží
|
||||
// (může jít o dočasný stav — např. cert refresh).
|
||||
return;
|
||||
}
|
||||
|
||||
// Od tohohle bodu ratchet postoupil — obálku na relayi už NIKDY znovu
|
||||
// nedešifrujeme (duplicitní decrypt libsignal odmítne). Vadný OBSAH se
|
||||
// proto ACKuje a zahazuje: redelivery nemůže nic spravit, jen by
|
||||
// trvale otravovala frontu při každém reconnectu.
|
||||
try {
|
||||
final content =
|
||||
jsonDecode(utf8.decode(opened.plaintext)) as Map<String, dynamic>;
|
||||
final type = content['t'] as String?;
|
||||
@@ -1059,7 +1070,12 @@ class MessagingService {
|
||||
'txt', 'att', 'rx', 'edit', 'del', 'dlv', 'rr', // 1:1 + skupiny
|
||||
'ginv', 'gleave', 'gmeta', // správa skupin
|
||||
};
|
||||
if (content['v'] != 1 || !known.contains(type)) return;
|
||||
if (content['v'] != 1 || !known.contains(type)) {
|
||||
// Neznámý typ/verze: obsah je pro tuhle verzi navždy nečitelný
|
||||
// a po upgradu ho stejně nejde znovu dešifrovat — ACK a zahodit.
|
||||
_relay?.ack(envelope.msgUuid);
|
||||
return;
|
||||
}
|
||||
|
||||
final contact = await _findOrCreateContact(opened, content);
|
||||
final senderFp = contact.id;
|
||||
@@ -1242,9 +1258,18 @@ class MessagingService {
|
||||
if (message.attachment != null) {
|
||||
unawaited(downloadAttachment(convId, message));
|
||||
}
|
||||
} on FormatException {
|
||||
// Malformovaný obsah (ne-UTF-8/ne-JSON plaintext, vadná base64
|
||||
// v profilu či rosteru) — trvale vadné, ACK a zahodit.
|
||||
_relay?.ack(envelope.msgUuid);
|
||||
} on TypeError {
|
||||
// Pole se špatným typem (txt/ttl/id/re/p jako číslo apod.) —
|
||||
// trvale vadné, ACK a zahodit.
|
||||
_relay?.ack(envelope.msgUuid);
|
||||
} catch (_) {
|
||||
// Nedešifrovatelná/cizí obálka: NEACKovat, ať ji fronta podrží
|
||||
// (může jít o dočasný stav — např. cert refresh)
|
||||
// Přechodné selhání zpracování (storage apod.): NEACKovat — další
|
||||
// pokus po reconnectu za nic nedá, i když duplicitní decrypt
|
||||
// pravděpodobně selže. Trvalé chyby obsahu řeší větve výše.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user