From e6eb093cc63ba020799844905439f1cabfddcd3c Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Tue, 11 Aug 2026 01:30:57 +0200
Subject: [PATCH] fix: a store that cannot brick itself on its own history
---
lib/widgets/message_bubble.dart | 47 ++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 44 insertions(+), 3 deletions(-)
diff --git a/lib/widgets/message_bubble.dart b/lib/widgets/message_bubble.dart
index bbb3c63..66e6e61 100644
--- a/lib/widgets/message_bubble.dart
+++ b/lib/widgets/message_bubble.dart
@@ -468,8 +468,7 @@
// Other files: save to temp and share
final ext = _mimeToExt(mime);
final dir = await getTemporaryDirectory();
- final fileName =
- '${message.content.isNotEmpty ? message.content.replaceAll(RegExp(r'[^\w\s.-]'), '').trim() : 'file'}.$ext';
+ final fileName = _fileNameFor(message.content, ext);
final file = File('${dir.path}/$fileName');
await file.writeAsBytes(bytes);
@@ -483,6 +482,23 @@
).showSnackBar(SnackBar(content: Text('Could not open file: $e')));
}
}
+ }
+
+ /// The name to save an incoming file under.
+ ///
+ /// The caption is normally the sender's own filename, so it already carries
+ /// an extension. Appending another unconditionally produced `clip.mp4.mp4` —
+ /// and where the caption had been reduced to a trailing dot, `clip..bin`,
+ /// which no player would open until it was renamed by hand.
+ ///
+ /// So: sanitise, drop trailing dots, and add the extension only when the name
+ /// does not already end in it.
+ String _fileNameFor(String caption, String ext) {
+ var base = caption.replaceAll(RegExp(r'[^\w\s.-]'), '').trim();
+ base = base.replaceAll(RegExp(r'\.+$'), '').trim();
+ if (base.isEmpty) base = 'file';
+ if (base.toLowerCase().endsWith('.${ext.toLowerCase()}')) return base;
+ return '$base.$ext';
}
String _mimeToExt(String mime) {
@@ -503,8 +519,33 @@
'application/xml': 'xml',
'application/zip': 'zip',
'application/gzip': 'gz',
+ // Video and audio were absent, which is how an mp4 became a `.bin` even
+ // once the sender labelled it correctly. The hub's table is the source
+ // this mirrors.
+ 'video/mp4': 'mp4',
+ 'video/quicktime': 'mov',
+ 'video/x-msvideo': 'avi',
+ 'video/x-matroska': 'mkv',
+ 'audio/mpeg': 'mp3',
+ 'audio/wav': 'wav',
+ 'audio/ogg': 'ogg',
+ 'audio/mp4': 'm4a',
+ 'image/jpeg': 'jpg',
+ 'image/png': 'png',
+ 'image/gif': 'gif',
+ 'image/webp': 'webp',
+ 'image/svg+xml': 'svg',
};
- return map[mime] ?? 'bin';
+ final normalised = mime.toLowerCase().split(';').first.trim();
+ final known = map[normalised];
+ if (known != null) return known;
+ // A type this table does not carry but whose subtype names itself:
+ // image/heic -> heic. Better than calling it `bin`.
+ final subtype = normalised.split('/').length > 1
+ ? normalised.split('/')[1]
+ : '';
+ if (RegExp(r'^[a-z0-9]{2,5}$').hasMatch(subtype)) return subtype;
+ return 'bin';
}
Widget _buildFooter(BuildContext context) {
--
Gitblit v1.3.1