♻️ Refactored the message repository logic
This commit is contained in:
@@ -1,484 +0,0 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:island/database/drift_db.dart';
|
|
||||||
import 'package:island/database/message.dart';
|
|
||||||
import 'package:island/models/chat.dart';
|
|
||||||
import 'package:island/models/file.dart';
|
|
||||||
import 'package:island/services/file.dart';
|
|
||||||
import 'package:island/widgets/alert.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
|
|
||||||
class MessageRepository {
|
|
||||||
final SnChatRoom room;
|
|
||||||
final SnChatMember identity;
|
|
||||||
final Dio _apiClient;
|
|
||||||
final AppDatabase _database;
|
|
||||||
|
|
||||||
final Map<String, LocalChatMessage> pendingMessages = {};
|
|
||||||
final Map<String, Map<int, double>> fileUploadProgress = {};
|
|
||||||
int? _totalCount;
|
|
||||||
|
|
||||||
MessageRepository(this.room, this.identity, this._apiClient, this._database);
|
|
||||||
|
|
||||||
Future<LocalChatMessage?> getLastMessages() async {
|
|
||||||
final dbMessages = await _database.getMessagesForRoom(
|
|
||||||
room.id,
|
|
||||||
offset: 0,
|
|
||||||
limit: 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dbMessages.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _database.companionToMessage(dbMessages.first);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> syncMessages() async {
|
|
||||||
final lastMessage = await getLastMessages();
|
|
||||||
if (lastMessage == null) return false;
|
|
||||||
try {
|
|
||||||
final resp = await _apiClient.post(
|
|
||||||
'/sphere/chat/${room.id}/sync',
|
|
||||||
data: {
|
|
||||||
'last_sync_timestamp':
|
|
||||||
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final response = MessageSyncResponse.fromJson(resp.data);
|
|
||||||
for (final change in response.changes) {
|
|
||||||
switch (change.action) {
|
|
||||||
case MessageChangeAction.create:
|
|
||||||
await receiveMessage(change.message!);
|
|
||||||
break;
|
|
||||||
case MessageChangeAction.update:
|
|
||||||
await receiveMessageUpdate(change.message!);
|
|
||||||
break;
|
|
||||||
case MessageChangeAction.delete:
|
|
||||||
await receiveMessageDeletion(change.messageId.toString());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> listMessages({
|
|
||||||
int offset = 0,
|
|
||||||
int take = 20,
|
|
||||||
bool synced = false,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
// For initial load, fetch latest messages in the background to sync.
|
|
||||||
if (offset == 0 && !synced) {
|
|
||||||
// Not awaiting this is intentional, for a quicker UI response.
|
|
||||||
// The UI should rely on a stream from the database to get updates.
|
|
||||||
_fetchAndCacheMessages(room.id, offset: 0, take: take).catchError((_) {
|
|
||||||
// Best effort, errors will be handled by later fetches.
|
|
||||||
return <LocalChatMessage>[];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final localMessages = await _getCachedMessages(
|
|
||||||
room.id,
|
|
||||||
offset: offset,
|
|
||||||
take: take,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If local cache has messages, return them. This is the common case for scrolling up.
|
|
||||||
if (localMessages.isNotEmpty) {
|
|
||||||
return localMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If local cache is empty, we've probably reached the end of cached history.
|
|
||||||
// Fetch from remote. This will also be hit on first load if cache is empty.
|
|
||||||
return await _fetchAndCacheMessages(room.id, offset: offset, take: take);
|
|
||||||
} catch (e) {
|
|
||||||
// Final fallback to cache in case of network errors during fetch.
|
|
||||||
final localMessages = await _getCachedMessages(
|
|
||||||
room.id,
|
|
||||||
offset: offset,
|
|
||||||
take: take,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (localMessages.isNotEmpty) {
|
|
||||||
return localMessages;
|
|
||||||
}
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> _getCachedMessages(
|
|
||||||
String roomId, {
|
|
||||||
int offset = 0,
|
|
||||||
int take = 20,
|
|
||||||
}) async {
|
|
||||||
// Get messages from local database
|
|
||||||
final dbMessages = await _database.getMessagesForRoom(
|
|
||||||
roomId,
|
|
||||||
offset: offset,
|
|
||||||
limit: take,
|
|
||||||
);
|
|
||||||
final dbLocalMessages =
|
|
||||||
dbMessages.map(_database.companionToMessage).toList();
|
|
||||||
|
|
||||||
// Combine with pending messages for the first page
|
|
||||||
if (offset == 0) {
|
|
||||||
final pendingForRoom =
|
|
||||||
pendingMessages.values.where((msg) => msg.roomId == roomId).toList();
|
|
||||||
|
|
||||||
final allMessages = [...pendingForRoom, ...dbLocalMessages];
|
|
||||||
allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
|
||||||
|
|
||||||
// Remove duplicates by ID, preserving the order
|
|
||||||
final uniqueMessages = <LocalChatMessage>[];
|
|
||||||
final seenIds = <String>{};
|
|
||||||
for (final message in allMessages) {
|
|
||||||
if (seenIds.add(message.id)) {
|
|
||||||
uniqueMessages.add(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return uniqueMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbLocalMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> _fetchAndCacheMessages(
|
|
||||||
String roomId, {
|
|
||||||
int offset = 0,
|
|
||||||
int take = 20,
|
|
||||||
}) async {
|
|
||||||
// Use cached total count if available, otherwise fetch it
|
|
||||||
if (_totalCount == null) {
|
|
||||||
final response = await _apiClient.get(
|
|
||||||
'/sphere/chat/$roomId/messages',
|
|
||||||
queryParameters: {'offset': 0, 'take': 1},
|
|
||||||
);
|
|
||||||
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offset >= _totalCount!) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
final response = await _apiClient.get(
|
|
||||||
'/sphere/chat/$roomId/messages',
|
|
||||||
queryParameters: {'offset': offset, 'take': take},
|
|
||||||
);
|
|
||||||
|
|
||||||
final List<dynamic> data = response.data;
|
|
||||||
// Update total count from response headers
|
|
||||||
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
|
|
||||||
|
|
||||||
final messages =
|
|
||||||
data.map((json) {
|
|
||||||
final remoteMessage = SnChatMessage.fromJson(json);
|
|
||||||
return LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
for (final message in messages) {
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(message));
|
|
||||||
if (message.nonce != null) {
|
|
||||||
pendingMessages.removeWhere(
|
|
||||||
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalChatMessage> sendMessage(
|
|
||||||
String token,
|
|
||||||
String baseUrl,
|
|
||||||
String roomId,
|
|
||||||
String content,
|
|
||||||
String nonce, {
|
|
||||||
required List<UniversalFile> attachments,
|
|
||||||
Map<String, dynamic>? meta,
|
|
||||||
SnChatMessage? replyingTo,
|
|
||||||
SnChatMessage? forwardingTo,
|
|
||||||
SnChatMessage? editingTo,
|
|
||||||
Function(LocalChatMessage)? onPending,
|
|
||||||
Function(String, Map<int, double>)? onProgress,
|
|
||||||
}) async {
|
|
||||||
// Generate a unique nonce for this message
|
|
||||||
final nonce = const Uuid().v4();
|
|
||||||
|
|
||||||
// Create a local message with pending status
|
|
||||||
final mockMessage = SnChatMessage(
|
|
||||||
id: 'pending_$nonce',
|
|
||||||
chatRoomId: roomId,
|
|
||||||
senderId: identity.id,
|
|
||||||
content: content,
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
nonce: nonce,
|
|
||||||
sender: identity,
|
|
||||||
);
|
|
||||||
|
|
||||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
mockMessage,
|
|
||||||
MessageStatus.pending,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store in memory and database
|
|
||||||
pendingMessages[localMessage.id] = localMessage;
|
|
||||||
fileUploadProgress[localMessage.id] = {};
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
|
||||||
onPending?.call(localMessage);
|
|
||||||
|
|
||||||
try {
|
|
||||||
var cloudAttachments = List.empty(growable: true);
|
|
||||||
// Upload files
|
|
||||||
for (var idx = 0; idx < attachments.length; idx++) {
|
|
||||||
final cloudFile =
|
|
||||||
await putMediaToCloud(
|
|
||||||
fileData: attachments[idx],
|
|
||||||
atk: token,
|
|
||||||
baseUrl: baseUrl,
|
|
||||||
filename: attachments[idx].data.name ?? 'Post media',
|
|
||||||
mimetype:
|
|
||||||
attachments[idx].data.mimeType ??
|
|
||||||
switch (attachments[idx].type) {
|
|
||||||
UniversalFileType.image => 'image/unknown',
|
|
||||||
UniversalFileType.video => 'video/unknown',
|
|
||||||
UniversalFileType.audio => 'audio/unknown',
|
|
||||||
UniversalFileType.file => 'application/octet-stream',
|
|
||||||
},
|
|
||||||
onProgress: (progress, _) {
|
|
||||||
fileUploadProgress[localMessage.id]?[idx] = progress;
|
|
||||||
onProgress?.call(
|
|
||||||
localMessage.id,
|
|
||||||
fileUploadProgress[localMessage.id] ?? {},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).future;
|
|
||||||
if (cloudFile == null) {
|
|
||||||
throw ArgumentError('Failed to upload the file...');
|
|
||||||
}
|
|
||||||
cloudAttachments.add(cloudFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send to server
|
|
||||||
final response = await _apiClient.request(
|
|
||||||
editingTo == null
|
|
||||||
? '/sphere/chat/$roomId/messages'
|
|
||||||
: '/sphere/chat/$roomId/messages/${editingTo.id}',
|
|
||||||
data: {
|
|
||||||
'content': content,
|
|
||||||
'attachments_id': cloudAttachments.map((e) => e.id).toList(),
|
|
||||||
'replied_message_id': replyingTo?.id,
|
|
||||||
'forwarded_message_id': forwardingTo?.id,
|
|
||||||
'meta': meta,
|
|
||||||
'nonce': nonce,
|
|
||||||
},
|
|
||||||
options: Options(method: editingTo == null ? 'POST' : 'PATCH'),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update with server response
|
|
||||||
final remoteMessage = SnChatMessage.fromJson(response.data);
|
|
||||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove from pending and update in database
|
|
||||||
pendingMessages.remove(localMessage.id);
|
|
||||||
await _database.deleteMessage(localMessage.id);
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
|
||||||
|
|
||||||
return updatedMessage;
|
|
||||||
} catch (e) {
|
|
||||||
// Update status to failed
|
|
||||||
localMessage.status = MessageStatus.failed;
|
|
||||||
pendingMessages[localMessage.id] = localMessage;
|
|
||||||
await _database.updateMessageStatus(
|
|
||||||
localMessage.id,
|
|
||||||
MessageStatus.failed,
|
|
||||||
);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalChatMessage> retryMessage(String pendingMessageId) async {
|
|
||||||
final message = await getMessageById(pendingMessageId);
|
|
||||||
if (message == null) {
|
|
||||||
throw Exception('Message not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update status back to pending
|
|
||||||
message.status = MessageStatus.pending;
|
|
||||||
pendingMessages[pendingMessageId] = message;
|
|
||||||
await _database.updateMessageStatus(
|
|
||||||
pendingMessageId,
|
|
||||||
MessageStatus.pending,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Send to server
|
|
||||||
var remoteMessage = message.toRemoteMessage();
|
|
||||||
final response = await _apiClient.post(
|
|
||||||
'/sphere/chat/${message.roomId}/messages',
|
|
||||||
data: {
|
|
||||||
'content': remoteMessage.content,
|
|
||||||
'attachments_id': remoteMessage.attachments,
|
|
||||||
'meta': remoteMessage.meta,
|
|
||||||
'nonce': message.nonce,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update with server response
|
|
||||||
remoteMessage = SnChatMessage.fromJson(response.data);
|
|
||||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove from pending and update in database
|
|
||||||
pendingMessages.remove(pendingMessageId);
|
|
||||||
await _database.deleteMessage(pendingMessageId);
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
|
||||||
|
|
||||||
return updatedMessage;
|
|
||||||
} catch (e) {
|
|
||||||
// Update status to failed
|
|
||||||
message.status = MessageStatus.failed;
|
|
||||||
pendingMessages[pendingMessageId] = message;
|
|
||||||
await _database.updateMessageStatus(
|
|
||||||
pendingMessageId,
|
|
||||||
MessageStatus.failed,
|
|
||||||
);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalChatMessage> receiveMessage(SnChatMessage remoteMessage) async {
|
|
||||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (remoteMessage.nonce != null) {
|
|
||||||
pendingMessages.removeWhere(
|
|
||||||
(_, pendingMsg) => pendingMsg.nonce == remoteMessage.nonce,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
|
||||||
return localMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalChatMessage> receiveMessageUpdate(
|
|
||||||
SnChatMessage remoteMessage,
|
|
||||||
) async {
|
|
||||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
|
|
||||||
await _database.updateMessage(_database.messageToCompanion(localMessage));
|
|
||||||
return localMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> receiveMessageDeletion(String messageId) async {
|
|
||||||
// Remove from pending messages if exists
|
|
||||||
pendingMessages.remove(messageId);
|
|
||||||
|
|
||||||
// Delete from local database
|
|
||||||
await _database.deleteMessage(messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalChatMessage> updateMessage(
|
|
||||||
String messageId,
|
|
||||||
String content, {
|
|
||||||
List<SnCloudFile>? attachments,
|
|
||||||
Map<String, dynamic>? meta,
|
|
||||||
}) async {
|
|
||||||
final message = pendingMessages[messageId];
|
|
||||||
if (message != null) {
|
|
||||||
// Update pending message
|
|
||||||
final rmMessage = message.toRemoteMessage();
|
|
||||||
final updatedRemoteMessage = rmMessage.copyWith(
|
|
||||||
content: content,
|
|
||||||
meta: meta ?? rmMessage.meta,
|
|
||||||
);
|
|
||||||
final updatedLocalMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
updatedRemoteMessage,
|
|
||||||
MessageStatus.pending,
|
|
||||||
);
|
|
||||||
pendingMessages[messageId] = updatedLocalMessage;
|
|
||||||
await _database.updateMessage(
|
|
||||||
_database.messageToCompanion(updatedLocalMessage),
|
|
||||||
);
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Update on server
|
|
||||||
final response = await _apiClient.put(
|
|
||||||
'/sphere/chat/${room.id}/messages/$messageId',
|
|
||||||
data: {'content': content, 'attachments': attachments, 'meta': meta},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update local copy
|
|
||||||
final remoteMessage = SnChatMessage.fromJson(response.data);
|
|
||||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
await _database.updateMessage(
|
|
||||||
_database.messageToCompanion(updatedMessage),
|
|
||||||
);
|
|
||||||
return updatedMessage;
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteMessage(String messageId) async {
|
|
||||||
try {
|
|
||||||
await _apiClient.delete('/sphere/chat/${room.id}/messages/$messageId');
|
|
||||||
pendingMessages.remove(messageId);
|
|
||||||
await _database.deleteMessage(messageId);
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalChatMessage?> getMessageById(String messageId) async {
|
|
||||||
try {
|
|
||||||
// Attempt to get the message from the local database
|
|
||||||
final localMessage =
|
|
||||||
await (_database.select(_database.chatMessages)
|
|
||||||
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
|
|
||||||
if (localMessage != null) {
|
|
||||||
return _database.companionToMessage(localMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not found locally, fetch from the server
|
|
||||||
final response = await _apiClient.get(
|
|
||||||
'/sphere/chat/${room.id}/messages/$messageId',
|
|
||||||
);
|
|
||||||
final remoteMessage = SnChatMessage.fromJson(response.data);
|
|
||||||
final message = LocalChatMessage.fromRemoteMessage(
|
|
||||||
remoteMessage,
|
|
||||||
MessageStatus.sent,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save the fetched message to the local database
|
|
||||||
await _database.saveMessage(_database.messageToCompanion(message));
|
|
||||||
return message;
|
|
||||||
} catch (e) {
|
|
||||||
if (e is DioException) return null;
|
|
||||||
// Handle errors
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -10,14 +11,15 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:island/database/drift_db.dart';
|
||||||
import 'package:island/database/message.dart';
|
import 'package:island/database/message.dart';
|
||||||
import 'package:island/database/message_repository.dart';
|
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/database.dart';
|
import 'package:island/pods/database.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
|
import 'package:island/services/file.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
@@ -39,17 +41,44 @@ import 'package:island/widgets/stickers/picker.dart';
|
|||||||
|
|
||||||
part 'room.g.dart';
|
part 'room.g.dart';
|
||||||
|
|
||||||
final messageRepositoryProvider =
|
final appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) {
|
||||||
FutureProvider.family<MessageRepository, String>((ref, roomId) async {
|
final controller = StreamController<AppLifecycleState>();
|
||||||
final room = await ref.watch(chatroomProvider(roomId).future);
|
|
||||||
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
|
final observer = _AppLifecycleObserver((state) {
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
if (controller.isClosed) return;
|
||||||
final database = ref.watch(databaseProvider);
|
controller.add(state);
|
||||||
return MessageRepository(room!, identity!, apiClient, database);
|
});
|
||||||
});
|
WidgetsBinding.instance.addObserver(observer);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
WidgetsBinding.instance.removeObserver(observer);
|
||||||
|
controller.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return controller.stream;
|
||||||
|
});
|
||||||
|
|
||||||
|
class _AppLifecycleObserver extends WidgetsBindingObserver {
|
||||||
|
final ValueChanged<AppLifecycleState> onChange;
|
||||||
|
_AppLifecycleObserver(this.onChange);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
onChange(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class MessagesNotifier extends _$MessagesNotifier {
|
class MessagesNotifier extends _$MessagesNotifier {
|
||||||
|
late final Dio _apiClient;
|
||||||
|
late final AppDatabase _database;
|
||||||
|
late final SnChatRoom _room;
|
||||||
|
late final SnChatMember _identity;
|
||||||
|
|
||||||
|
final Map<String, LocalChatMessage> _pendingMessages = {};
|
||||||
|
final Map<String, Map<int, double>> _fileUploadProgress = {};
|
||||||
|
int? _totalCount;
|
||||||
|
|
||||||
late final String _roomId;
|
late final String _roomId;
|
||||||
int _currentPage = 0;
|
int _currentPage = 0;
|
||||||
static const int _pageSize = 20;
|
static const int _pageSize = 20;
|
||||||
@@ -58,16 +87,183 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
@override
|
@override
|
||||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
||||||
_roomId = roomId;
|
_roomId = roomId;
|
||||||
|
_apiClient = ref.watch(apiClientProvider);
|
||||||
|
_database = ref.watch(databaseProvider);
|
||||||
|
final room = await ref.watch(chatroomProvider(roomId).future);
|
||||||
|
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
|
||||||
|
if (room == null || identity == null) {
|
||||||
|
throw Exception('Room or identity not found');
|
||||||
|
}
|
||||||
|
_room = room;
|
||||||
|
_identity = identity;
|
||||||
|
|
||||||
|
ref.listen(appLifecycleStateProvider, (_, next) {
|
||||||
|
if (next.hasValue && next.value == AppLifecycleState.resumed) {
|
||||||
|
syncMessages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return await loadInitial();
|
return await loadInitial();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<LocalChatMessage>> _getCachedMessages({
|
||||||
|
int offset = 0,
|
||||||
|
int take = 20,
|
||||||
|
}) async {
|
||||||
|
final dbMessages = await _database.getMessagesForRoom(
|
||||||
|
_roomId,
|
||||||
|
offset: offset,
|
||||||
|
limit: take,
|
||||||
|
);
|
||||||
|
final dbLocalMessages =
|
||||||
|
dbMessages.map(_database.companionToMessage).toList();
|
||||||
|
|
||||||
|
if (offset == 0) {
|
||||||
|
final pendingForRoom =
|
||||||
|
_pendingMessages.values
|
||||||
|
.where((msg) => msg.roomId == _roomId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final allMessages = [...pendingForRoom, ...dbLocalMessages];
|
||||||
|
allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
|
|
||||||
|
final uniqueMessages = <LocalChatMessage>[];
|
||||||
|
final seenIds = <String>{};
|
||||||
|
for (final message in allMessages) {
|
||||||
|
if (seenIds.add(message.id)) {
|
||||||
|
uniqueMessages.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbLocalMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<LocalChatMessage>> _fetchAndCacheMessages({
|
||||||
|
int offset = 0,
|
||||||
|
int take = 20,
|
||||||
|
}) async {
|
||||||
|
if (_totalCount == null) {
|
||||||
|
final response = await _apiClient.get(
|
||||||
|
'/sphere/chat/$_roomId/messages',
|
||||||
|
queryParameters: {'offset': 0, 'take': 1},
|
||||||
|
);
|
||||||
|
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset >= _totalCount!) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _apiClient.get(
|
||||||
|
'/sphere/chat/$_roomId/messages',
|
||||||
|
queryParameters: {'offset': offset, 'take': take},
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<dynamic> data = response.data;
|
||||||
|
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
|
||||||
|
|
||||||
|
final messages =
|
||||||
|
data.map((json) {
|
||||||
|
final remoteMessage = SnChatMessage.fromJson(json);
|
||||||
|
return LocalChatMessage.fromRemoteMessage(
|
||||||
|
remoteMessage,
|
||||||
|
MessageStatus.sent,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
for (final message in messages) {
|
||||||
|
await _database.saveMessage(_database.messageToCompanion(message));
|
||||||
|
if (message.nonce != null) {
|
||||||
|
_pendingMessages.removeWhere(
|
||||||
|
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> syncMessages() async {
|
||||||
|
final dbMessages = await _database.getMessagesForRoom(
|
||||||
|
_room.id,
|
||||||
|
offset: 0,
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
final lastMessage =
|
||||||
|
dbMessages.isEmpty
|
||||||
|
? null
|
||||||
|
: _database.companionToMessage(dbMessages.first);
|
||||||
|
if (lastMessage == null) return false;
|
||||||
|
try {
|
||||||
|
final resp = await _apiClient.post(
|
||||||
|
'/sphere/chat/${_room.id}/sync',
|
||||||
|
data: {
|
||||||
|
'last_sync_timestamp':
|
||||||
|
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final response = MessageSyncResponse.fromJson(resp.data);
|
||||||
|
for (final change in response.changes) {
|
||||||
|
switch (change.action) {
|
||||||
|
case MessageChangeAction.create:
|
||||||
|
await receiveMessage(change.message!);
|
||||||
|
break;
|
||||||
|
case MessageChangeAction.update:
|
||||||
|
await receiveMessageUpdate(change.message!);
|
||||||
|
break;
|
||||||
|
case MessageChangeAction.delete:
|
||||||
|
await receiveMessageDeletion(change.messageId.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<LocalChatMessage>> listMessages({
|
||||||
|
int offset = 0,
|
||||||
|
int take = 20,
|
||||||
|
bool synced = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
if (offset == 0 && !synced) {
|
||||||
|
_fetchAndCacheMessages(offset: 0, take: take).catchError((_) {
|
||||||
|
return <LocalChatMessage>[];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final localMessages = await _getCachedMessages(
|
||||||
|
offset: offset,
|
||||||
|
take: take,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (localMessages.isNotEmpty) {
|
||||||
|
return localMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _fetchAndCacheMessages(offset: offset, take: take);
|
||||||
|
} catch (e) {
|
||||||
|
final localMessages = await _getCachedMessages(
|
||||||
|
offset: offset,
|
||||||
|
take: take,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (localMessages.isNotEmpty) {
|
||||||
|
return localMessages;
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> loadInitial() async {
|
Future<List<LocalChatMessage>> loadInitial() async {
|
||||||
try {
|
try {
|
||||||
final repository = await ref.read(
|
final synced = await syncMessages();
|
||||||
messageRepositoryProvider(_roomId).future,
|
final messages = await listMessages(
|
||||||
);
|
|
||||||
final synced = await repository.syncMessages();
|
|
||||||
final messages = await repository.listMessages(
|
|
||||||
offset: 0,
|
offset: 0,
|
||||||
take: _pageSize,
|
take: _pageSize,
|
||||||
synced: synced,
|
synced: synced,
|
||||||
@@ -86,10 +282,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
try {
|
try {
|
||||||
final currentMessages = state.value ?? [];
|
final currentMessages = state.value ?? [];
|
||||||
_currentPage++;
|
_currentPage++;
|
||||||
final repository = await ref.read(
|
final newMessages = await listMessages(
|
||||||
messageRepositoryProvider(_roomId).future,
|
|
||||||
);
|
|
||||||
final newMessages = await repository.listMessages(
|
|
||||||
offset: _currentPage * _pageSize,
|
offset: _currentPage * _pageSize,
|
||||||
take: _pageSize,
|
take: _pageSize,
|
||||||
);
|
);
|
||||||
@@ -113,163 +306,247 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
SnChatMessage? replyingTo,
|
SnChatMessage? replyingTo,
|
||||||
Function(String, Map<int, double>)? onProgress,
|
Function(String, Map<int, double>)? onProgress,
|
||||||
}) async {
|
}) async {
|
||||||
|
final baseUrl = ref.read(serverUrlProvider);
|
||||||
|
final token = await getToken(ref.watch(tokenProvider));
|
||||||
|
if (token == null) throw ArgumentError('Access token is null');
|
||||||
|
|
||||||
|
final nonce = const Uuid().v4();
|
||||||
|
final mockMessage = SnChatMessage(
|
||||||
|
id: 'pending_$nonce',
|
||||||
|
chatRoomId: _roomId,
|
||||||
|
senderId: _identity.id,
|
||||||
|
content: content,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
nonce: nonce,
|
||||||
|
sender: _identity,
|
||||||
|
);
|
||||||
|
|
||||||
|
final localMessage = LocalChatMessage.fromRemoteMessage(
|
||||||
|
mockMessage,
|
||||||
|
MessageStatus.pending,
|
||||||
|
);
|
||||||
|
|
||||||
|
_pendingMessages[localMessage.id] = localMessage;
|
||||||
|
_fileUploadProgress[localMessage.id] = {};
|
||||||
|
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
||||||
|
|
||||||
|
final currentMessages = state.value ?? [];
|
||||||
|
state = AsyncValue.data([localMessage, ...currentMessages]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final repository = await ref.read(
|
var cloudAttachments = List.empty(growable: true);
|
||||||
messageRepositoryProvider(_roomId).future,
|
for (var idx = 0; idx < attachments.length; idx++) {
|
||||||
);
|
final cloudFile =
|
||||||
final baseUrl = ref.read(serverUrlProvider);
|
await putMediaToCloud(
|
||||||
final token = await getToken(ref.watch(tokenProvider));
|
fileData: attachments[idx],
|
||||||
if (token == null) throw ArgumentError('Access token is null');
|
atk: token,
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
filename: attachments[idx].data.name ?? 'Post media',
|
||||||
|
mimetype:
|
||||||
|
attachments[idx].data.mimeType ??
|
||||||
|
switch (attachments[idx].type) {
|
||||||
|
UniversalFileType.image => 'image/unknown',
|
||||||
|
UniversalFileType.video => 'video/unknown',
|
||||||
|
UniversalFileType.audio => 'audio/unknown',
|
||||||
|
UniversalFileType.file => 'application/octet-stream',
|
||||||
|
},
|
||||||
|
onProgress: (progress, _) {
|
||||||
|
_fileUploadProgress[localMessage.id]?[idx] = progress;
|
||||||
|
onProgress?.call(
|
||||||
|
localMessage.id,
|
||||||
|
_fileUploadProgress[localMessage.id] ?? {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).future;
|
||||||
|
if (cloudFile == null) {
|
||||||
|
throw ArgumentError('Failed to upload the file...');
|
||||||
|
}
|
||||||
|
cloudAttachments.add(cloudFile);
|
||||||
|
}
|
||||||
|
|
||||||
final currentMessages = state.value ?? [];
|
final response = await _apiClient.request(
|
||||||
await repository.sendMessage(
|
editingTo == null
|
||||||
token,
|
? '/sphere/chat/$_roomId/messages'
|
||||||
baseUrl,
|
: '/sphere/chat/$_roomId/messages/${editingTo.id}',
|
||||||
_roomId,
|
data: {
|
||||||
content,
|
'content': content,
|
||||||
const Uuid().v4(),
|
'attachments_id': cloudAttachments.map((e) => e.id).toList(),
|
||||||
attachments: attachments,
|
'replied_message_id': replyingTo?.id,
|
||||||
editingTo: editingTo,
|
'forwarded_message_id': forwardingTo?.id,
|
||||||
forwardingTo: forwardingTo,
|
'meta': {},
|
||||||
replyingTo: replyingTo,
|
'nonce': nonce,
|
||||||
onPending: (pending) {
|
|
||||||
state = AsyncValue.data([pending, ...currentMessages]);
|
|
||||||
},
|
},
|
||||||
onProgress: onProgress,
|
options: Options(method: editingTo == null ? 'POST' : 'PATCH'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Refresh messages
|
final remoteMessage = SnChatMessage.fromJson(response.data);
|
||||||
final messages = await repository.listMessages(
|
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||||
offset: 0,
|
remoteMessage,
|
||||||
take: _pageSize,
|
MessageStatus.sent,
|
||||||
);
|
);
|
||||||
state = AsyncValue.data(messages);
|
|
||||||
|
_pendingMessages.remove(localMessage.id);
|
||||||
|
await _database.deleteMessage(localMessage.id);
|
||||||
|
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
||||||
|
|
||||||
|
final newMessages =
|
||||||
|
(state.value ?? []).map((m) {
|
||||||
|
if (m.id == localMessage.id) {
|
||||||
|
return updatedMessage;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}).toList();
|
||||||
|
state = AsyncValue.data(newMessages);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
localMessage.status = MessageStatus.failed;
|
||||||
|
_pendingMessages[localMessage.id] = localMessage;
|
||||||
|
await _database.updateMessageStatus(
|
||||||
|
localMessage.id,
|
||||||
|
MessageStatus.failed,
|
||||||
|
);
|
||||||
|
final newMessages =
|
||||||
|
(state.value ?? []).map((m) {
|
||||||
|
if (m.id == localMessage.id) {
|
||||||
|
return m..status = MessageStatus.failed;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}).toList();
|
||||||
|
state = AsyncValue.data(newMessages);
|
||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> retryMessage(String pendingMessageId) async {
|
Future<void> retryMessage(String pendingMessageId) async {
|
||||||
try {
|
final message = await fetchMessageById(pendingMessageId);
|
||||||
final repository = await ref.read(
|
if (message == null) {
|
||||||
messageRepositoryProvider(_roomId).future,
|
throw Exception('Message not found');
|
||||||
);
|
}
|
||||||
final updatedMessage = await repository.retryMessage(pendingMessageId);
|
|
||||||
|
|
||||||
// Update the message in the list
|
message.status = MessageStatus.pending;
|
||||||
final currentMessages = state.value ?? [];
|
_pendingMessages[pendingMessageId] = message;
|
||||||
final index = currentMessages.indexWhere((m) => m.id == pendingMessageId);
|
await _database.updateMessageStatus(
|
||||||
if (index >= 0) {
|
pendingMessageId,
|
||||||
final newList = [...currentMessages];
|
MessageStatus.pending,
|
||||||
newList[index] = updatedMessage;
|
);
|
||||||
state = AsyncValue.data(newList);
|
|
||||||
}
|
try {
|
||||||
|
var remoteMessage = message.toRemoteMessage();
|
||||||
|
final response = await _apiClient.post(
|
||||||
|
'/sphere/chat/${message.roomId}/messages',
|
||||||
|
data: {
|
||||||
|
'content': remoteMessage.content,
|
||||||
|
'attachments_id': remoteMessage.attachments,
|
||||||
|
'meta': remoteMessage.meta,
|
||||||
|
'nonce': message.nonce,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
remoteMessage = SnChatMessage.fromJson(response.data);
|
||||||
|
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||||
|
remoteMessage,
|
||||||
|
MessageStatus.sent,
|
||||||
|
);
|
||||||
|
|
||||||
|
_pendingMessages.remove(pendingMessageId);
|
||||||
|
await _database.deleteMessage(pendingMessageId);
|
||||||
|
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
||||||
|
|
||||||
|
final newMessages =
|
||||||
|
(state.value ?? []).map((m) {
|
||||||
|
if (m.id == pendingMessageId) {
|
||||||
|
return updatedMessage;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}).toList();
|
||||||
|
state = AsyncValue.data(newMessages);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
message.status = MessageStatus.failed;
|
||||||
|
_pendingMessages[pendingMessageId] = message;
|
||||||
|
await _database.updateMessageStatus(
|
||||||
|
pendingMessageId,
|
||||||
|
MessageStatus.failed,
|
||||||
|
);
|
||||||
|
final newMessages =
|
||||||
|
(state.value ?? []).map((m) {
|
||||||
|
if (m.id == pendingMessageId) {
|
||||||
|
return m..status = MessageStatus.failed;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}).toList();
|
||||||
|
state = AsyncValue.data(newMessages);
|
||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> receiveMessage(SnChatMessage remoteMessage) async {
|
Future<void> receiveMessage(SnChatMessage remoteMessage) async {
|
||||||
try {
|
if (remoteMessage.chatRoomId != _roomId) return;
|
||||||
final repository = await ref.read(
|
|
||||||
messageRepositoryProvider(_roomId).future,
|
final localMessage = LocalChatMessage.fromRemoteMessage(
|
||||||
|
remoteMessage,
|
||||||
|
MessageStatus.sent,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (remoteMessage.nonce != null) {
|
||||||
|
_pendingMessages.removeWhere(
|
||||||
|
(_, pendingMsg) => pendingMsg.nonce == remoteMessage.nonce,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if this message is not for this room
|
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
||||||
if (remoteMessage.chatRoomId != _roomId) return;
|
|
||||||
|
|
||||||
final localMessage = await repository.receiveMessage(remoteMessage);
|
final currentMessages = state.value ?? [];
|
||||||
|
final existingIndex = currentMessages.indexWhere(
|
||||||
|
(m) =>
|
||||||
|
m.id == localMessage.id ||
|
||||||
|
(localMessage.nonce != null && m.nonce == localMessage.nonce),
|
||||||
|
);
|
||||||
|
|
||||||
// Add the new message to the state
|
if (existingIndex >= 0) {
|
||||||
final currentMessages = state.value ?? [];
|
final newList = [...currentMessages];
|
||||||
|
newList[existingIndex] = localMessage;
|
||||||
// Check if the message already exists (by id or nonce)
|
state = AsyncValue.data(newList);
|
||||||
final existingIndex = currentMessages.indexWhere(
|
} else {
|
||||||
(m) =>
|
state = AsyncValue.data([localMessage, ...currentMessages]);
|
||||||
m.id == localMessage.id ||
|
|
||||||
(localMessage.nonce != null && m.nonce == localMessage.nonce),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
// Replace existing message
|
|
||||||
final newList = [...currentMessages];
|
|
||||||
newList[existingIndex] = localMessage;
|
|
||||||
state = AsyncValue.data(newList);
|
|
||||||
} else {
|
|
||||||
// Add new message at the beginning (newest first)
|
|
||||||
state = AsyncValue.data([localMessage, ...currentMessages]);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async {
|
Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async {
|
||||||
try {
|
if (remoteMessage.chatRoomId != _roomId) return;
|
||||||
final repository = await ref.read(
|
|
||||||
messageRepositoryProvider(_roomId).future,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Skip if this message is not for this room
|
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||||
if (remoteMessage.chatRoomId != _roomId) return;
|
remoteMessage,
|
||||||
|
MessageStatus.sent,
|
||||||
|
);
|
||||||
|
await _database.updateMessage(_database.messageToCompanion(updatedMessage));
|
||||||
|
|
||||||
final updatedMessage = await repository.receiveMessageUpdate(
|
final currentMessages = state.value ?? [];
|
||||||
remoteMessage,
|
final index = currentMessages.indexWhere((m) => m.id == updatedMessage.id);
|
||||||
);
|
|
||||||
|
|
||||||
// Update the message in the list
|
if (index >= 0) {
|
||||||
final currentMessages = state.value ?? [];
|
final newList = [...currentMessages];
|
||||||
final index = currentMessages.indexWhere(
|
newList[index] = updatedMessage;
|
||||||
(m) => m.id == updatedMessage.id,
|
state = AsyncValue.data(newList);
|
||||||
);
|
|
||||||
|
|
||||||
if (index >= 0) {
|
|
||||||
final newList = [...currentMessages];
|
|
||||||
newList[index] = updatedMessage;
|
|
||||||
state = AsyncValue.data(newList);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> receiveMessageDeletion(String messageId) async {
|
Future<void> receiveMessageDeletion(String messageId) async {
|
||||||
try {
|
_pendingMessages.remove(messageId);
|
||||||
final repository = await ref.read(
|
await _database.deleteMessage(messageId);
|
||||||
messageRepositoryProvider(_roomId).future,
|
|
||||||
);
|
|
||||||
|
|
||||||
await repository.receiveMessageDeletion(messageId);
|
final currentMessages = state.value ?? [];
|
||||||
|
final filteredMessages =
|
||||||
|
currentMessages.where((m) => m.id != messageId).toList();
|
||||||
|
|
||||||
// Remove the message from the list
|
if (filteredMessages.length != currentMessages.length) {
|
||||||
final currentMessages = state.value ?? [];
|
state = AsyncValue.data(filteredMessages);
|
||||||
final filteredMessages =
|
|
||||||
currentMessages.where((m) => m.id != messageId).toList();
|
|
||||||
|
|
||||||
if (filteredMessages.length != currentMessages.length) {
|
|
||||||
state = AsyncValue.data(filteredMessages);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showErrorAlert(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteMessage(String messageId) async {
|
Future<void> deleteMessage(String messageId) async {
|
||||||
try {
|
try {
|
||||||
final repository = await ref.read(
|
await _apiClient.delete('/sphere/chat/$_roomId/messages/$messageId');
|
||||||
messageRepositoryProvider(_roomId).future,
|
await receiveMessageDeletion(messageId);
|
||||||
);
|
|
||||||
|
|
||||||
await repository.deleteMessage(messageId);
|
|
||||||
|
|
||||||
// Remove the message from the list
|
|
||||||
final currentMessages = state.value ?? [];
|
|
||||||
final filteredMessages =
|
|
||||||
currentMessages.where((m) => m.id != messageId).toList();
|
|
||||||
|
|
||||||
if (filteredMessages.length != currentMessages.length) {
|
|
||||||
state = AsyncValue.data(filteredMessages);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
}
|
}
|
||||||
@@ -277,13 +554,27 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
|
|
||||||
Future<LocalChatMessage?> fetchMessageById(String messageId) async {
|
Future<LocalChatMessage?> fetchMessageById(String messageId) async {
|
||||||
try {
|
try {
|
||||||
final repository = await ref.read(
|
final localMessage =
|
||||||
messageRepositoryProvider(_roomId).future,
|
await (_database.select(_database.chatMessages)
|
||||||
|
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
|
||||||
|
if (localMessage != null) {
|
||||||
|
return _database.companionToMessage(localMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _apiClient.get(
|
||||||
|
'/sphere/chat/$_roomId/messages/$messageId',
|
||||||
);
|
);
|
||||||
return await repository.getMessageById(messageId);
|
final remoteMessage = SnChatMessage.fromJson(response.data);
|
||||||
} catch (err) {
|
final message = LocalChatMessage.fromRemoteMessage(
|
||||||
showErrorAlert(err);
|
remoteMessage,
|
||||||
return null;
|
MessageStatus.sent,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _database.saveMessage(_database.messageToCompanion(message));
|
||||||
|
return message;
|
||||||
|
} catch (e) {
|
||||||
|
if (e is DioException) return null;
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -746,6 +1037,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
skipError: true,
|
skipError: true,
|
||||||
data:
|
data:
|
||||||
(identity) => MessageItem(
|
(identity) => MessageItem(
|
||||||
|
key: ValueKey(message.id),
|
||||||
message: message,
|
message: message,
|
||||||
isCurrentUser:
|
isCurrentUser:
|
||||||
identity?.id == message.senderId,
|
identity?.id == message.senderId,
|
||||||
|
@@ -6,7 +6,7 @@ part of 'room.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$messagesNotifierHash() => r'afc4d43f4948ec571118cef0321838a6cefc89c0';
|
String _$messagesNotifierHash() => r'3b10c3101404f6528c7a83baa0d39cba1a30f579';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
@@ -7,7 +7,7 @@ part of 'notification.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$notificationUnreadCountNotifierHash() =>
|
String _$notificationUnreadCountNotifierHash() =>
|
||||||
r'd199abf0d16944587e747798399a267a790341f3';
|
r'0763b66bd64e5a9b7c317887e109ab367515dfa4';
|
||||||
|
|
||||||
/// See also [NotificationUnreadCountNotifier].
|
/// See also [NotificationUnreadCountNotifier].
|
||||||
@ProviderFor(NotificationUnreadCountNotifier)
|
@ProviderFor(NotificationUnreadCountNotifier)
|
||||||
|
Reference in New Issue
Block a user