/// Parsed page of Talk's `talk-message-current` search provider. class SearchChatMessagesResponse { final List entries; /// Opaque pagination cursor; `null` when the provider has no further page. final Object? cursor; const SearchChatMessagesResponse({required this.entries, this.cursor}); factory SearchChatMessagesResponse.fromJson( Map json, { required String term, }) { final rawEntries = json['entries']; final entries = rawEntries is List ? rawEntries .whereType>() .map((e) => SearchChatMessagesEntry.fromJson(e, term: term)) .whereType() .toList() : const []; final paginated = json['isPaginated'] == true; final cursor = json['cursor']; return SearchChatMessagesResponse( entries: entries, cursor: paginated && entries.isNotEmpty && cursor != null ? cursor : null, ); } } class SearchChatMessagesEntry { final int messageId; final String? actorId; final String? actorDisplayName; /// Server-built excerpt of the message, placeholders already resolved. final String excerpt; final DateTime? sentAt; const SearchChatMessagesEntry({ required this.messageId, required this.excerpt, this.actorId, this.actorDisplayName, this.sentAt, }); /// Returns `null` for entries without a recognisable message id. static SearchChatMessagesEntry? fromJson( Map json, { required String term, }) { final attributes = json['attributes']; final attrs = attributes is Map ? attributes : const {}; final messageId = _parseInt(attrs['messageId']) ?? _parseInt( RegExp( r'#message_(\d+)', ).firstMatch(json['resourceUrl']?.toString() ?? '')?.group(1), ); if (messageId == null) return null; // Title and subline have swapped roles across Talk versions (author vs. // excerpt); the one containing the search term is the excerpt. final title = json['title']?.toString() ?? ''; final subline = json['subline']?.toString() ?? ''; final q = term.trim().toLowerCase(); final titleIsExcerpt = q.isNotEmpty && title.toLowerCase().contains(q) && !subline.toLowerCase().contains(q); final excerpt = titleIsExcerpt ? title : subline; final author = titleIsExcerpt ? subline : title; final timestamp = _parseInt(attrs['timestamp']); final actorId = attrs['actorId']?.toString(); return SearchChatMessagesEntry( messageId: messageId, excerpt: excerpt, actorId: actorId == null || actorId.isEmpty ? null : actorId, actorDisplayName: author.isEmpty ? null : author, sentAt: timestamp == null ? null : DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), ); } static int? _parseInt(Object? raw) => switch (raw) { final int v => v, final String v => int.tryParse(v), _ => null, }; }