✨ Chat message sending and receiving
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@ -5,6 +6,7 @@ import 'package:hive/hive.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@ -14,10 +16,14 @@ class ChatMessageController extends ChangeNotifier {
|
||||
|
||||
late final SnNetworkProvider _sn;
|
||||
late final UserDirectoryProvider _ud;
|
||||
late final WebSocketProvider _ws;
|
||||
|
||||
StreamSubscription? _wsSubscription;
|
||||
|
||||
ChatMessageController(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
_ud = context.read<UserDirectoryProvider>();
|
||||
_ws = context.read<WebSocketProvider>();
|
||||
}
|
||||
|
||||
bool isPending = true;
|
||||
@ -29,17 +35,75 @@ class ChatMessageController extends ChangeNotifier {
|
||||
messageTotal != null && messages.length >= messageTotal!;
|
||||
|
||||
String? _boxKey;
|
||||
SnChannel? _channel;
|
||||
SnChannel? channel;
|
||||
SnChannelMember? profile;
|
||||
|
||||
List<SnChatMessage> messages = List.empty(growable: true);
|
||||
/// Messages are the all the messages that in the channel
|
||||
final List<SnChatMessage> messages = List.empty(growable: true);
|
||||
|
||||
/// Unconfirmed messages are the messages that sent by client but did not receive the reply from websocket server.
|
||||
/// Stored as a list of nonce to provide the loading state
|
||||
final List<String> unconfirmedMessages = List.empty(growable: true);
|
||||
|
||||
Box<SnChatMessage>? get _box =>
|
||||
(_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
|
||||
|
||||
Future<void> initialize(SnChannel channel) async {
|
||||
_channel = channel;
|
||||
_boxKey = '$kChatMessageBoxPrefix${channel.id}';
|
||||
Future<void> initialize(SnChannel chan) async {
|
||||
channel = chan;
|
||||
|
||||
// Initialize local data
|
||||
_boxKey = '$kChatMessageBoxPrefix${chan.id}';
|
||||
await Hive.openBox<SnChatMessage>(_boxKey!);
|
||||
|
||||
// Fetch channel profile
|
||||
final resp = await _sn.client.get(
|
||||
'/cgi/im/channels/${chan.keyPath}/me',
|
||||
);
|
||||
profile = SnChannelMember.fromJson(
|
||||
resp.data as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
_wsSubscription = _ws.stream.stream.listen((event) {
|
||||
switch (event.method) {
|
||||
case 'events.new':
|
||||
final payload = SnChatMessage.fromJson(event.payload!);
|
||||
_addMessage(payload);
|
||||
break;
|
||||
case 'calls.new':
|
||||
final payload = SnChatMessage.fromJson(event.payload!);
|
||||
if (payload.channel.id == channel?.id) {
|
||||
// TODO impl call
|
||||
}
|
||||
break;
|
||||
case 'calls.end':
|
||||
final payload = SnChatMessage.fromJson(event.payload!);
|
||||
if (payload.channel.id == channel?.id) {
|
||||
// TODO impl call
|
||||
}
|
||||
break;
|
||||
case 'status.typing':
|
||||
if (event.payload?['channel_id'] != channel?.id) break;
|
||||
final member = SnChannelMember.fromJson(event.payload!['member']);
|
||||
if (member.id == profile?.id) break;
|
||||
// TODO impl typing users
|
||||
// if (!_typingUsers.any((x) => x.id == member.id)) {
|
||||
// setState(() {
|
||||
// _typingUsers.add(member);
|
||||
// });
|
||||
// }
|
||||
// _typingInactiveTimer[member.id]?.cancel();
|
||||
// _typingInactiveTimer[member.id] = Timer(
|
||||
// const Duration(seconds: 3),
|
||||
// () {
|
||||
// setState(() {
|
||||
// _typingUsers.removeWhere((x) => x.id == member.id);
|
||||
// _typingInactiveTimer.remove(member.id);
|
||||
// });
|
||||
// },
|
||||
// );
|
||||
}
|
||||
});
|
||||
|
||||
isPending = false;
|
||||
notifyListeners();
|
||||
}
|
||||
@ -51,14 +115,58 @@ class ChatMessageController extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _addUnconfirmedMessage(SnChatMessage message) async {
|
||||
messages.insert(0, message);
|
||||
unconfirmedMessages.add(message.uuid);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _addMessage(SnChatMessage message) async {
|
||||
messages.add(message);
|
||||
final idx = messages.indexWhere((e) => e.uuid == message.uuid);
|
||||
if (idx != -1) {
|
||||
unconfirmedMessages.remove(message.uuid);
|
||||
messages[idx] = message;
|
||||
} else {
|
||||
messages.insert(0, message);
|
||||
}
|
||||
await _applyMessage(message);
|
||||
notifyListeners();
|
||||
|
||||
if (_box == null) return;
|
||||
await _box!.put(message.id, message);
|
||||
}
|
||||
|
||||
Future<void> _applyMessage(SnChatMessage message) async {
|
||||
if (message.channelId != channel?.id) return;
|
||||
|
||||
switch (message.type) {
|
||||
case 'messages.edit':
|
||||
final body = message.body;
|
||||
if (body['related_event'] != null) {
|
||||
final idx = messages.indexWhere((x) => x.id == body['related_event']);
|
||||
if (idx != -1) {
|
||||
final newBody = message.body;
|
||||
newBody.remove('related_event');
|
||||
messages[idx] = messages[idx].copyWith(
|
||||
body: newBody,
|
||||
updatedAt: message.updatedAt,
|
||||
);
|
||||
if (_box!.containsKey(body['related_event'])) {
|
||||
await _box!.put(body['related_event'], messages[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'messages.delete':
|
||||
final body = message.body;
|
||||
if (body['related_event'] != null) {
|
||||
messages.removeWhere((x) => x.id == body['related_event']);
|
||||
if (_box!.containsKey(body['related_event'])) {
|
||||
await _box!.delete(body['related_event']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendMessage(
|
||||
String type,
|
||||
String content, {
|
||||
@ -66,27 +174,46 @@ class ChatMessageController extends ChangeNotifier {
|
||||
int? relatedId,
|
||||
List<String>? attachments,
|
||||
}) async {
|
||||
if (_channel == null) return;
|
||||
if (channel == null) return;
|
||||
const uuid = Uuid();
|
||||
final nonce = uuid.v4();
|
||||
final resp = await _sn.client.post(
|
||||
'/cgi/im/channels/${_channel!.keyPath}/messages',
|
||||
data: {
|
||||
'type': type,
|
||||
'uuid': nonce,
|
||||
'body': {
|
||||
'text': content,
|
||||
'algorithm': 'plain',
|
||||
if (quoteId != null) 'quote_id': quoteId,
|
||||
if (relatedId != null) 'related_id': relatedId,
|
||||
if (attachments != null) 'attachments': attachments,
|
||||
final body = {
|
||||
'text': content,
|
||||
'algorithm': 'plain',
|
||||
if (quoteId != null) 'quote_id': quoteId,
|
||||
if (relatedId != null) 'related_id': relatedId,
|
||||
if (attachments != null) 'attachments': attachments,
|
||||
};
|
||||
|
||||
// Mock the message locally
|
||||
final message = SnChatMessage(
|
||||
id: 0,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
uuid: nonce,
|
||||
body: body,
|
||||
type: type,
|
||||
channel: channel!,
|
||||
channelId: channel!.id,
|
||||
sender: profile!,
|
||||
senderId: profile!.id,
|
||||
);
|
||||
_addUnconfirmedMessage(message);
|
||||
|
||||
// Send to server
|
||||
try {
|
||||
await _sn.client.post(
|
||||
'/cgi/im/channels/${channel!.keyPath}/messages',
|
||||
data: {
|
||||
'type': type,
|
||||
'uuid': nonce,
|
||||
'body': body,
|
||||
},
|
||||
},
|
||||
);
|
||||
final out = SnChatMessage.fromJson(
|
||||
resp.data['data'] as Map<String, dynamic>,
|
||||
);
|
||||
await _addMessage(out);
|
||||
);
|
||||
} catch (err) {
|
||||
print(err);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the local storage is up to date with the server.
|
||||
@ -100,15 +227,12 @@ class ChatMessageController extends ChangeNotifier {
|
||||
|
||||
try {
|
||||
final resp = await _sn.client.get(
|
||||
'/cgi/im/channels/${_channel!.keyPath}/events/update',
|
||||
'/cgi/im/channels/${channel!.keyPath}/events/update',
|
||||
queryParameters: {
|
||||
'pivot': _box!.values.last.id,
|
||||
},
|
||||
);
|
||||
if (resp.data['up_to_date'] == true) {
|
||||
await loadMessages();
|
||||
return;
|
||||
}
|
||||
if (resp.data['up_to_date'] == true) return;
|
||||
// Only preload the first 100 messages to prevent first time check update cause load to server and waste local storage.
|
||||
// FIXME If the local is missing more than 100 messages, it won't be fetched, this is a problem, we need to fix it.
|
||||
final countToFetch = math.min(resp.data['count'] as int, 100);
|
||||
@ -119,6 +243,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
} catch (err) {
|
||||
rethrow;
|
||||
} finally {
|
||||
await loadMessages();
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
@ -140,7 +265,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
final resp = await _sn.client.get(
|
||||
'/cgi/im/channels/${_channel!.keyPath}/events',
|
||||
'/cgi/im/channels/${channel!.keyPath}/events',
|
||||
queryParameters: {
|
||||
'take': take,
|
||||
'offset': offset,
|
||||
@ -177,7 +302,10 @@ class ChatMessageController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
@override
|
||||
void dispose() {
|
||||
_box?.close();
|
||||
_wsSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user