import 'dart:convert'; import '../../models/attachment.dart'; import '../../models/chat_preview_media.dart'; const int _maxPreviewThumbs = 3; const int _maxThumbLength = 20000; String? attachPreviewLabel(dynamic attaches) { final parts = _attachPreviewParts(attaches); if (parts == null) return null; final detail = parts.detail; return detail == null ? parts.label : '${parts.label}: $detail'; } ({String label, String? detail})? _attachPreviewParts(dynamic attaches) { final first = _firstPreviewAttach(attaches); if (first == null) return null; final type = (first['_type'] as String? ?? '').toUpperCase(); switch (type) { case 'PHOTO': return ( label: _mediaAttachCount(attaches) > 1 ? 'Изображения' : 'Изображение', detail: null, ); case 'VIDEO': if (_isVideoNote(first)) return (label: 'Видео-сообщение', detail: null); return (label: 'Видео', detail: null); case 'AUDIO': return (label: 'Голосовое сообщение', detail: null); case 'FILE': return (label: 'Файл', detail: _nonEmpty(first['name'])); case 'STICKER': return (label: 'Стикер', detail: null); case 'SHARE': return (label: 'Ссылка', detail: _nonEmpty(first['title'])); case 'POLL': return (label: 'Опрос', detail: _nonEmpty(first['title'])); case 'LOCATION': return (label: 'Геопозиция', detail: null); case 'CONTACT': return (label: 'Контакт', detail: null); case 'CONTROL': final label = _controlPreviewLabel(first); return label == null ? null : (label: label, detail: null); case 'INLINE_KEYBOARD': return null; case 'CALL': return (label: _callPreviewLabel(first), detail: null); default: return (label: 'Вложение', detail: null); } } ChatPreviewKind? _attachPreviewKind(Map attach) { switch ((attach['_type'] as String? ?? '').toUpperCase()) { case 'PHOTO': return ChatPreviewKind.photo; case 'VIDEO': return _isVideoNote(attach) ? ChatPreviewKind.videoNote : ChatPreviewKind.video; case 'AUDIO': return ChatPreviewKind.audio; case 'FILE': return ChatPreviewKind.file; case 'STICKER': return ChatPreviewKind.sticker; case 'SHARE': return ChatPreviewKind.share; case 'POLL': return ChatPreviewKind.poll; case 'LOCATION': return ChatPreviewKind.location; case 'CONTACT': return ChatPreviewKind.contact; case 'CONTROL': return ChatPreviewKind.control; case 'INLINE_KEYBOARD': return null; case 'CALL': final video = attach['callType']?.toString().toUpperCase() == 'VIDEO'; if (_isFailedCall(attach)) { return video ? ChatPreviewKind.missedVideoCall : ChatPreviewKind.missedCall; } return video ? ChatPreviewKind.videoCall : ChatPreviewKind.call; default: return ChatPreviewKind.other; } } String _callPreviewLabel(Map attach) { final video = attach['callType']?.toString().toUpperCase() == 'VIDEO'; if (attach['joinLink'] != null) { return video ? 'Групповой видеозвонок' : 'Групповой звонок'; } if (_isFailedCall(attach)) { return video ? 'Пропущенный видеозвонок' : 'Пропущенный звонок'; } return video ? 'Видеозвонок' : 'Звонок'; } bool _isFailedCall(Map attach) { final duration = (attach['duration'] as num?)?.toInt() ?? 0; final hangup = attach['hangupType']?.toString(); return duration == 0 || hangup == 'CANCELED' || hangup == 'REJECTED' || hangup == 'MISSED'; } String? _nonEmpty(dynamic raw) { final value = raw?.toString(); return value != null && value.isNotEmpty ? value : null; } Map? _firstPreviewAttach(dynamic attaches) { if (attaches is! List || attaches.isEmpty) return null; for (final attach in attaches) { if (attach is! Map) continue; final type = (attach['_type'] as String? ?? '').toUpperCase(); if (type == 'INLINE_KEYBOARD') continue; return attach; } return null; } int _mediaAttachCount(dynamic attaches) { if (attaches is! List) return 0; var count = 0; for (final attach in attaches.whereType()) { final type = (attach['_type'] as String? ?? '').toUpperCase(); if (type == 'PHOTO' || (type == 'VIDEO' && !_isVideoNote(attach))) count++; } return count; } bool _isVideoNote(Map attach) { final raw = attach['videoType']; if (raw is int) return raw == 1; return raw?.toString() == '1'; } String? _controlPreviewLabel(Map c) { final title = c['title']?.toString(); if (title != null && title.isNotEmpty) return title; final short = c['shortMessage']?.toString(); if (short != null && short.isNotEmpty) return short; switch (c['event']?.toString()) { case 'new': return 'Чат создан'; case 'add': case 'joinByLink': return 'Новый участник'; case 'leave': return 'Участник вышел'; case 'remove': return 'Участник удалён'; case 'pin': return 'Закреплённое сообщение'; case 'changeTitle': return 'Название чата изменено'; case 'changeIcon': return 'Фото чата обновлено'; default: return 'Системное сообщение'; } } String? messagePreviewText(Map msg) { final original = _forwardOrigin(msg); if (original != null) { final inner = original is Map ? _bodyPreviewText(original) : null; return inner != null && inner.isNotEmpty ? '↪ $inner' : '↪ Пересланное сообщение'; } return _bodyPreviewText(msg); } String? messagePreviewMedia(Map msg) { final origin = _forwardOrigin(msg); final body = origin ?? msg; if (body is! Map) return null; final first = _firstPreviewAttach(body['attaches']); if (first == null) return null; final kind = _attachPreviewKind(first); if (kind == null) return null; final text = body['text']?.toString(); final captioned = text != null && text.isNotEmpty; final parts = captioned ? null : _attachPreviewParts(body['attaches']); final label = parts == null ? null : (origin == null ? parts.label : '↪ ${parts.label}'); return ChatPreviewMedia( kind: kind, thumbs: _previewThumbs(body['attaches']), label: label, detail: parts?.detail, ).encode(); } dynamic _forwardOrigin(Map msg) { final link = msg['link']; if (link is! Map) return null; if (link['type']?.toString().toUpperCase() != 'FORWARD') return null; return link['message']; } List _previewThumbs(dynamic attaches) { if (attaches is! List) return const []; final thumbs = []; for (final attach in attaches.whereType()) { if (thumbs.length >= _maxPreviewThumbs) break; final type = (attach['_type'] as String? ?? '').toUpperCase(); final isVideo = type == 'VIDEO'; if (type != 'PHOTO' && !isVideo) continue; final source = _thumbSource(attach, isVideo); if (source == null) continue; thumbs.add(ChatPreviewThumb(source: source, video: isVideo)); } return thumbs; } String? _thumbSource(Map attach, bool isVideo) { final data = decodeAttachPreview(attach['previewData']); if (data != null && data.length <= _maxThumbLength) return data; final url = isVideo ? _nonEmpty(attach['thumbnail']) : _nonEmpty(attach['baseUrl']); if (url != null && url.startsWith('http')) return url; return null; } ({String? text, bool isPreview}) pinnedMessagePreview(Map msg) { final link = msg['link']; if (link is Map && link['type']?.toString().toUpperCase() == 'FORWARD') { final original = link['message']; if (original is Map) { final inner = pinnedMessagePreview(original); return inner.text != null && inner.text!.isNotEmpty ? (text: '↪ ${inner.text}', isPreview: inner.isPreview) : (text: '↪ пересланное сообщение', isPreview: true); } return (text: '↪ пересланное сообщение', isPreview: true); } return _pinnedBodyPreview(msg); } ({String? text, bool isPreview}) _pinnedBodyPreview(Map msg) { final text = msg['text']?.toString(); if (text != null && text.isNotEmpty) return (text: text, isPreview: false); final label = _pinnedAttachPreviewLabel(msg['attaches']); return (text: label, isPreview: label != null); } String? _pinnedAttachPreviewLabel(dynamic attaches) { final first = _firstPreviewAttach(attaches); if (first == null) return null; final type = (first['_type'] as String? ?? '').toUpperCase(); switch (type) { case 'PHOTO': return 'фото'; case 'VIDEO': return _isVideoNote(first) ? 'кружок' : 'видео'; case 'AUDIO': return 'голосовое сообщение'; case 'FILE': return 'файл'; case 'STICKER': return 'стикер'; case 'SHARE': return 'ссылка'; case 'POLL': return 'голосование'; case 'LOCATION': return 'геопозиция'; case 'CONTACT': return 'контакт'; case 'CALL': return 'звонок'; case 'CONTROL': return _controlPreviewLabel(first)?.toLowerCase(); default: return 'вложение'; } } String? _bodyPreviewText(Map msg) { final text = msg['text']?.toString(); if (text != null && text.isNotEmpty) return text; return attachPreviewLabel(msg['attaches']); } String? messagePreviewElements(Map msg) { final text = msg['text']; if (text is! String || text.isEmpty) return null; final elements = msg['elements']; if (elements is List && elements.isNotEmpty) return jsonEncode(elements); return null; }