veilidchat/lib/chat/cubits/single_contact_messages_cubit.dart

474 lines
16 KiB
Dart
Raw Normal View History

2024-03-24 12:13:27 -04:00
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
2024-06-03 21:20:00 -04:00
import 'package:flutter/foundation.dart';
2024-03-24 12:13:27 -04:00
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto;
2024-06-03 21:20:00 -04:00
import '../../tools/tools.dart';
2024-04-17 21:31:26 -04:00
import '../models/models.dart';
2024-05-31 18:27:50 -04:00
import 'reconciliation/reconciliation.dart';
2024-05-29 16:09:09 -04:00
2024-04-17 21:31:26 -04:00
class RenderStateElement {
RenderStateElement(
{required this.message,
required this.isLocal,
2024-05-29 10:47:43 -04:00
this.reconciledTimestamp,
2024-04-17 21:31:26 -04:00
this.sent = false,
this.sentOffline = false});
MessageSendState? get sendState {
if (!isLocal) {
return null;
}
2024-06-02 11:04:19 -04:00
if (reconciledTimestamp != null) {
2024-04-21 22:16:22 -04:00
return MessageSendState.delivered;
}
2024-06-02 11:04:19 -04:00
if (sent) {
if (!sentOffline) {
return MessageSendState.sent;
} else {
return MessageSendState.sending;
}
2024-04-17 21:31:26 -04:00
}
2024-06-02 11:04:19 -04:00
return null;
2024-04-17 21:31:26 -04:00
}
2024-03-24 12:13:27 -04:00
2024-04-17 21:31:26 -04:00
proto.Message message;
bool isLocal;
2024-05-29 10:47:43 -04:00
Timestamp? reconciledTimestamp;
2024-04-17 21:31:26 -04:00
bool sent;
bool sentOffline;
2024-03-24 12:13:27 -04:00
}
2024-06-06 00:19:07 -04:00
typedef SingleContactMessagesState = AsyncValue<WindowState<MessageState>>;
2024-03-24 12:13:27 -04:00
// Cubit that processes single-contact chats
// Builds the reconciled chat record from the local and remote conversation keys
class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
SingleContactMessagesCubit({
2024-06-18 21:20:06 -04:00
required AccountInfo accountInfo,
2024-03-24 12:13:27 -04:00
required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationRecordKey,
required TypedKey localMessagesRecordKey,
required TypedKey remoteConversationRecordKey,
required TypedKey remoteMessagesRecordKey,
2024-06-18 21:20:06 -04:00
}) : _accountInfo = accountInfo,
2024-03-24 12:13:27 -04:00
_remoteIdentityPublicKey = remoteIdentityPublicKey,
_localConversationRecordKey = localConversationRecordKey,
_localMessagesRecordKey = localMessagesRecordKey,
_remoteConversationRecordKey = remoteConversationRecordKey,
_remoteMessagesRecordKey = remoteMessagesRecordKey,
2024-06-03 21:20:00 -04:00
_commandController = StreamController(),
2024-03-24 12:13:27 -04:00
super(const AsyncValue.loading()) {
// Async Init
_initWait.add(_init);
2024-03-24 12:13:27 -04:00
}
@override
Future<void> close() async {
await _initWait();
2024-06-03 21:20:00 -04:00
await _commandController.close();
await _commandRunnerFut;
2024-06-02 11:04:19 -04:00
await _unsentMessagesQueue.close();
2024-04-17 21:31:26 -04:00
await _sentSubscription?.cancel();
await _rcvdSubscription?.cancel();
await _reconciledSubscription?.cancel();
await _sentMessagesCubit?.close();
await _rcvdMessagesCubit?.close();
await _reconciledMessagesCubit?.close();
2024-06-21 22:44:35 -04:00
// If the local conversation record is gone, then delete the reconciled
// messages table as well
final conversationDead = await DHTRecordPool.instance
.isDeletedRecordKey(_localConversationRecordKey);
if (conversationDead) {
await SingleContactMessagesCubit.cleanupAndDeleteMessages(
localConversationRecordKey: _localConversationRecordKey);
}
2024-03-24 12:13:27 -04:00
await super.close();
}
// Initialize everything
Future<void> _init() async {
2024-06-02 11:04:19 -04:00
_unsentMessagesQueue = PersistentQueue<proto.Message>(
2024-07-04 23:09:37 -04:00
table: 'SingleContactUnsentMessages',
key: _remoteConversationRecordKey.toString(),
fromBuffer: proto.Message.fromBuffer,
closure: _processUnsentMessages,
onError: (e, sp) {
log.error('Exception while processing unsent messages: $e\n$sp\n');
});
2024-04-20 21:24:03 -04:00
2024-03-24 12:13:27 -04:00
// Make crypto
2024-05-28 22:01:50 -04:00
await _initCrypto();
2024-03-24 12:13:27 -04:00
// Reconciled messages key
2024-04-17 21:31:26 -04:00
await _initReconciledMessagesCubit();
2024-03-24 12:13:27 -04:00
// Local messages key
2024-04-17 21:31:26 -04:00
await _initSentMessagesCubit();
2024-03-24 12:13:27 -04:00
// Remote messages key
2024-04-17 21:31:26 -04:00
await _initRcvdMessagesCubit();
2024-06-03 21:20:00 -04:00
// Command execution background process
_commandRunnerFut = Future.delayed(Duration.zero, _commandRunner);
2024-03-24 12:13:27 -04:00
}
// Make crypto
2024-05-28 22:01:50 -04:00
Future<void> _initCrypto() async {
2024-06-18 21:20:06 -04:00
_conversationCrypto =
await _accountInfo.makeConversationCrypto(_remoteIdentityPublicKey);
2024-05-31 18:27:50 -04:00
_senderMessageIntegrity = await MessageIntegrity.create(
2024-06-18 21:20:06 -04:00
author: _accountInfo.identityTypedPublicKey);
2024-03-24 12:13:27 -04:00
}
// Open local messages key
2024-04-17 21:31:26 -04:00
Future<void> _initSentMessagesCubit() async {
2024-06-18 21:20:06 -04:00
final writer = _accountInfo.identityWriter;
2024-03-24 12:13:27 -04:00
2024-05-25 22:46:43 -04:00
_sentMessagesCubit = DHTLogCubit(
open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer,
2024-04-20 21:24:03 -04:00
debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::'
'SentMessages',
parent: _localConversationRecordKey,
2024-05-31 18:27:50 -04:00
crypto: _conversationCrypto),
2024-03-24 12:13:27 -04:00
decodeElement: proto.Message.fromBuffer);
2024-04-17 21:31:26 -04:00
_sentSubscription =
_sentMessagesCubit!.stream.listen(_updateSentMessagesState);
_updateSentMessagesState(_sentMessagesCubit!.state);
2024-03-24 12:13:27 -04:00
}
// Open remote messages key
2024-04-17 21:31:26 -04:00
Future<void> _initRcvdMessagesCubit() async {
2024-05-25 22:46:43 -04:00
_rcvdMessagesCubit = DHTLogCubit(
open: () async => DHTLog.openRead(_remoteMessagesRecordKey,
2024-04-17 21:31:26 -04:00
debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::'
'RcvdMessages',
parent: _remoteConversationRecordKey,
2024-05-31 18:27:50 -04:00
crypto: _conversationCrypto),
2024-03-24 12:13:27 -04:00
decodeElement: proto.Message.fromBuffer);
2024-04-17 21:31:26 -04:00
_rcvdSubscription =
_rcvdMessagesCubit!.stream.listen(_updateRcvdMessagesState);
_updateRcvdMessagesState(_rcvdMessagesCubit!.state);
2024-03-24 12:13:27 -04:00
}
2024-05-28 22:01:50 -04:00
Future<VeilidCrypto> _makeLocalMessagesCrypto() async =>
VeilidCryptoPrivate.fromTypedKey(
2024-06-18 21:20:06 -04:00
_accountInfo.userLogin!.identitySecret, 'tabledb');
2024-05-28 22:01:50 -04:00
2024-03-24 12:13:27 -04:00
// Open reconciled chat record key
2024-04-17 21:31:26 -04:00
Future<void> _initReconciledMessagesCubit() async {
2024-05-31 18:55:44 -04:00
final tableName =
2024-06-02 11:04:19 -04:00
_reconciledMessagesTableDBName(_localConversationRecordKey);
2024-05-27 22:58:37 -04:00
2024-05-28 22:01:50 -04:00
final crypto = await _makeLocalMessagesCrypto();
2024-05-27 22:58:37 -04:00
2024-06-02 11:04:19 -04:00
_reconciledMessagesCubit = TableDBArrayProtobufCubit(
open: () async => TableDBArrayProtobuf.make(
table: tableName,
crypto: crypto,
fromBuffer: proto.ReconciledMessage.fromBuffer),
);
2024-05-30 23:25:47 -04:00
_reconciliation = MessageReconciliation(
output: _reconciledMessagesCubit!,
onError: (e, st) {
emit(AsyncValue.error(e, st));
});
2024-04-17 21:31:26 -04:00
_reconciledSubscription =
_reconciledMessagesCubit!.stream.listen(_updateReconciledMessagesState);
_updateReconciledMessagesState(_reconciledMessagesCubit!.state);
2024-03-24 12:13:27 -04:00
}
2024-04-20 21:24:03 -04:00
////////////////////////////////////////////////////////////////////////////
2024-05-29 10:47:43 -04:00
// Public interface
2024-04-20 21:24:03 -04:00
2024-05-25 22:46:43 -04:00
// Set the tail position of the log for pagination.
// If tail is 0, the end of the log is used.
// If tail is negative, the position is subtracted from the current log
// length.
// If tail is positive, the position is absolute from the head of the log
// If follow is enabled, the tail offset will update when the log changes
Future<void> setWindow(
{int? tail, int? count, bool? follow, bool forceRefresh = false}) async {
await _initWait();
2024-06-06 00:19:07 -04:00
2024-06-07 14:42:04 -04:00
// print('setWindow: tail=$tail count=$count, follow=$follow');
2024-06-06 00:19:07 -04:00
2024-05-25 22:46:43 -04:00
await _reconciledMessagesCubit!.setWindow(
tail: tail, count: count, follow: follow, forceRefresh: forceRefresh);
}
2024-05-29 10:47:43 -04:00
// Set a user-visible 'text' message with possible attachments
void sendTextMessage({required proto.Message_Text messageText}) {
final message = proto.Message()..text = messageText;
_sendMessage(message: message);
}
2024-06-03 21:20:00 -04:00
// Run a chat command
void runCommand(String command) {
final (cmd, rest) = command.splitOnce(' ');
if (kDebugMode) {
if (cmd == '/repeat' && rest != null) {
final (countStr, text) = rest.splitOnce(' ');
final count = int.tryParse(countStr);
if (count != null) {
runCommandRepeat(count, text ?? '');
}
}
}
}
// Run a repeat command
void runCommandRepeat(int count, String text) {
_commandController.sink.add(() async {
for (var i = 0; i < count; i++) {
final protoMessageText = proto.Message_Text()
..text = text.replaceAll(RegExp(r'\$n\b'), i.toString());
final message = proto.Message()..text = protoMessageText;
_sendMessage(message: message);
await Future<void>.delayed(const Duration(milliseconds: 50));
}
});
}
2024-05-25 22:46:43 -04:00
////////////////////////////////////////////////////////////////////////////
2024-05-29 10:47:43 -04:00
// Internal implementation
2024-05-25 22:46:43 -04:00
2024-04-20 21:24:03 -04:00
// Called when the sent messages cubit gets a change
// This will re-render when messages are sent from another machine
2024-05-25 22:46:43 -04:00
void _updateSentMessagesState(DHTLogBusyState<proto.Message> avmessages) {
2024-04-20 21:24:03 -04:00
final sentMessages = avmessages.state.asData?.value;
if (sentMessages == null) {
2024-03-24 12:13:27 -04:00
return;
}
2024-05-28 22:01:50 -04:00
2024-06-15 23:29:15 -04:00
_reconciliation.reconcileMessages(
2024-06-18 21:20:06 -04:00
_accountInfo.identityTypedPublicKey, sentMessages, _sentMessagesCubit!);
2024-06-02 11:04:19 -04:00
// Update the view
_renderState();
2024-04-17 21:31:26 -04:00
}
2024-04-20 21:24:03 -04:00
// Called when the received messages cubit gets a change
2024-05-25 22:46:43 -04:00
void _updateRcvdMessagesState(DHTLogBusyState<proto.Message> avmessages) {
2024-04-20 21:24:03 -04:00
final rcvdMessages = avmessages.state.asData?.value;
if (rcvdMessages == null) {
2024-04-17 21:31:26 -04:00
return;
}
2024-04-20 21:24:03 -04:00
2024-05-30 23:25:47 -04:00
_reconciliation.reconcileMessages(
2024-05-29 16:09:09 -04:00
_remoteIdentityPublicKey, rcvdMessages, _rcvdMessagesCubit!);
2024-03-24 12:13:27 -04:00
}
2024-05-28 22:01:50 -04:00
// Called when the reconciled messages window gets a change
2024-04-17 21:31:26 -04:00
void _updateReconciledMessagesState(
2024-06-02 11:04:19 -04:00
TableDBArrayProtobufBusyState<proto.ReconciledMessage> avmessages) {
2024-04-17 21:31:26 -04:00
// Update the view
_renderState();
2024-03-24 12:13:27 -04:00
}
2024-05-28 22:01:50 -04:00
Future<void> _processMessageToSend(
proto.Message message, proto.Message? previousMessage) async {
// Get the previous message if we don't have one
previousMessage ??= await _sentMessagesCubit!.operate((r) async =>
r.length == 0
? null
: await r.getProtobuf(proto.Message.fromBuffer, r.length - 1));
2024-05-31 18:27:50 -04:00
message.id =
await _senderMessageIntegrity.generateMessageId(previousMessage);
2024-05-28 22:01:50 -04:00
// Now sign it
2024-05-31 18:27:50 -04:00
await _senderMessageIntegrity.signMessage(
2024-06-18 21:20:06 -04:00
message, _accountInfo.identitySecretKey);
2024-05-28 22:01:50 -04:00
}
2024-04-20 21:24:03 -04:00
// Async process to send messages in the background
2024-06-02 11:04:19 -04:00
Future<void> _processUnsentMessages(IList<proto.Message> messages) async {
2024-05-28 22:01:50 -04:00
// Go through and assign ids to all the messages in order
proto.Message? previousMessage;
final processedMessages = messages.toList();
for (final message in processedMessages) {
2024-07-04 23:09:37 -04:00
try {
await _processMessageToSend(message, previousMessage);
previousMessage = message;
} on Exception catch (e) {
log.error('Exception processing unsent message: $e');
}
2024-05-28 22:01:50 -04:00
}
2024-06-21 22:44:35 -04:00
// _sendingMessages = messages;
// _renderState();
2024-07-04 23:09:37 -04:00
try {
await _sentMessagesCubit!.operateAppendEventual((writer) =>
writer.addAll(messages.map((m) => m.writeToBuffer()).toList()));
} on Exception catch (e) {
log.error('Exception appending unsent messages: $e');
}
2024-06-21 22:44:35 -04:00
// _sendingMessages = const IList.empty();
2024-04-20 21:24:03 -04:00
}
2024-04-17 21:31:26 -04:00
// Produce a state for this cubit from the input cubits and queues
void _renderState() {
// Get all reconciled messages
final reconciledMessages =
_reconciledMessagesCubit?.state.state.asData?.value;
// Get all sent messages
final sentMessages = _sentMessagesCubit?.state.state.asData?.value;
2024-06-20 23:00:10 -04:00
//Get all items in the unsent queue
2024-06-21 22:44:35 -04:00
//final unsentMessages = _unsentMessagesQueue.queue;
2024-04-17 21:31:26 -04:00
// If we aren't ready to render a state, say we're loading
2024-04-20 21:24:03 -04:00
if (reconciledMessages == null || sentMessages == null) {
2024-04-17 21:31:26 -04:00
emit(const AsyncLoading());
return;
}
// Generate state for each message
2024-06-02 11:04:19 -04:00
// final reconciledMessagesMap =
// IMap<String, proto.ReconciledMessage>.fromValues(
// keyMapper: (x) => x.content.authorUniqueIdString,
2024-06-20 23:00:10 -04:00
// values: reconciledMessages.windowElements,
2024-06-02 11:04:19 -04:00
// );
2024-04-17 21:31:26 -04:00
final sentMessagesMap =
2024-05-31 18:27:50 -04:00
IMap<String, OnlineElementState<proto.Message>>.fromValues(
keyMapper: (x) => x.value.authorUniqueIdString,
2024-06-02 11:04:19 -04:00
values: sentMessages.window,
2024-04-17 21:31:26 -04:00
);
2024-06-02 11:04:19 -04:00
// final unsentMessagesMap = IMap<String, proto.Message>.fromValues(
// keyMapper: (x) => x.authorUniqueIdString,
// values: unsentMessages,
// );
final renderedElements = <RenderStateElement>[];
2024-06-21 22:44:35 -04:00
final renderedIds = <String>{};
2024-06-03 21:20:00 -04:00
for (final m in reconciledMessages.windowElements) {
2024-06-18 21:20:06 -04:00
final isLocal =
m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey;
2024-06-02 11:04:19 -04:00
final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime);
final sm =
isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null;
final sent = isLocal && sm != null;
final sentOffline = isLocal && sm != null && sm.isOffline;
renderedElements.add(RenderStateElement(
message: m.content,
isLocal: isLocal,
reconciledTimestamp: reconciledTimestamp,
sent: sent,
sentOffline: sentOffline,
));
2024-06-21 22:44:35 -04:00
renderedIds.add(m.content.authorUniqueIdString);
2024-04-17 21:31:26 -04:00
}
2024-06-21 22:44:35 -04:00
// Render in-flight messages at the bottom
// for (final m in _sendingMessages) {
// if (renderedIds.contains(m.authorUniqueIdString)) {
// continue;
// }
// renderedElements.add(RenderStateElement(
// message: m,
// isLocal: true,
// sent: true,
// sentOffline: true,
// ));
// }
2024-04-17 21:31:26 -04:00
// Render the state
2024-06-03 21:20:00 -04:00
final messages = renderedElements
2024-04-17 21:31:26 -04:00
.map((x) => MessageState(
2024-06-02 11:04:19 -04:00
content: x.message,
sentTimestamp: Timestamp.fromInt64(x.message.timestamp),
reconciledTimestamp: x.reconciledTimestamp,
sendState: x.sendState))
2024-04-17 21:31:26 -04:00
.toIList();
// Emit the rendered state
2024-06-06 00:19:07 -04:00
emit(AsyncValue.data(WindowState<MessageState>(
window: messages,
2024-06-03 21:20:00 -04:00
length: reconciledMessages.length,
windowTail: reconciledMessages.windowTail,
windowCount: reconciledMessages.windowCount,
follow: reconciledMessages.follow)));
2024-04-17 21:31:26 -04:00
}
2024-05-29 10:47:43 -04:00
void _sendMessage({required proto.Message message}) {
// Add common fields
// id and signature will get set by _processMessageToSend
message
2024-06-18 21:20:06 -04:00
..author = _accountInfo.identityTypedPublicKey.toProto()
2024-05-29 10:47:43 -04:00
..timestamp = Veilid.instance.now().toInt64();
2024-05-27 18:04:00 -04:00
2024-07-04 23:09:37 -04:00
if ((message.writeToBuffer().lengthInBytes + 256) > 4096) {
throw const FormatException('message is too long');
}
2024-05-29 10:47:43 -04:00
// Put in the queue
2024-06-02 11:04:19 -04:00
_unsentMessagesQueue.addSync(message);
2024-04-17 21:31:26 -04:00
// Update the view
_renderState();
2024-03-24 12:13:27 -04:00
}
2024-06-03 21:20:00 -04:00
Future<void> _commandRunner() async {
await for (final command in _commandController.stream) {
await command();
}
}
2024-05-27 18:04:00 -04:00
/////////////////////////////////////////////////////////////////////////
2024-05-29 10:47:43 -04:00
// Static utility functions
2024-05-27 18:04:00 -04:00
static Future<void> cleanupAndDeleteMessages(
{required TypedKey localConversationRecordKey}) async {
final recmsgdbname =
_reconciledMessagesTableDBName(localConversationRecordKey);
await Veilid.instance.deleteTableDB(recmsgdbname);
}
static String _reconciledMessagesTableDBName(
TypedKey localConversationRecordKey) =>
2024-06-02 11:04:19 -04:00
'msg_${localConversationRecordKey.toString().replaceAll(':', '_')}';
2024-05-27 18:04:00 -04:00
/////////////////////////////////////////////////////////////////////////
final WaitSet<void> _initWait = WaitSet();
2024-06-18 21:20:06 -04:00
late final AccountInfo _accountInfo;
2024-03-24 12:13:27 -04:00
final TypedKey _remoteIdentityPublicKey;
final TypedKey _localConversationRecordKey;
final TypedKey _localMessagesRecordKey;
final TypedKey _remoteConversationRecordKey;
final TypedKey _remoteMessagesRecordKey;
2024-05-31 18:27:50 -04:00
late final VeilidCrypto _conversationCrypto;
late final MessageIntegrity _senderMessageIntegrity;
2024-03-24 12:13:27 -04:00
2024-05-25 22:46:43 -04:00
DHTLogCubit<proto.Message>? _sentMessagesCubit;
DHTLogCubit<proto.Message>? _rcvdMessagesCubit;
2024-06-02 11:04:19 -04:00
TableDBArrayProtobufCubit<proto.ReconciledMessage>? _reconciledMessagesCubit;
2024-03-24 12:13:27 -04:00
2024-05-30 23:25:47 -04:00
late final MessageReconciliation _reconciliation;
2024-06-02 11:04:19 -04:00
late final PersistentQueue<proto.Message> _unsentMessagesQueue;
2024-06-21 22:44:35 -04:00
// IList<proto.Message> _sendingMessages = const IList.empty();
2024-05-25 22:46:43 -04:00
StreamSubscription<DHTLogBusyState<proto.Message>>? _sentSubscription;
StreamSubscription<DHTLogBusyState<proto.Message>>? _rcvdSubscription;
2024-06-02 11:04:19 -04:00
StreamSubscription<TableDBArrayProtobufBusyState<proto.ReconciledMessage>>?
2024-05-28 22:01:50 -04:00
_reconciledSubscription;
2024-06-03 21:20:00 -04:00
final StreamController<Future<void> Function()> _commandController;
late final Future<void> _commandRunnerFut;
2024-03-24 12:13:27 -04:00
}