diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html
index 8854391..0d3d26a 100644
--- a/android/build/reports/problems/problems-report.html
+++ b/android/build/reports/problems/problems-report.html
@@ -650,7 +650,7 @@ code + .copy-button {
diff --git a/lib/data/datasources/local_db_service.dart b/lib/data/datasources/local_db_service.dart
index a36590a..7ed7330 100644
--- a/lib/data/datasources/local_db_service.dart
+++ b/lib/data/datasources/local_db_service.dart
@@ -1,6 +1,7 @@
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:chepuhagram/data/models/message_model.dart';
+import 'dart:typed_data';
class LocalDbService {
static final LocalDbService _instance = LocalDbService._internal();
@@ -19,7 +20,7 @@ class LocalDbService {
String path = join(await getDatabasesPath(), 'chat_app.db');
return await openDatabase(
path,
- version: 4,
+ version: 7,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE messages(
@@ -32,7 +33,10 @@ class LocalDbService {
read_at TEXT,
reply_to_id INTEGER,
reply_to_text TEXT,
- edited_at TEXT
+ edited_at TEXT,
+ message_type TEXT DEFAULT 'text',
+ file_id TEXT,
+ encrypted_key TEXT
)
''');
},
@@ -52,6 +56,38 @@ class LocalDbService {
if (oldVersion < 4) {
await db.execute('ALTER TABLE messages ADD COLUMN edited_at TEXT');
}
+ if (oldVersion < 5) {
+ try {
+ await db.execute(
+ 'ALTER TABLE messages ADD COLUMN message_type TEXT',
+ );
+ } catch (e) {
+ print('message_type column already exists: $e');
+ }
+ try {
+ await db.execute('ALTER TABLE messages ADD COLUMN file_id TEXT');
+ } catch (e) {
+ print('file_id column already exists: $e');
+ }
+ }
+ if (oldVersion < 6) {
+ try {
+ await db.execute(
+ 'ALTER TABLE messages ADD COLUMN encrypted_key TEXT',
+ );
+ } catch (e) {
+ print('encrypted_key column already exists: $e');
+ }
+ }
+ if (oldVersion < 7) {
+ try {
+ await db.execute(
+ 'ALTER TABLE messages ADD COLUMN local_file_bytes BLOB',
+ );
+ } catch (e) {
+ print('local_file_bytes column already exists: $e');
+ }
+ }
},
);
}
@@ -61,23 +97,36 @@ class LocalDbService {
await db.delete('messages');
}
- // Сохранение списка сообщений (из истории)
Future saveMessages(List messages) async {
final db = await database;
+ final List incomingIds = messages.map((msg) {
+ return (msg is MessageModel) ? msg.id! : (msg['id'] as int);
+ }).toList();
+
Batch batch = db.batch();
+
+ if (incomingIds.isNotEmpty) {
+ batch.delete('messages', where: 'id NOT IN (${incomingIds.join(',')})');
+ }
for (var msg in messages) {
if (msg is MessageModel) {
batch.insert('messages', {
'id': msg.id,
'sender_id': msg.senderId,
'receiver_id': msg.receiverId,
- 'content': msg.text, // ВАЖНО: сохраняй зашифрованный текст!
+ 'content': msg.text,
'timestamp': msg.createdAt.toIso8601String(),
'delivered_at': null,
'read_at': null,
'reply_to_id': msg.replyToId,
'reply_to_text': msg.replyToText,
'edited_at': msg.editedAt?.toIso8601String(),
+ 'message_type': msg.messageType == MessageType.image
+ ? 'image'
+ : 'text',
+ 'file_id': msg.fileId,
+ 'encrypted_key': msg.encryptedFileKey,
+ 'local_file_bytes': msg.localFileBytes,
}, conflictAlgorithm: ConflictAlgorithm.replace);
} else {
// Если это Map из API
@@ -93,6 +142,9 @@ class LocalDbService {
'reply_to_id': msg['reply_to_id'],
'reply_to_text': msg['reply_to_text'],
'edited_at': msg['edited_at'],
+ 'message_type': msg['message_type'] ?? 'text',
+ 'file_id': msg['file_id'],
+ 'encrypted_key': msg['encrypted_key'],
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
}
@@ -159,6 +211,19 @@ class LocalDbService {
);
}
+ Future updateMessageLocalFileBytes(
+ int messageId,
+ Uint8List localFileBytes,
+ ) async {
+ final db = await database;
+ await db.update(
+ 'messages',
+ {'local_file_bytes': localFileBytes},
+ where: 'id = ?',
+ whereArgs: [messageId],
+ );
+ }
+
Future updateMessageContent(
int messageId,
String content,
diff --git a/lib/data/models/contact_model.dart b/lib/data/models/contact_model.dart
index ee16ad5..7098b78 100644
--- a/lib/data/models/contact_model.dart
+++ b/lib/data/models/contact_model.dart
@@ -6,8 +6,8 @@ class Contact {
String name;
String surname;
final String? lastMessage;
- final String? avatarFileId;
- final String? avatarUrl;
+ String? avatarFileId;
+ String? avatarUrl;
final DateTime? lastMessageTime;
final bool isOnline;
final int unreadCount;
@@ -51,8 +51,8 @@ class Contact {
name: name ?? this.name,
surname: surname ?? this.surname,
lastMessage: lastMessage ?? this.lastMessage,
- avatarFileId: avatarFileId ?? this.avatarFileId,
- avatarUrl: avatarUrl ?? this.avatarUrl,
+ avatarFileId: avatarFileId,
+ avatarUrl: avatarUrl,
lastMessageTime: lastMessageTime ?? this.lastMessageTime,
isOnline: isOnline ?? this.isOnline,
unreadCount: unreadCount ?? this.unreadCount,
diff --git a/lib/data/models/message_model.dart b/lib/data/models/message_model.dart
index 5a0e167..c4b3b11 100644
--- a/lib/data/models/message_model.dart
+++ b/lib/data/models/message_model.dart
@@ -2,6 +2,8 @@ import 'dart:typed_data';
enum MessageStatus { sending, sent, delivered, read, failed }
+enum MessageType { text, image }
+
class MessageModel {
final int? id; // server id (null пока не подтверждено сервером)
final int? tempId; // client temp id (для сопоставления ack)
@@ -15,6 +17,9 @@ class MessageModel {
final String? replyToText; // текст сообщения, на которое отвечают (для отображения)
final DateTime? editedAt;
final Uint8List? localFileBytes;
+ final MessageType messageType;
+ final String? fileId;
+ final String? encryptedFileKey;
MessageModel({
this.id,
@@ -28,7 +33,10 @@ class MessageModel {
this.replyToId,
this.replyToText,
this.editedAt,
- this.localFileBytes
+ this.localFileBytes,
+ this.messageType = MessageType.text,
+ this.fileId,
+ this.encryptedFileKey,
});
MessageModel copyWith({
@@ -44,6 +52,9 @@ class MessageModel {
String? replyToText,
DateTime? editedAt,
Uint8List? localFileBytes,
+ MessageType? messageType,
+ String? fileId,
+ String? encryptedFileKey,
}) {
return MessageModel(
id: id ?? this.id,
@@ -58,6 +69,9 @@ class MessageModel {
replyToText: replyToText ?? this.replyToText,
editedAt: editedAt ?? this.editedAt,
localFileBytes: localFileBytes ?? this.localFileBytes,
+ messageType: messageType ?? this.messageType,
+ fileId: fileId ?? this.fileId,
+ encryptedFileKey: encryptedFileKey ?? this.encryptedFileKey,
);
}
@@ -78,6 +92,9 @@ class MessageModel {
replyToId: json['reply_to_id'] == null ? null : int.tryParse(json['reply_to_id'].toString()),
replyToText: json['reply_to_text'] == null ? null : json['reply_to_text'].toString(),
editedAt: json['edited_at'] == null ? null : DateTime.tryParse(json['edited_at'].toString()),
+ messageType: json['message_type'] == 'image' ? MessageType.image : MessageType.text,
+ fileId: json['file_id']?.toString(),
+ encryptedFileKey: json['encrypted_key']?.toString(),
);
}
@@ -93,6 +110,9 @@ class MessageModel {
'reply_to_id': replyToId,
'reply_to_text': replyToText,
'edited_at': editedAt?.toIso8601String(),
+ 'message_type': messageType == MessageType.image ? 'image' : 'text',
+ 'file_id': fileId,
+ 'encrypted_key': encryptedFileKey,
};
}
}
diff --git a/lib/domain/services/api_service.dart b/lib/domain/services/api_service.dart
index 4629f80..0a7c52d 100644
--- a/lib/domain/services/api_service.dart
+++ b/lib/domain/services/api_service.dart
@@ -1,21 +1,23 @@
import 'package:jwt_decoder/jwt_decoder.dart';
+import 'dart:convert';
+import 'dart:typed_data';
+
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:chepuhagram/core/constants.dart';
import 'package:http/http.dart' as http;
-import 'dart:convert';
class ApiService extends ChangeNotifier {
final _client = http.Client();
final _storage = const FlutterSecureStorage();
bool _isRefreshing = false;
- Future uploadMedia(List bytes) async {
+ Future uploadMedia(List bytes, {String purpose = 'media'}) async {
try {
- final token = getAccessToken();
+ final token = await getAccessToken();
var request = http.MultipartRequest(
'POST',
- Uri.parse('${AppConstants.baseUrl}/media/upload'),
+ Uri.parse('${AppConstants.baseUrl}/media/v2/upload'),
);
request.headers.addAll({
'Authorization': 'Bearer $token',
@@ -28,9 +30,8 @@ class ApiService extends ChangeNotifier {
filename: 'media.enc', // Имя файла для сервера
),
);
-
- // Добавь заголовки авторизации, если они у тебя есть (JWT и т.д.)
- // request.headers.addAll({'Authorization': 'Bearer $token'});
+ // Добавляем purpose
+ request.fields['purpose'] = purpose;
var streamedResponse = await request.send().timeout(Duration(seconds: 30));
var response = await http.Response.fromStream(streamedResponse);
@@ -227,6 +228,26 @@ class ApiService extends ChangeNotifier {
return jsonDecode(response.body) as List;
}
+ Future downloadMedia(String fileId) async {
+ try {
+ final token = await getAccessToken();
+ final response = await _client.get(
+ Uri.parse('${AppConstants.baseUrl}/media/$fileId'),
+ headers: {
+ 'Authorization': 'Bearer $token',
+ },
+ );
+ if (response.statusCode == 200) {
+ return response.bodyBytes;
+ }
+ print('Ошибка загрузки медиа: ${response.statusCode}');
+ return null;
+ } catch (e) {
+ print('Ошибка downloadMedia: $e');
+ return null;
+ }
+ }
+
Future