288 lines
9.8 KiB
Dart
288 lines
9.8 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:chepuhagram/data/models/message_model.dart';
|
|
import 'package:drift/drift.dart';
|
|
import 'package:drift_sqflite/drift_sqflite.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
part 'local_db_service.g.dart';
|
|
|
|
class Messages extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
IntColumn get senderId => integer().named('sender_id')();
|
|
IntColumn get receiverId => integer().named('receiver_id')();
|
|
TextColumn get content => text().named('content')();
|
|
TextColumn get timestamp => text().named('timestamp')();
|
|
TextColumn get deliveredAt => text().named('delivered_at').nullable()();
|
|
TextColumn get readAt => text().named('read_at').nullable()();
|
|
IntColumn get replyToId => integer().named('reply_to_id').nullable()();
|
|
TextColumn get replyToText => text().named('reply_to_text').nullable()();
|
|
TextColumn get editedAt => text().named('edited_at').nullable()();
|
|
TextColumn get messageType =>
|
|
text().named('message_type').withDefault(const Constant('text'))();
|
|
TextColumn get fileId => text().named('file_id').nullable()();
|
|
TextColumn get encryptedKey => text().named('encrypted_key').nullable()();
|
|
TextColumn get fileName => text().named('file_name').nullable()();
|
|
IntColumn get fileSize => integer().named('file_size').nullable()();
|
|
}
|
|
|
|
class FileNameMappings extends Table {
|
|
TextColumn get fileId => text().named('file_id')();
|
|
TextColumn get originalFileName => text().named('original_file_name')();
|
|
|
|
@override
|
|
Set<Column> get primaryKey => {fileId};
|
|
}
|
|
|
|
@DriftDatabase(tables: [Messages, FileNameMappings])
|
|
class LocalDbService extends _$LocalDbService {
|
|
factory LocalDbService() => instance;
|
|
|
|
LocalDbService._internal() : super(_openConnection()) {
|
|
print('LocalDbService constructor called');
|
|
}
|
|
|
|
static final LocalDbService instance = LocalDbService._internal();
|
|
|
|
@override
|
|
int get schemaVersion => 9;
|
|
|
|
@override
|
|
MigrationStrategy get migration => MigrationStrategy(
|
|
onCreate: (Migrator m) async {
|
|
await m.createAll();
|
|
},
|
|
onUpgrade: (Migrator m, int from, int to) async {
|
|
if (from < 8) {
|
|
await m.deleteTable('messages');
|
|
await m.createAll();
|
|
return;
|
|
}
|
|
if (from < 9) {
|
|
await m.createTable(fileNameMappings);
|
|
}
|
|
},
|
|
);
|
|
|
|
Future<void> clearDatabase() async {
|
|
await delete(messages).go();
|
|
}
|
|
|
|
Future<void> saveMessages(List<dynamic> messageList) async {
|
|
if (messageList.isEmpty) return;
|
|
|
|
final List<int> incomingIds = messageList
|
|
.map<int?>(
|
|
(msg) => (msg is MessageModel)
|
|
? msg.id
|
|
: (msg['id'] == null ? null : int.tryParse(msg['id'].toString())),
|
|
)
|
|
.whereType<int>()
|
|
.toList();
|
|
|
|
// Преобразуем входящие данные в компаньоны заранее
|
|
final companions = messageList.map<MessagesCompanion>((msg) {
|
|
final int? id;
|
|
final int senderId;
|
|
final int receiverId;
|
|
final String content;
|
|
final String timestamp;
|
|
final int? replyToId;
|
|
final String? replyToText;
|
|
final String? editedAt;
|
|
final String messageType;
|
|
final String? fileId;
|
|
final String? encryptedKey;
|
|
final String? fileName;
|
|
final int? fileSize;
|
|
|
|
if (msg is MessageModel) {
|
|
id = msg.id;
|
|
senderId = msg.senderId;
|
|
receiverId = msg.receiverId;
|
|
content = msg.text;
|
|
timestamp = msg.createdAt.toIso8601String();
|
|
replyToId = msg.replyToId;
|
|
replyToText = msg.replyToText;
|
|
editedAt = msg.editedAt?.toIso8601String();
|
|
messageType = msg.messageType.name;
|
|
fileId = msg.fileId;
|
|
encryptedKey = msg.encryptedFileKey;
|
|
fileName = msg.fileName;
|
|
fileSize = msg.fileSize;
|
|
} else {
|
|
id = msg['id'] == null ? null : int.tryParse(msg['id'].toString());
|
|
senderId = int.parse(msg['sender_id'].toString());
|
|
receiverId = int.parse(msg['receiver_id'].toString());
|
|
content = msg['content']?.toString() ?? '';
|
|
timestamp =
|
|
msg['timestamp']?.toString() ?? DateTime.now().toIso8601String();
|
|
replyToId = msg['reply_to_id'] == null
|
|
? null
|
|
: int.tryParse(msg['reply_to_id'].toString());
|
|
replyToText = msg['reply_to_text']?.toString();
|
|
editedAt = msg['edited_at']?.toString();
|
|
messageType = msg['message_type']?.toString() ?? 'text';
|
|
fileId = msg['file_id']?.toString();
|
|
encryptedKey = msg['encrypted_key']?.toString();
|
|
fileName = msg['file_name']?.toString();
|
|
fileSize = msg['file_size'] == null
|
|
? null
|
|
: int.tryParse(msg['file_size'].toString());
|
|
}
|
|
|
|
return MessagesCompanion(
|
|
id: id == null ? const Value.absent() : Value(id),
|
|
senderId: Value(senderId),
|
|
receiverId: Value(receiverId),
|
|
content: Value(content),
|
|
timestamp: Value(timestamp),
|
|
deliveredAt: const Value(null),
|
|
readAt: const Value(null),
|
|
replyToId: Value(replyToId),
|
|
replyToText: Value(replyToText),
|
|
editedAt: Value(editedAt),
|
|
messageType: Value(messageType),
|
|
fileId: Value(fileId),
|
|
encryptedKey: Value(encryptedKey),
|
|
fileName: Value(fileName),
|
|
fileSize: Value(fileSize),
|
|
);
|
|
}).toList();
|
|
|
|
// Выполняем все операции в рамках ОДНОЙ транзакции БД
|
|
await transaction(() async {
|
|
if (incomingIds.isNotEmpty) {
|
|
// ВНИМАНИЕ: Ограничьте удаление только текущим чатом,
|
|
// иначе эта строка очистит сообщения из всех остальных диалогов!
|
|
final first = companions.first;
|
|
await (delete(messages)..where(
|
|
(tbl) =>
|
|
((tbl.senderId.equals(first.senderId.value) &
|
|
tbl.receiverId.equals(first.receiverId.value)) |
|
|
(tbl.senderId.equals(first.receiverId.value) &
|
|
tbl.receiverId.equals(first.senderId.value))) &
|
|
tbl.id.isNotIn(incomingIds),
|
|
))
|
|
.go();
|
|
}
|
|
|
|
// Быстрая пакетная вставка/обновление
|
|
await batch((b) {
|
|
b.insertAll(
|
|
messages,
|
|
companions,
|
|
mode: InsertMode
|
|
.insertOrReplace, // Безопасный аналог insertOnConflictUpdate для batch
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
Future<List<Map<String, dynamic>>> getChatHistory(
|
|
int contactId,
|
|
int myId,
|
|
) async {
|
|
final query = select(messages)
|
|
..where(
|
|
(tbl) =>
|
|
(tbl.senderId.equals(contactId) & tbl.receiverId.equals(myId)) |
|
|
(tbl.senderId.equals(myId) & tbl.receiverId.equals(contactId)),
|
|
)
|
|
..orderBy([(tbl) => OrderingTerm(expression: tbl.timestamp)]);
|
|
|
|
final rows = await query.get();
|
|
return rows.map((row) => row.toJson()).toList();
|
|
}
|
|
|
|
Future<int> deleteChatHistory(int contactId, int myId) async {
|
|
return await (delete(messages)..where(
|
|
(tbl) =>
|
|
(tbl.senderId.equals(contactId) & tbl.receiverId.equals(myId)) |
|
|
(tbl.senderId.equals(myId) & tbl.receiverId.equals(contactId)),
|
|
))
|
|
.go();
|
|
}
|
|
|
|
Future<Map<String, dynamic>?> getLastMessage(int contactId, int myId) async {
|
|
final query =
|
|
(select(messages)
|
|
..where(
|
|
(tbl) =>
|
|
(tbl.senderId.equals(contactId) &
|
|
tbl.receiverId.equals(myId)) |
|
|
(tbl.senderId.equals(myId) &
|
|
tbl.receiverId.equals(contactId)),
|
|
)
|
|
..orderBy([
|
|
(tbl) => OrderingTerm(
|
|
expression: tbl.timestamp,
|
|
mode: OrderingMode.desc,
|
|
),
|
|
])
|
|
..limit(1))
|
|
.get();
|
|
|
|
final rows = await query;
|
|
if (rows.isEmpty) return null;
|
|
return rows.first.toJson();
|
|
}
|
|
|
|
Future<void> updateDeliveredAt(int messageId, DateTime deliveredAt) async {
|
|
await (update(messages)..where((tbl) => tbl.id.equals(messageId))).write(
|
|
MessagesCompanion(deliveredAt: Value(deliveredAt.toIso8601String())),
|
|
);
|
|
}
|
|
|
|
Future<void> updateReadAt(int messageId, DateTime readAt) async {
|
|
await (update(messages)..where((tbl) => tbl.id.equals(messageId))).write(
|
|
MessagesCompanion(readAt: Value(readAt.toIso8601String())),
|
|
);
|
|
}
|
|
|
|
Future<void> updateMessageContent(
|
|
int messageId,
|
|
String content,
|
|
DateTime? editedAt,
|
|
) async {
|
|
await (update(messages)..where((tbl) => tbl.id.equals(messageId))).write(
|
|
MessagesCompanion(
|
|
content: Value(content),
|
|
editedAt: Value(editedAt?.toIso8601String()),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> saveOriginalFileNameForFileId(
|
|
String fileId,
|
|
String fileName,
|
|
) async {
|
|
await into(fileNameMappings).insertOnConflictUpdate(
|
|
FileNameMappingsCompanion(
|
|
fileId: Value(fileId),
|
|
originalFileName: Value(fileName),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<String?> getOriginalFileNameForFileId(String fileId) async {
|
|
final query = select(fileNameMappings)
|
|
..where((tbl) => tbl.fileId.equals(fileId));
|
|
final result = await query.getSingleOrNull();
|
|
return result?.originalFileName;
|
|
}
|
|
|
|
Future<void> deleteMessage(int messageId) async {
|
|
await (delete(messages)..where((tbl) => tbl.id.equals(messageId))).go();
|
|
}
|
|
}
|
|
|
|
QueryExecutor _openConnection() {
|
|
// This now uses the SqfliteQueryExecutor, which will respect the global
|
|
// databaseFactory set in `main.dart`. This fixes the 'unable to open file'
|
|
// error on Windows. The database file will be 'chat_app.db' inside the
|
|
// path configured in `main.dart`.
|
|
return SqfliteQueryExecutor.inDatabaseFolder(path: 'chat_app.db');
|
|
}
|