работа с закрепленными
This commit is contained in:
@@ -43,6 +43,7 @@ CachedChat? parseChatRow(
|
||||
final muteFav = _resolveMuteAndFavorite(chatsConfig, id, existing);
|
||||
final presence = _resolvePresence(type, otherId, presenceMap);
|
||||
final adminsOwner = _resolveAdmins(chat);
|
||||
final pinned = _resolvePinnedMessage(chat['pinnedMessage']);
|
||||
|
||||
return CachedChat(
|
||||
id: id,
|
||||
@@ -66,6 +67,10 @@ CachedChat? parseChatRow(
|
||||
options: titleIcon.options,
|
||||
owner: adminsOwner.owner,
|
||||
admins: adminsOwner.admins,
|
||||
pinnedMsgId: pinned.id,
|
||||
pinnedMsgText: pinned.text,
|
||||
pinnedMsgTime: pinned.time,
|
||||
pinnedMsgIsPreview: pinned.isPreview,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.e("Ошибка при парсинге чата: $e");
|
||||
@@ -130,6 +135,24 @@ _resolveLastMessage(dynamic lastMsg) {
|
||||
);
|
||||
}
|
||||
|
||||
({int? id, String? text, int? time, bool isPreview}) _resolvePinnedMessage(
|
||||
dynamic pinned,
|
||||
) {
|
||||
if (pinned is! Map) {
|
||||
return (id: null, text: null, time: null, isPreview: false);
|
||||
}
|
||||
final rawId = pinned['id'];
|
||||
final id = rawId is int ? rawId : int.tryParse(rawId?.toString() ?? '');
|
||||
if (id == null) return (id: null, text: null, time: null, isPreview: false);
|
||||
final preview = pinnedMessagePreview(pinned);
|
||||
return (
|
||||
id: id,
|
||||
text: preview.text,
|
||||
time: pinned['time'] as int?,
|
||||
isPreview: preview.isPreview,
|
||||
);
|
||||
}
|
||||
|
||||
({int? favIndex, int dontDisturbUntil}) _resolveMuteAndFavorite(
|
||||
Map<dynamic, dynamic> chatsConfig,
|
||||
int id,
|
||||
@@ -270,6 +293,10 @@ bool sameChatContent(CachedChat a, CachedChat b) {
|
||||
if (a.title != b.title) return false;
|
||||
if (a.iconUrl != b.iconUrl) return false;
|
||||
if (a.owner != b.owner) return false;
|
||||
if (a.pinnedMsgId != b.pinnedMsgId) return false;
|
||||
if (a.pinnedMsgText != b.pinnedMsgText) return false;
|
||||
if (a.pinnedMsgTime != b.pinnedMsgTime) return false;
|
||||
if (a.pinnedMsgIsPreview != b.pinnedMsgIsPreview) return false;
|
||||
if (a.dontDisturbUntil != b.dontDisturbUntil) return false;
|
||||
if (a.favIndex != b.favIndex) return false;
|
||||
if (a.lastMsgId != b.lastMsgId) return false;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import 'dart:convert';
|
||||
|
||||
String? attachPreviewLabel(dynamic attaches) {
|
||||
if (attaches is! List || attaches.isEmpty) return null;
|
||||
final first = attaches.first;
|
||||
if (first is! Map) return null;
|
||||
final first = _firstPreviewAttach(attaches);
|
||||
if (first == null) return null;
|
||||
final type = (first['_type'] as String? ?? '').toUpperCase();
|
||||
switch (type) {
|
||||
case 'PHOTO':
|
||||
return 'Фото';
|
||||
case 'VIDEO':
|
||||
return 'Видео';
|
||||
return _isVideoNote(first) ? 'Видео-сообщение' : 'Видео';
|
||||
case 'AUDIO':
|
||||
return 'Голосовое сообщение';
|
||||
case 'FILE':
|
||||
@@ -52,6 +51,23 @@ String? attachPreviewLabel(dynamic attaches) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -90,6 +106,60 @@ String? messagePreviewText(Map msg) {
|
||||
return _bodyPreviewText(msg);
|
||||
}
|
||||
|
||||
({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;
|
||||
|
||||
@@ -59,6 +59,10 @@ class CachedChat {
|
||||
final Set<String> options;
|
||||
final int? owner;
|
||||
final Set<int> admins;
|
||||
final int? pinnedMsgId;
|
||||
final String? pinnedMsgText;
|
||||
final int? pinnedMsgTime;
|
||||
final bool pinnedMsgIsPreview;
|
||||
|
||||
CachedChat({
|
||||
required this.id,
|
||||
@@ -83,6 +87,10 @@ class CachedChat {
|
||||
this.options = const {},
|
||||
this.owner,
|
||||
this.admins = const {},
|
||||
this.pinnedMsgId,
|
||||
this.pinnedMsgText,
|
||||
this.pinnedMsgTime,
|
||||
this.pinnedMsgIsPreview = false,
|
||||
}) : lastMsgTextOneLine = lastMsgText != null && lastMsgText.contains('\n')
|
||||
? lastMsgText.replaceAll('\n', ' ')
|
||||
: lastMsgText;
|
||||
@@ -110,6 +118,15 @@ class CachedChat {
|
||||
|
||||
bool iAmAdmin(int myId) => owner == myId || admins.contains(myId);
|
||||
|
||||
bool get hasPinnedMessage => pinnedMsgId != null;
|
||||
|
||||
bool get isGroupChat => type == 'CHAT' || type == 'GROUP';
|
||||
|
||||
bool canPinMessages(int myId) {
|
||||
if (!isGroupChat) return false;
|
||||
return iAmAdmin(myId) || options.contains('ALL_CAN_PIN_MESSAGE');
|
||||
}
|
||||
|
||||
bool get isMuted {
|
||||
if (dontDisturbUntil == ChatsModule.muteOff) return false;
|
||||
if (dontDisturbUntil < 0) return true;
|
||||
@@ -141,6 +158,10 @@ class CachedChat {
|
||||
options: _decodeOptions(row['options']),
|
||||
owner: row['owner'] as int?,
|
||||
admins: _decodeAdmins(row['admins']),
|
||||
pinnedMsgId: row['pinned_msg_id'] as int?,
|
||||
pinnedMsgText: row['pinned_msg_text'] as String?,
|
||||
pinnedMsgTime: row['pinned_msg_time'] as int?,
|
||||
pinnedMsgIsPreview: (row['pinned_msg_is_preview'] as int? ?? 0) == 1,
|
||||
);
|
||||
|
||||
static Set<String> _decodeOptions(dynamic raw) {
|
||||
@@ -182,6 +203,10 @@ class CachedChat {
|
||||
'options': options.isEmpty ? null : options.join(','),
|
||||
'owner': owner,
|
||||
'admins': admins.isEmpty ? null : admins.join(','),
|
||||
'pinned_msg_id': pinnedMsgId,
|
||||
'pinned_msg_text': pinnedMsgText,
|
||||
'pinned_msg_time': pinnedMsgTime,
|
||||
'pinned_msg_is_preview': pinnedMsgIsPreview ? 1 : 0,
|
||||
};
|
||||
|
||||
static const Object _keep = Object();
|
||||
@@ -207,6 +232,10 @@ class CachedChat {
|
||||
Set<String>? options,
|
||||
Object? owner = _keep,
|
||||
Set<int>? admins,
|
||||
Object? pinnedMsgId = _keep,
|
||||
Object? pinnedMsgText = _keep,
|
||||
Object? pinnedMsgTime = _keep,
|
||||
bool? pinnedMsgIsPreview,
|
||||
}) {
|
||||
return CachedChat(
|
||||
id: id,
|
||||
@@ -243,6 +272,16 @@ class CachedChat {
|
||||
options: options ?? this.options,
|
||||
owner: identical(owner, _keep) ? this.owner : owner as int?,
|
||||
admins: admins ?? this.admins,
|
||||
pinnedMsgId: identical(pinnedMsgId, _keep)
|
||||
? this.pinnedMsgId
|
||||
: pinnedMsgId as int?,
|
||||
pinnedMsgText: identical(pinnedMsgText, _keep)
|
||||
? this.pinnedMsgText
|
||||
: pinnedMsgText as String?,
|
||||
pinnedMsgTime: identical(pinnedMsgTime, _keep)
|
||||
? this.pinnedMsgTime
|
||||
: pinnedMsgTime as int?,
|
||||
pinnedMsgIsPreview: pinnedMsgIsPreview ?? this.pinnedMsgIsPreview,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -700,10 +739,45 @@ class ChatsModule {
|
||||
}
|
||||
if (unread != null) newRow['unread_count'] = unread;
|
||||
|
||||
final pinned = _extractPinnedMessage(msg);
|
||||
if (pinned != null) {
|
||||
newRow['pinned_msg_id'] = pinned.id;
|
||||
newRow['pinned_msg_text'] = pinned.text;
|
||||
newRow['pinned_msg_time'] = pinned.time;
|
||||
newRow['pinned_msg_is_preview'] = pinned.isPreview ? 1 : 0;
|
||||
}
|
||||
|
||||
await AppDatabase.saveChats([newRow]);
|
||||
_bump();
|
||||
}
|
||||
|
||||
({int? id, String? text, int? time, bool isPreview})? _extractPinnedMessage(
|
||||
Map msg,
|
||||
) {
|
||||
final attaches = msg['attaches'];
|
||||
if (attaches is! List) return null;
|
||||
for (final a in attaches.whereType<Map>()) {
|
||||
if ((a['_type'] as String?) != 'CONTROL') continue;
|
||||
final event = a['event']?.toString();
|
||||
if (event != 'pin' && event != 'unpin') continue;
|
||||
final pinned = a['pinnedMessage'];
|
||||
if (event == 'unpin' || pinned is! Map) {
|
||||
return (id: null, text: null, time: null, isPreview: false);
|
||||
}
|
||||
final rawId = pinned['id'];
|
||||
final id = rawId is int ? rawId : int.tryParse(rawId?.toString() ?? '');
|
||||
if (id == null) return null;
|
||||
final preview = pinnedMessagePreview(pinned.cast<dynamic, dynamic>());
|
||||
return (
|
||||
id: id,
|
||||
text: preview.text,
|
||||
time: pinned['time'] as int?,
|
||||
isPreview: preview.isPreview,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _reconcileLastMessage(
|
||||
int accountId,
|
||||
int chatId,
|
||||
@@ -1269,6 +1343,39 @@ class ChatsModule {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<String?> setPinnedMessage(
|
||||
Api api, {
|
||||
required int chatId,
|
||||
required int? messageId,
|
||||
bool notify = true,
|
||||
}) async {
|
||||
try {
|
||||
final packet = await api.sendRequest(Opcode.chatUpdate, {
|
||||
'chatId': chatId,
|
||||
'notifyPin': notify,
|
||||
'pinMessageId': messageId ?? 0,
|
||||
});
|
||||
if (!packet.isOk) {
|
||||
return messageFromErrorPayload(packet.payload);
|
||||
}
|
||||
final data = packet.payload;
|
||||
final chat = data is Map ? data['chat'] : null;
|
||||
if (chat is Map) {
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId != null) {
|
||||
await cacheServerChat(chat.cast<dynamic, dynamic>(), accountId);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} on PacketError catch (e) {
|
||||
logger.w('setPinnedMessage $chatId: ${e.message}');
|
||||
return e.message;
|
||||
} catch (e) {
|
||||
logger.w('setPinnedMessage $chatId: $e');
|
||||
return 'Не удалось изменить закрепление';
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> togglePin(
|
||||
Api api, {
|
||||
required List<int> chatIds,
|
||||
|
||||
Reference in New Issue
Block a user