This commit is contained in:
Christien Rioux 2024-03-24 12:13:27 -04:00
parent 64d4d0cefb
commit 41bb198d92
40 changed files with 1623 additions and 1272 deletions

View file

@ -0,0 +1,273 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:bloc_tools/bloc_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
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;
class _SingleContactMessageQueueEntry {
_SingleContactMessageQueueEntry({this.localMessages, this.remoteMessages});
IList<proto.Message>? localMessages;
IList<proto.Message>? remoteMessages;
}
typedef SingleContactMessagesState = AsyncValue<IList<proto.Message>>;
// Cubit that processes single-contact chats
// Builds the reconciled chat record from the local and remote conversation keys
class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
SingleContactMessagesCubit({
required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationRecordKey,
required TypedKey localMessagesRecordKey,
required TypedKey remoteConversationRecordKey,
required TypedKey remoteMessagesRecordKey,
required OwnedDHTRecordPointer reconciledChatRecord,
}) : _activeAccountInfo = activeAccountInfo,
_remoteIdentityPublicKey = remoteIdentityPublicKey,
_localConversationRecordKey = localConversationRecordKey,
_localMessagesRecordKey = localMessagesRecordKey,
_remoteConversationRecordKey = remoteConversationRecordKey,
_remoteMessagesRecordKey = remoteMessagesRecordKey,
_reconciledChatRecord = reconciledChatRecord,
_messagesUpdateQueue = StreamController(),
super(const AsyncValue.loading()) {
// Async Init
Future.delayed(Duration.zero, _init);
}
@override
Future<void> close() async {
await _messagesUpdateQueue.close();
await _localSubscription?.cancel();
await _remoteSubscription?.cancel();
await _reconciledChatSubscription?.cancel();
await _localMessagesCubit?.close();
await _remoteMessagesCubit?.close();
await _reconciledChatMessagesCubit?.close();
await super.close();
}
// Initialize everything
Future<void> _init() async {
// Make crypto
await _initMessagesCrypto();
// Reconciled messages key
await _initReconciledChatMessages();
// Local messages key
await _initLocalMessages();
// Remote messages key
await _initRemoteMessages();
// Messages listener
Future.delayed(Duration.zero, () async {
await for (final entry in _messagesUpdateQueue.stream) {
await _updateMessagesStateAsync(entry);
}
});
}
// Make crypto
Future<void> _initMessagesCrypto() async {
_messagesCrypto = await _activeAccountInfo
.makeConversationCrypto(_remoteIdentityPublicKey);
}
// Open local messages key
Future<void> _initLocalMessages() async {
final writer = _activeAccountInfo.conversationWriter;
_localMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openWrite(
_localMessagesRecordKey, writer,
parent: _localConversationRecordKey, crypto: _messagesCrypto),
decodeElement: proto.Message.fromBuffer);
_localSubscription =
_localMessagesCubit!.stream.listen(_updateLocalMessagesState);
_updateLocalMessagesState(_localMessagesCubit!.state);
}
// Open remote messages key
Future<void> _initRemoteMessages() async {
_remoteMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openRead(_remoteMessagesRecordKey,
parent: _remoteConversationRecordKey, crypto: _messagesCrypto),
decodeElement: proto.Message.fromBuffer);
_remoteSubscription =
_remoteMessagesCubit!.stream.listen(_updateRemoteMessagesState);
_updateRemoteMessagesState(_remoteMessagesCubit!.state);
}
// Open reconciled chat record key
Future<void> _initReconciledChatMessages() async {
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
_reconciledChatMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openOwned(_reconciledChatRecord,
parent: accountRecordKey),
decodeElement: proto.Message.fromBuffer);
_reconciledChatSubscription =
_reconciledChatMessagesCubit!.stream.listen(_updateReconciledChatState);
_updateReconciledChatState(_reconciledChatMessagesCubit!.state);
}
// Called when the local messages list gets a change
void _updateLocalMessagesState(
BlocBusyState<AsyncValue<IList<proto.Message>>> avmessages) {
final localMessages = avmessages.state.data?.value;
if (localMessages == null) {
return;
}
// Add local messages updates to queue to process asynchronously
_messagesUpdateQueue
.add(_SingleContactMessageQueueEntry(localMessages: localMessages));
}
// Called when the remote messages list gets a change
void _updateRemoteMessagesState(
BlocBusyState<AsyncValue<IList<proto.Message>>> avmessages) {
final remoteMessages = avmessages.state.data?.value;
if (remoteMessages == null) {
return;
}
// Add remote messages updates to queue to process asynchronously
_messagesUpdateQueue
.add(_SingleContactMessageQueueEntry(remoteMessages: remoteMessages));
}
// Called when the reconciled messages list gets a change
void _updateReconciledChatState(
BlocBusyState<AsyncValue<IList<proto.Message>>> avmessages) {
// When reconciled messages are updated, pass this
// directly to the messages cubit state
emit(avmessages.state);
}
Future<void> _mergeMessagesInner(
{required DHTShortArray reconciledMessages,
required IList<proto.Message> messages}) async {
// Ensure remoteMessages is sorted by timestamp
final newMessages = messages
.sort((a, b) => a.timestamp.compareTo(b.timestamp))
.removeDuplicates();
// Existing messages will always be sorted by timestamp so merging is easy
final existingMessages =
_reconciledChatMessagesCubit!.state.state.data!.value.toList();
var ePos = 0;
var nPos = 0;
while (ePos < existingMessages.length && nPos < newMessages.length) {
final existingMessage = existingMessages[ePos];
final newMessage = newMessages[nPos];
// If timestamp to insert is less than
// the current position, insert it here
final newTs = Timestamp.fromInt64(newMessage.timestamp);
final existingTs = Timestamp.fromInt64(existingMessage.timestamp);
final cmp = newTs.compareTo(existingTs);
if (cmp < 0) {
// New message belongs here
// Insert into dht backing array
await reconciledMessages.tryInsertItem(
ePos, newMessage.writeToBuffer());
// Insert into local copy as well for this operation
existingMessages.insert(ePos, newMessage);
// Next message
nPos++;
ePos++;
} else if (cmp == 0) {
// Duplicate, skip
nPos++;
ePos++;
} else if (cmp > 0) {
// New message belongs later
ePos++;
}
}
// If there are any new messages left, append them all
while (nPos < newMessages.length) {
final newMessage = newMessages[nPos];
// Append to dht backing array
await reconciledMessages.tryAddItem(newMessage.writeToBuffer());
// Insert into local copy as well for this operation
existingMessages.add(newMessage);
nPos++;
}
}
Future<void> _updateMessagesStateAsync(
_SingleContactMessageQueueEntry entry) async {
final reconciledChatMessagesCubit = _reconciledChatMessagesCubit!;
// Merge remote and local messages into the reconciled chat log
await reconciledChatMessagesCubit.operate((reconciledMessages) async {
// xxx for now, keep two lists, but can probable simplify this out soon
if (entry.localMessages != null) {
await _mergeMessagesInner(
reconciledMessages: reconciledMessages,
messages: entry.localMessages!);
}
if (entry.remoteMessages != null) {
await _mergeMessagesInner(
reconciledMessages: reconciledMessages,
messages: entry.remoteMessages!);
}
});
}
// Force refresh of messages
Future<void> refresh() async {
final lcc = _localMessagesCubit;
final rcc = _remoteMessagesCubit;
if (lcc != null) {
await lcc.refresh();
}
if (rcc != null) {
await rcc.refresh();
}
}
Future<void> addMessage({required proto.Message message}) async {
await _localMessagesCubit!.operate(
(shortArray) => shortArray.tryAddItem(message.writeToBuffer()));
}
final ActiveAccountInfo _activeAccountInfo;
final TypedKey _remoteIdentityPublicKey;
final TypedKey _localConversationRecordKey;
final TypedKey _localMessagesRecordKey;
final TypedKey _remoteConversationRecordKey;
final TypedKey _remoteMessagesRecordKey;
final OwnedDHTRecordPointer _reconciledChatRecord;
late final DHTRecordCrypto _messagesCrypto;
DHTShortArrayCubit<proto.Message>? _localMessagesCubit;
DHTShortArrayCubit<proto.Message>? _remoteMessagesCubit;
DHTShortArrayCubit<proto.Message>? _reconciledChatMessagesCubit;
final StreamController<_SingleContactMessageQueueEntry> _messagesUpdateQueue;
StreamSubscription<BlocBusyState<AsyncValue<IList<proto.Message>>>>?
_localSubscription;
StreamSubscription<BlocBusyState<AsyncValue<IList<proto.Message>>>>?
_remoteSubscription;
StreamSubscription<BlocBusyState<AsyncValue<IList<proto.Message>>>>?
_reconciledChatSubscription;
}