diff --git a/lib/helpers/gif_helper.dart b/lib/helpers/gif_helper.dart new file mode 100644 index 0000000..a223ffc --- /dev/null +++ b/lib/helpers/gif_helper.dart @@ -0,0 +1,33 @@ +class GifHelper { + /// Parse a known GIF format, which can be any of: + /// g:GIFID + /// https://media.giphy.com/media/GIFID/giphy.gif + /// https://giphy.com/gifs/Optional-title-with-dashes-GIFID + /// + /// GIFID is a Giphy GIF ID. The https:// is optional (and + /// can also be http://). The giphy.com/gifs form can also + /// include a trailing slash. + /// + /// Returns null if text is not a valid GIF format + static String? parseGifId(String text) { + final trimmed = text.trim(); + final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed); + if (match != null) { + return match.group(1); + } + final directUrlMatch = RegExp( + r'^(?:https?:\/\/)?media\.giphy\.com\/media\/([A-Za-z0-9_-]+)\/giphy\.gif$', + ).firstMatch(trimmed); + if (directUrlMatch != null) { + return directUrlMatch.group(1); + } + // Giphy understands page URLs with just the ID, or any string and a + // dash before the ID, and redirects to a page with a dash-separated + // title, a dash, and the ID. IDs in this form *probably* can't + // contain dashes. + final pageMatch = RegExp( + r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$', + ).firstMatch(trimmed); + return pageMatch?.group(1); + } +} diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 628ae1c..131d74c 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -11,6 +11,7 @@ import '../connector/meshcore_connector.dart'; import '../utils/platform_info.dart'; import '../helpers/chat_scroll_controller.dart'; import '../connector/meshcore_protocol.dart'; +import '../helpers/gif_helper.dart'; import '../helpers/reaction_helper.dart'; import '../helpers/utf8_length_limiter.dart'; import '../l10n/l10n.dart'; @@ -355,7 +356,7 @@ class _ChannelChatScreenState extends State { final settingsService = context.watch(); final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; - final gifId = _parseGifId(message.text); + final gifId = GifHelper.parseGifId(message.text); final poi = _parsePoiMessage(message.text); final translatedDisplayText = message.translatedText != null && @@ -699,7 +700,7 @@ class _ChannelChatScreenState extends State { final colorScheme = Theme.of(context).colorScheme; final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7); - final gifId = _parseGifId(replyText); + final gifId = GifHelper.parseGifId(replyText); final poi = _parsePoiMessage(replyText); Widget contentPreview; @@ -811,12 +812,6 @@ class _ChannelChatScreenState extends State { ); } - String? _parseGifId(String text) { - final trimmed = text.trim(); - final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed); - return match?.group(1); - } - _PoiInfo? _parsePoiMessage(String text) { final trimmed = text.trim(); final match = RegExp( @@ -1053,7 +1048,7 @@ class _ChannelChatScreenState extends State { child: ValueListenableBuilder( valueListenable: _textController, builder: (context, value, child) { - final gifId = _parseGifId(value.text); + final gifId = GifHelper.parseGifId(value.text); if (gifId != null) { return Focus( autofocus: true, diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 398e1b5..daba56b 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -16,6 +16,7 @@ import '../connector/meshcore_protocol.dart'; import '../helpers/reaction_helper.dart'; import '../widgets/message_status_icon.dart'; import '../helpers/chat_scroll_controller.dart'; +import '../helpers/gif_helper.dart'; import '../helpers/path_helper.dart'; import '../helpers/utf8_length_limiter.dart'; import '../models/channel_message.dart'; @@ -523,7 +524,7 @@ class _ChatScreenState extends State { child: ValueListenableBuilder( valueListenable: _textController, builder: (context, value, child) { - final gifId = _parseGifId(value.text); + final gifId = GifHelper.parseGifId(value.text); if (gifId != null) { return Focus( autofocus: true, @@ -598,28 +599,6 @@ class _ChatScreenState extends State { ); } - String? _parseGifId(String text) { - final trimmed = text.trim(); - final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed); - if (match != null) { - return match.group(1); - } - final directUrlMatch = RegExp( - r'^(?:https?:\/\/)?media\.giphy\.com\/media\/([A-Za-z0-9_-]+)\/giphy\.gif$', - ).firstMatch(trimmed); - if (directUrlMatch != null) { - return directUrlMatch.group(1); - } - // Giphy understands page URLs with just the ID, or any string and a - // dash before the ID, and redirects to a page with a dash-separated - // title, a dash, and the ID. IDs in this form *probably* can't - // contain dashes. - final pageMatch = RegExp( - r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$', - ).firstMatch(trimmed); - return pageMatch?.group(1); - } - void _showGifPicker(BuildContext context) { showModalBottomSheet( context: context, @@ -1589,7 +1568,7 @@ class _MessageBubble extends StatelessWidget { final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final colorScheme = Theme.of(context).colorScheme; - final gifId = _parseGifId(message.text); + final gifId = GifHelper.parseGifId(message.text); final poi = _parsePoiMessage(message.text); final isFailed = message.status == MessageStatus.failed; final bubbleColor = isFailed @@ -1863,12 +1842,6 @@ class _MessageBubble extends StatelessWidget { ); } - String? _parseGifId(String text) { - final trimmed = text.trim(); - final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed); - return match?.group(1); - } - _PoiInfo? _parsePoiMessage(String text) { final trimmed = text.trim(); final match = RegExp(