diff --git a/lib/helpers/gif_helper.dart b/lib/helpers/gif_helper.dart index ce2b3f3..f97ee3a 100644 --- a/lib/helpers/gif_helper.dart +++ b/lib/helpers/gif_helper.dart @@ -34,8 +34,13 @@ class GifHelper { return pageMatch?.group(1); } - /// Encode a GIF in a format that parseGif() can parse. + /// Encode a GIF as a Giphy page URL, a format parseGif() also parses. + /// + /// A real URL rather than the lineage-only `g:` token: recipients on + /// stock (or any non-lineage) MeshCore client can open it, where `g:` + /// is opaque text to them. parseGif already matches this form, so Offband + /// keeps rendering inline and legacy `g:` payloads still decode. (#282) static String encodeGif(String gifId) { - return 'g:$gifId'; + return 'https://giphy.com/gifs/$gifId'; } } diff --git a/test/connector/gif_url_outbound_guard_test.dart b/test/connector/gif_url_outbound_guard_test.dart new file mode 100644 index 0000000..86c6fef --- /dev/null +++ b/test/connector/gif_url_outbound_guard_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/helpers/gif_helper.dart'; +import 'package:meshcore_open/helpers/smaz.dart'; +import 'package:meshcore_open/storage/prefs_manager.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +// #282: the GIF payload is now a URL. Outbound text prep skips Smaz/Cyr2Lat for +// "structured payloads" (it keyed on `g:`/`m:`/`V1|`). A transformed GIF URL +// would arrive as opaque `s:`-prefixed base64 on a stock client, which is worse +// than the raw text this change exists to fix. Pin that it survives intact. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const gifId = 'zaMiq1BvCdAVIiu3Mb'; + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + PrefsManager.reset(); + await PrefsManager.initialize(); + }); + + test('Smaz leaves the GIF URL intact on a Smaz-enabled channel', () async { + final connector = MeshCoreConnector(); + await connector.setChannelSmazEnabled(0, true); + + final payload = GifHelper.encodeGif(gifId); + final prepared = connector.prepareChannelOutboundText(0, payload); + + expect(prepared, payload); + expect(prepared.startsWith('s:'), isFalse); + expect(GifHelper.parseGif(prepared), gifId); + }); + + test('Smaz declines to compress a GIF URL at all', () { + // Why the outbound `g:`/`m:`/`V1|` structured-payload guard did not need + // widening for #282: encodeIfSmaller only swaps in the `s:`+base64 form + // when it is genuinely smaller, and base64 overhead exceeds any dictionary + // gain on a URL. Pinned so a future dictionary change cannot silently start + // compressing GIF URLs into something a stock client cannot read. + final payload = GifHelper.encodeGif(gifId); + expect(Smaz.encodeIfSmaller(payload), payload); + }); +} diff --git a/test/helpers/gif_url_encode_test.dart b/test/helpers/gif_url_encode_test.dart new file mode 100644 index 0000000..1502735 --- /dev/null +++ b/test/helpers/gif_url_encode_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/helpers/gif_helper.dart'; + +// #282: the picker sends a real Giphy URL instead of the lineage-only +// `g:` token, so stock / non-lineage MeshCore clients can retrieve the +// GIF instead of seeing opaque text. Offband keeps rendering inline because +// parseGif already matches this URL form. +void main() { + const gifId = 'zaMiq1BvCdAVIiu3Mb'; + + group('encodeGif emits a cross-client retrievable Giphy URL', () { + test('emits the giphy.com page URL, not g:', () { + expect(GifHelper.encodeGif(gifId), 'https://giphy.com/gifs/$gifId'); + }); + + test('what it emits round-trips back to the id through parseGif', () { + expect(GifHelper.parseGif(GifHelper.encodeGif(gifId)), gifId); + }); + }); + + group('back-compat: legacy payloads still render', () { + test('still parses a legacy g: payload', () { + expect(GifHelper.parseGif('g:$gifId'), gifId); + }); + + test('still parses a legacy g: reply payload', () { + expect(GifHelper.parseGif('@[Some Node] g:$gifId'), gifId); + }); + + test('parses a reply that carries the new URL form', () { + expect( + GifHelper.parseGif('@[Some Node] https://giphy.com/gifs/$gifId'), + gifId, + ); + }); + }); +}