Chepuhagram/lib/data/datasources/local_db_service.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');
}