feat: история изменения сообщений

This commit is contained in:
torvalds
2026-06-29 15:05:22 +07:00
parent 3f44a8e5be
commit 66aa3a423c
6 changed files with 584 additions and 11 deletions
+13
View File
@@ -538,6 +538,19 @@ class ChatsModule {
mergedPayload[entry.key.toString()] = entry.value;
}
final newRow = Map<String, dynamic>.from(existing);
if (KometSettings.viewRedacted.value) {
final oldText = existing['text']?.toString();
if ((oldText ?? '') != (msgText ?? '') &&
oldText != null &&
oldText.isNotEmpty) {
final history = CachedMessage.appendEditHistory(
CachedMessage.parseEditHistory(existing['edit_history']),
oldText,
DateTime.now().millisecondsSinceEpoch,
);
newRow['edit_history'] = jsonEncode(history);
}
}
newRow['text'] = msgText;
newRow['status'] = status;
newRow['payload'] = jsonEncode(mergedPayload);
+79
View File
@@ -0,0 +1,79 @@
import '../api.dart';
import '../../core/protocol/opcode_map.dart';
class ComplaintReason {
final int reasonId;
final String reasonTitle;
const ComplaintReason({required this.reasonId, required this.reasonTitle});
}
class ComplaintsModule {
static Map<int, List<ComplaintReason>>? _cache;
static Future<Map<int, List<ComplaintReason>>> fetchReasons(Api api) async {
final cached = _cache;
if (cached != null) return cached;
final response = await api.sendRequest(
Opcode.complainReasonsGet,
{'complainSync': 0},
);
if (!response.isOk) return cached ?? const {};
final payload = response.payload;
if (payload is! Map) return const {};
final complains = payload['complains'];
final map = <int, List<ComplaintReason>>{};
if (complains is List) {
for (final entry in complains) {
if (entry is! Map) continue;
final typeId = entry['typeId'];
final reasons = entry['reasons'];
if (typeId is! int || reasons is! List) continue;
map[typeId] = reasons
.whereType<Map>()
.map(
(r) => ComplaintReason(
reasonId: r['reasonId'] is int ? r['reasonId'] as int : 0,
reasonTitle: r['reasonTitle']?.toString() ?? '',
),
)
.where((r) => r.reasonId != 0)
.toList();
}
}
_cache = map;
return map;
}
static Future<List<ComplaintReason>> reasonsFor(Api api, int typeId) async {
final map = await fetchReasons(api);
final forType = map[typeId];
if (forType != null && forType.isNotEmpty) return forType;
for (final list in map.values) {
if (list.isNotEmpty) return list;
}
return const [];
}
static Future<bool> sendComplaint(
Api api, {
required int reasonId,
required int typeId,
required List<int> ids,
required int parentId,
}) async {
final response = await api.sendRequest(Opcode.complain, {
'reasonId': reasonId,
'typeId': typeId,
'ids': ids,
'parentId': parentId,
});
if (!response.isOk) return false;
final payload = response.payload;
return payload is Map && payload['success'] == true;
}
}
+82 -5
View File
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../api.dart';
import '../../core/config/komet_settings.dart';
import '../../core/protocol/opcode_map.dart';
import '../../core/protocol/packet.dart';
import '../../core/storage/app_database.dart';
@@ -365,6 +366,7 @@ class CachedMessage {
final List<MessageAttachment>? attachments;
final bool isControl;
final bool deleted;
final List<Map<String, dynamic>>? editHistory;
const CachedMessage({
required this.id,
@@ -378,12 +380,14 @@ class CachedMessage {
this.attachments,
this.isControl = false,
this.deleted = false,
this.editHistory,
});
CachedMessage copyWith({
String? status,
bool? deleted,
List<MessageAttachment>? attachments,
List<Map<String, dynamic>>? editHistory,
}) => CachedMessage(
id: id,
accountId: accountId,
@@ -396,8 +400,39 @@ class CachedMessage {
attachments: attachments ?? this.attachments,
isControl: isControl,
deleted: deleted ?? this.deleted,
editHistory: editHistory ?? this.editHistory,
);
static List<Map<String, dynamic>>? parseEditHistory(dynamic raw) {
if (raw is! String || raw.isEmpty) return null;
try {
final decoded = jsonDecode(raw);
if (decoded is List) {
final list = decoded
.whereType<Map>()
.map((e) => Map<String, dynamic>.from(e))
.toList();
return list.isEmpty ? null : list;
}
} catch (_) {}
return null;
}
static List<Map<String, dynamic>> appendEditHistory(
List<Map<String, dynamic>>? current,
String? oldText,
int time,
) {
final list = current != null
? List<Map<String, dynamic>>.from(current)
: <Map<String, dynamic>>[];
if (list.isNotEmpty && (list.last['text'] as String?) == oldText) {
return list;
}
list.add({'text': oldText, 'time': time});
return list;
}
factory CachedMessage.fromDbRow(Map<String, dynamic> row) {
Map<String, dynamic>? payload;
final payloadRaw = row['payload'];
@@ -449,6 +484,7 @@ class CachedMessage {
deleted: row['deleted'] is int
? row['deleted'] == 1
: row['deleted']?.toString() == '1',
editHistory: parseEditHistory(row['edit_history']),
);
}
@@ -488,6 +524,7 @@ class CachedMessage {
'status': status,
'payload': payload != null ? jsonEncode(payload) : null,
'deleted': deleted ? 1 : 0,
'edit_history': editHistory != null ? jsonEncode(editHistory) : null,
};
static CachedMessage fromPushPayload(int accountId, int chatId, Map msg) {
@@ -551,7 +588,6 @@ class MessagesModule {
if (messagesData is! List) return [];
final List<CachedMessage> results = [];
final List<Map<String, dynamic>> rows = [];
for (var i = 0; i < messagesData.length; i++) {
final m = messagesData[i];
@@ -560,7 +596,6 @@ class MessagesModule {
final msg = _parseMessage(m.cast<dynamic, dynamic>(), accountId, chatId);
if (msg != null) {
results.add(msg);
rows.add(msg.toDbRow());
}
if (i > 0 && i % 20 == 0) {
@@ -568,15 +603,57 @@ class MessagesModule {
}
}
if (rows.isNotEmpty) {
final toSave = KometSettings.viewRedacted.value && results.isNotEmpty
? await _mergeEditHistory(accountId, chatId, results)
: results;
if (toSave.isNotEmpty) {
try {
await AppDatabase.saveMessages(rows);
await AppDatabase.saveMessages(
toSave.map((m) => m.toDbRow()).toList(),
);
} catch (e) {
logger.e('saveMessages error: $e');
}
}
return results;
return toSave;
}
Future<List<CachedMessage>> _mergeEditHistory(
int accountId,
int chatId,
List<CachedMessage> serverMessages,
) async {
final cachedRows = await AppDatabase.loadMessagesByIds(
accountId,
chatId,
serverMessages.map((m) => m.id).toList(),
);
final byId = <String, Map<String, dynamic>>{};
for (final row in cachedRows) {
final id = row['id']?.toString();
if (id != null) byId[id] = row;
}
final now = DateTime.now().millisecondsSinceEpoch;
final out = <CachedMessage>[];
for (final msg in serverMessages) {
final existing = byId[msg.id];
if (existing == null) {
out.add(msg);
continue;
}
var history = CachedMessage.parseEditHistory(existing['edit_history']);
final oldText = existing['text']?.toString();
if ((oldText ?? '') != (msg.text ?? '') &&
oldText != null &&
oldText.isNotEmpty) {
history = CachedMessage.appendEditHistory(history, oldText, now);
}
out.add(history == null ? msg : msg.copyWith(editHistory: history));
}
return out;
}
/// Загружает сообщения из локальной базы данных.