feat/fix: иконки у каналов/групп/ботов, + миниатюры и иконки к видам медиа на экране чатов
This commit is contained in:
@@ -58,6 +58,7 @@ CachedChat? parseChatRow(
|
||||
lastMsgTime: lastMessage.time,
|
||||
lastMsgText: lastMessage.text,
|
||||
lastMsgElements: lastMessage.elements,
|
||||
lastMsgPreview: lastMessage.preview,
|
||||
lastMsgSenderId: lastMessage.senderId,
|
||||
unreadCount: (chat['newMessages'] as int?) ?? 0,
|
||||
lastEventTime: (chat['lastEventTime'] as int?) ?? 0,
|
||||
@@ -125,16 +126,31 @@ CachedChat? parseChatRow(
|
||||
);
|
||||
}
|
||||
|
||||
({int? id, int? time, String? text, String? elements, int? senderId})
|
||||
({
|
||||
int? id,
|
||||
int? time,
|
||||
String? text,
|
||||
String? elements,
|
||||
String? preview,
|
||||
int? senderId,
|
||||
})
|
||||
_resolveLastMessage(dynamic lastMsg) {
|
||||
if (lastMsg is! Map) {
|
||||
return (id: null, time: null, text: null, elements: null, senderId: null);
|
||||
return (
|
||||
id: null,
|
||||
time: null,
|
||||
text: null,
|
||||
elements: null,
|
||||
preview: null,
|
||||
senderId: null,
|
||||
);
|
||||
}
|
||||
return (
|
||||
id: lastMsg['id'] as int?,
|
||||
time: lastMsg['time'] as int?,
|
||||
text: messagePreviewText(lastMsg),
|
||||
elements: messagePreviewElements(lastMsg),
|
||||
preview: messagePreviewMedia(lastMsg),
|
||||
senderId: lastMsg['sender'] as int?,
|
||||
);
|
||||
}
|
||||
@@ -308,6 +324,7 @@ bool sameChatContent(CachedChat a, CachedChat b) {
|
||||
if (a.lastMsgTime != b.lastMsgTime) return false;
|
||||
if (a.lastMsgText != b.lastMsgText) return false;
|
||||
if (a.lastMsgElements != b.lastMsgElements) return false;
|
||||
if (a.lastMsgPreview != b.lastMsgPreview) return false;
|
||||
if (a.lastMsgSenderId != b.lastMsgSenderId) return false;
|
||||
if (a.unreadCount != b.unreadCount) return false;
|
||||
if (a.lastEventTime != b.lastEventTime) return false;
|
||||
|
||||
@@ -1,56 +1,121 @@
|
||||
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 'Фото';
|
||||
return (
|
||||
label: _mediaAttachCount(attaches) > 1 ? 'Изображения' : 'Изображение',
|
||||
detail: null,
|
||||
);
|
||||
case 'VIDEO':
|
||||
return _isVideoNote(first) ? 'Видео-сообщение' : 'Видео';
|
||||
if (_isVideoNote(first)) return (label: 'Видео-сообщение', detail: null);
|
||||
return (label: 'Видео', detail: null);
|
||||
case 'AUDIO':
|
||||
return 'Голосовое сообщение';
|
||||
return (label: 'Голосовое сообщение', detail: null);
|
||||
case 'FILE':
|
||||
final name = first['name']?.toString();
|
||||
return name != null && name.isNotEmpty ? 'Файл: $name' : 'Файл';
|
||||
return (label: 'Файл', detail: _nonEmpty(first['name']));
|
||||
case 'STICKER':
|
||||
return 'Стикер';
|
||||
return (label: 'Стикер', detail: null);
|
||||
case 'SHARE':
|
||||
final title = first['title']?.toString();
|
||||
return title != null && title.isNotEmpty ? 'Ссылка: $title' : 'Ссылка';
|
||||
return (label: 'Ссылка', detail: _nonEmpty(first['title']));
|
||||
case 'POLL':
|
||||
final title = first['title']?.toString();
|
||||
return title != null && title.isNotEmpty ? 'Опрос: $title' : 'Опрос';
|
||||
return (label: 'Опрос', detail: _nonEmpty(first['title']));
|
||||
case 'LOCATION':
|
||||
return 'Геопозиция';
|
||||
return (label: 'Геопозиция', detail: null);
|
||||
case 'CONTACT':
|
||||
return 'Контакт';
|
||||
return (label: 'Контакт', detail: null);
|
||||
case 'CONTROL':
|
||||
return _controlPreviewLabel(first);
|
||||
final label = _controlPreviewLabel(first);
|
||||
return label == null ? null : (label: label, detail: null);
|
||||
case 'INLINE_KEYBOARD':
|
||||
return null;
|
||||
case 'CALL':
|
||||
final video = first['callType']?.toString().toUpperCase() == 'VIDEO';
|
||||
final dur = (first['duration'] as num?)?.toInt() ?? 0;
|
||||
final hangup = first['hangupType']?.toString();
|
||||
final failed =
|
||||
dur == 0 ||
|
||||
hangup == 'CANCELED' ||
|
||||
hangup == 'REJECTED' ||
|
||||
hangup == 'MISSED';
|
||||
if (first['joinLink'] != null) {
|
||||
return video ? 'Групповой видеозвонок' : 'Групповой звонок';
|
||||
}
|
||||
if (failed) {
|
||||
return video ? 'Пропущенный видеозвонок' : 'Пропущенный звонок';
|
||||
}
|
||||
return video ? 'Видеозвонок' : 'Звонок';
|
||||
return (label: _callPreviewLabel(first), detail: null);
|
||||
default:
|
||||
return 'Вложение';
|
||||
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) {
|
||||
@@ -62,6 +127,16 @@ Map? _firstPreviewAttach(dynamic attaches) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int _mediaAttachCount(dynamic attaches) {
|
||||
if (attaches is! List) return 0;
|
||||
var count = 0;
|
||||
for (final attach in attaches.whereType<Map>()) {
|
||||
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;
|
||||
@@ -95,9 +170,8 @@ String? _controlPreviewLabel(Map c) {
|
||||
}
|
||||
|
||||
String? messagePreviewText(Map msg) {
|
||||
final link = msg['link'];
|
||||
if (link is Map && link['type']?.toString().toUpperCase() == 'FORWARD') {
|
||||
final original = link['message'];
|
||||
final original = _forwardOrigin(msg);
|
||||
if (original != null) {
|
||||
final inner = original is Map ? _bodyPreviewText(original) : null;
|
||||
return inner != null && inner.isNotEmpty
|
||||
? '↪ $inner'
|
||||
@@ -106,6 +180,62 @@ String? messagePreviewText(Map msg) {
|
||||
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<ChatPreviewThumb> _previewThumbs(dynamic attaches) {
|
||||
if (attaches is! List) return const [];
|
||||
final thumbs = <ChatPreviewThumb>[];
|
||||
for (final attach in attaches.whereType<Map>()) {
|
||||
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') {
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../../core/storage/chat_members_store.dart';
|
||||
import '../../core/storage/token_storage.dart';
|
||||
import '../../core/utils/logger.dart';
|
||||
import '../../core/utils/text_format.dart';
|
||||
import '../../models/chat_preview_media.dart';
|
||||
import '../../models/contact_info.dart';
|
||||
import '../api.dart';
|
||||
import 'chat_parsing.dart';
|
||||
@@ -63,6 +64,7 @@ class CachedChat {
|
||||
final String? lastMsgText;
|
||||
final String? lastMsgTextOneLine;
|
||||
final String? lastMsgElements;
|
||||
final String? lastMsgPreview;
|
||||
final int? lastMsgSenderId;
|
||||
final String? lastMsgStatus;
|
||||
final int unreadCount;
|
||||
@@ -92,6 +94,7 @@ class CachedChat {
|
||||
this.lastMsgTime,
|
||||
this.lastMsgText,
|
||||
this.lastMsgElements,
|
||||
this.lastMsgPreview,
|
||||
this.lastMsgSenderId,
|
||||
this.lastMsgStatus,
|
||||
required this.unreadCount,
|
||||
@@ -116,6 +119,10 @@ class CachedChat {
|
||||
|
||||
bool get isOfficial => options.contains('OFFICIAL');
|
||||
|
||||
late final ChatPreviewMedia? lastMsgMedia = ChatPreviewMedia.decode(
|
||||
lastMsgPreview,
|
||||
);
|
||||
|
||||
List<FormatRange> get lastMsgFormatRanges {
|
||||
final raw = lastMsgElements;
|
||||
if (raw == null || raw.isEmpty) return const [];
|
||||
@@ -176,6 +183,7 @@ class CachedChat {
|
||||
lastMsgTime: row['last_msg_time'] as int?,
|
||||
lastMsgText: row['last_msg_text'] as String?,
|
||||
lastMsgElements: row['last_msg_elements'] as String?,
|
||||
lastMsgPreview: row['last_msg_preview'] as String?,
|
||||
lastMsgSenderId: row['last_msg_sender'] as int?,
|
||||
lastMsgStatus: row['last_msg_status'] as String?,
|
||||
unreadCount: row['unread_count'] as int,
|
||||
@@ -220,6 +228,7 @@ class CachedChat {
|
||||
'last_msg_time': lastMsgTime,
|
||||
'last_msg_text': lastMsgText,
|
||||
'last_msg_elements': lastMsgElements,
|
||||
'last_msg_preview': lastMsgPreview,
|
||||
'last_msg_sender': lastMsgSenderId,
|
||||
'last_msg_status': lastMsgStatus,
|
||||
'unread_count': unreadCount,
|
||||
@@ -252,6 +261,7 @@ class CachedChat {
|
||||
Object? lastMsgTime = _keep,
|
||||
Object? lastMsgText = _keep,
|
||||
Object? lastMsgElements = _keep,
|
||||
Object? lastMsgPreview = _keep,
|
||||
Object? lastMsgSenderId = _keep,
|
||||
Object? lastMsgStatus = _keep,
|
||||
int? unreadCount,
|
||||
@@ -289,6 +299,9 @@ class CachedChat {
|
||||
lastMsgElements: identical(lastMsgElements, _keep)
|
||||
? this.lastMsgElements
|
||||
: lastMsgElements as String?,
|
||||
lastMsgPreview: identical(lastMsgPreview, _keep)
|
||||
? this.lastMsgPreview
|
||||
: lastMsgPreview as String?,
|
||||
lastMsgSenderId: identical(lastMsgSenderId, _keep)
|
||||
? this.lastMsgSenderId
|
||||
: lastMsgSenderId as int?,
|
||||
@@ -547,6 +560,7 @@ class ChatsModule {
|
||||
required String text,
|
||||
required String status,
|
||||
List<Map<String, dynamic>>? elements,
|
||||
String? preview,
|
||||
}) async {
|
||||
final thisId = int.tryParse(messageId);
|
||||
await _updateChat(accountId, chatId, (chat) {
|
||||
@@ -558,6 +572,7 @@ class ChatsModule {
|
||||
lastMsgElements: (elements != null && elements.isNotEmpty)
|
||||
? jsonEncode(elements)
|
||||
: null,
|
||||
lastMsgPreview: preview,
|
||||
lastMsgTime: time,
|
||||
lastEventTime: time,
|
||||
lastMsgSenderId: accountId,
|
||||
@@ -891,6 +906,7 @@ class ChatsModule {
|
||||
}
|
||||
newRow['last_msg_text'] = messagePreviewText(msg);
|
||||
newRow['last_msg_elements'] = messagePreviewElements(msg);
|
||||
newRow['last_msg_preview'] = messagePreviewMedia(msg);
|
||||
if (senderId != null) newRow['last_msg_sender'] = senderId;
|
||||
newRow['last_msg_status'] = 'sent';
|
||||
}
|
||||
@@ -960,7 +976,9 @@ class ChatsModule {
|
||||
final rawText = m['text']?.toString();
|
||||
String? previewText = rawText;
|
||||
String? elementsJson;
|
||||
String? previewMedia;
|
||||
final payload = _decodePayload(m['payload']);
|
||||
if (payload != null) previewMedia = messagePreviewMedia(payload);
|
||||
if (rawText == null || rawText.isEmpty) {
|
||||
if (payload != null) previewText = messagePreviewText(payload);
|
||||
} else {
|
||||
@@ -969,6 +987,7 @@ class ChatsModule {
|
||||
newRow['last_msg_id'] = int.tryParse(m['id']?.toString() ?? '');
|
||||
newRow['last_msg_text'] = previewText ?? m['text'];
|
||||
newRow['last_msg_elements'] = elementsJson;
|
||||
newRow['last_msg_preview'] = previewMedia;
|
||||
newRow['last_msg_time'] = m['time'];
|
||||
newRow['last_msg_sender'] = m['sender_id'];
|
||||
newRow['last_msg_status'] = m['status'];
|
||||
@@ -976,6 +995,7 @@ class ChatsModule {
|
||||
newRow['last_msg_id'] = null;
|
||||
newRow['last_msg_text'] = lastMsgPlaceholder;
|
||||
newRow['last_msg_elements'] = null;
|
||||
newRow['last_msg_preview'] = null;
|
||||
newRow['last_msg_sender'] = null;
|
||||
newRow['last_msg_status'] = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user