class SignalSharePayload { const SignalSharePayload({ required this.messageText, this.sourceDisplayName, this.sourceUserId, this.sourceThreadId, }); final String messageText; final String? sourceDisplayName; final String? sourceUserId; final String? sourceThreadId; } /// Parses raw shared text from Signal into normalized sender/message fields. class SignalShareParser { const SignalShareParser._(); static final RegExp _namePrefixPattern = RegExp( r'^([^:\n]{2,80}):\s*(.+)$', dotAll: true, ); static SignalSharePayload parse(String raw) { final String input = raw.trim(); if (input.isEmpty) { return const SignalSharePayload(messageText: ''); } final RegExpMatch? match = _namePrefixPattern.firstMatch(input); if (match == null) { return SignalSharePayload(messageText: input); } final String? senderName = _cleanSender(match.group(1)); final String messageText = match.group(2)?.trim() ?? input; return SignalSharePayload( messageText: messageText, sourceDisplayName: senderName, sourceUserId: _normalizedKey(senderName), sourceThreadId: null, ); } static bool looksLikeSignalMessage(String raw) { final String input = raw.trim(); if (input.isEmpty) { return false; } return _namePrefixPattern.hasMatch(input); } static String? _cleanSender(String? input) { if (input == null) { return null; } final String cleaned = input.trim(); return cleaned.isEmpty ? null : cleaned; } static String? _normalizedKey(String? input) { if (input == null) { return null; } final String key = input .toLowerCase() .replaceAll(RegExp(r'[^a-z0-9]+'), ' ') .trim() .replaceAll(RegExp(r'\s+'), ' '); return key.isEmpty ? null : 'signal:$key'; } }