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

@ -1,2 +1,2 @@
export 'active_chat_cubit.dart';
export 'messages_cubit.dart';
export 'single_contact_messages_cubit.dart';

View file

@ -1,225 +0,0 @@
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 _MessageQueueEntry {
_MessageQueueEntry({required this.remoteMessages});
IList<proto.Message> remoteMessages;
}
typedef MessagesState = AsyncValue<IList<proto.Message>>;
class MessagesCubit extends Cubit<MessagesState> {
MessagesCubit(
{required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationRecordKey,
required TypedKey localMessagesRecordKey,
required TypedKey remoteConversationRecordKey,
required TypedKey remoteMessagesRecordKey})
: _activeAccountInfo = activeAccountInfo,
_remoteIdentityPublicKey = remoteIdentityPublicKey,
_remoteMessagesQueue = StreamController(),
super(const AsyncValue.loading()) {
// Local messages key
Future.delayed(
Duration.zero,
() async => _initLocalMessages(
localConversationRecordKey, localMessagesRecordKey));
// Remote messages key
Future.delayed(
Duration.zero,
() async => _initRemoteMessages(
remoteConversationRecordKey, remoteMessagesRecordKey));
// Remote messages listener
Future.delayed(Duration.zero, () async {
await for (final entry in _remoteMessagesQueue.stream) {
await _updateRemoteMessagesStateAsync(entry);
}
});
}
@override
Future<void> close() async {
await _remoteMessagesQueue.close();
await _localSubscription?.cancel();
await _remoteSubscription?.cancel();
await _localMessagesCubit?.close();
await _remoteMessagesCubit?.close();
await super.close();
}
// Open local messages key
Future<void> _initLocalMessages(TypedKey localConversationRecordKey,
TypedKey localMessagesRecordKey) async {
final crypto = await _getMessagesCrypto();
final writer = _activeAccountInfo.conversationWriter;
_localMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openWrite(
localMessagesRecordKey, writer,
parent: localConversationRecordKey, crypto: crypto),
decodeElement: proto.Message.fromBuffer);
_localSubscription =
_localMessagesCubit!.stream.listen(_updateLocalMessagesState);
_updateLocalMessagesState(_localMessagesCubit!.state);
}
// Open remote messages key
Future<void> _initRemoteMessages(TypedKey remoteConversationRecordKey,
TypedKey remoteMessagesRecordKey) async {
// Open remote record key if it is specified
final crypto = await _getMessagesCrypto();
_remoteMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openRead(remoteMessagesRecordKey,
parent: remoteConversationRecordKey, crypto: crypto),
decodeElement: proto.Message.fromBuffer);
_remoteSubscription =
_remoteMessagesCubit!.stream.listen(_updateRemoteMessagesState);
_updateRemoteMessagesState(_remoteMessagesCubit!.state);
}
// Called when the local messages list gets a change
void _updateLocalMessagesState(
BlocBusyState<AsyncValue<IList<proto.Message>>> avmessages) {
// When local messages are updated, pass this
// directly to the messages cubit state
emit(avmessages.state);
}
// 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
_remoteMessagesQueue
.add(_MessageQueueEntry(remoteMessages: remoteMessages));
}
Future<void> _updateRemoteMessagesStateAsync(_MessageQueueEntry entry) async {
final localMessagesCubit = _localMessagesCubit!;
// Updated remote messages need to be merged with the local messages state
await localMessagesCubit.operate((shortArray) async {
// Ensure remoteMessages is sorted by timestamp
final remoteMessages = entry.remoteMessages
.sort((a, b) => a.timestamp.compareTo(b.timestamp));
// dedup? build local timestamp set?
// Existing messages will always be sorted by timestamp so merging is easy
var localMessages = localMessagesCubit.state.state.data!.value;
var pos = 0;
for (final newMessage in remoteMessages) {
var skip = false;
while (pos < localMessages.length) {
final m = localMessages[pos];
pos++;
// If timestamp to insert is less than
// the current position, insert it here
final newTs = Timestamp.fromInt64(newMessage.timestamp);
final curTs = Timestamp.fromInt64(m.timestamp);
final cmp = newTs.compareTo(curTs);
if (cmp < 0) {
break;
} else if (cmp == 0) {
skip = true;
break;
}
}
// Insert at this position
if (!skip) {
// Insert into dht backing array
await shortArray.tryInsertItem(pos, newMessage.writeToBuffer());
// Insert into local copy as well for this operation
localMessages = localMessages.insert(pos, newMessage);
}
}
});
}
// Initialize local messages
static Future<T> initLocalMessages<T>({
required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationKey,
required FutureOr<T> Function(DHTShortArray) callback,
}) async {
final crypto =
await _makeMessagesCrypto(activeAccountInfo, remoteIdentityPublicKey);
final writer = activeAccountInfo.conversationWriter;
return (await DHTShortArray.create(
parent: localConversationKey, crypto: crypto, smplWriter: writer))
.deleteScope((messages) async => await callback(messages));
}
// 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()));
}
Future<DHTRecordCrypto> _getMessagesCrypto() async {
var messagesCrypto = _messagesCrypto;
if (messagesCrypto != null) {
return messagesCrypto;
}
messagesCrypto =
await _makeMessagesCrypto(_activeAccountInfo, _remoteIdentityPublicKey);
_messagesCrypto = messagesCrypto;
return messagesCrypto;
}
static Future<DHTRecordCrypto> _makeMessagesCrypto(
ActiveAccountInfo activeAccountInfo,
TypedKey remoteIdentityPublicKey) async {
final identitySecret = activeAccountInfo.userLogin.identitySecret;
final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind);
final sharedSecret =
await cs.cachedDH(remoteIdentityPublicKey.value, identitySecret.value);
final messagesCrypto = await DHTRecordCryptoPrivate.fromSecret(
identitySecret.kind, sharedSecret);
return messagesCrypto;
}
final ActiveAccountInfo _activeAccountInfo;
final TypedKey _remoteIdentityPublicKey;
DHTShortArrayCubit<proto.Message>? _localMessagesCubit;
DHTShortArrayCubit<proto.Message>? _remoteMessagesCubit;
final StreamController<_MessageQueueEntry> _remoteMessagesQueue;
StreamSubscription<BlocBusyState<AsyncValue<IList<proto.Message>>>>?
_localSubscription;
StreamSubscription<BlocBusyState<AsyncValue<IList<proto.Message>>>>?
_remoteSubscription;
//
DHTRecordCrypto? _messagesCrypto;
}

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;
}

View file

@ -19,8 +19,8 @@ import '../chat.dart';
class ChatComponent extends StatelessWidget {
const ChatComponent._(
{required TypedKey localUserIdentityKey,
required MessagesCubit messagesCubit,
required MessagesState messagesState,
required SingleContactMessagesCubit messagesCubit,
required SingleContactMessagesState messagesState,
required types.User localUser,
required types.User remoteUser,
super.key})
@ -31,8 +31,8 @@ class ChatComponent extends StatelessWidget {
_remoteUser = remoteUser;
final TypedKey _localUserIdentityKey;
final MessagesCubit _messagesCubit;
final MessagesState _messagesState;
final SingleContactMessagesCubit _messagesCubit;
final SingleContactMessagesState _messagesState;
final types.User _localUser;
final types.User _remoteUser;
@ -78,8 +78,8 @@ class ChatComponent extends StatelessWidget {
firstName: editedName);
// Get the messages cubit
final messages = context.select<ActiveConversationMessagesBlocMapCubit,
(MessagesCubit, MessagesState)?>(
final messages = context.select<ActiveSingleContactChatBlocMapCubit,
(SingleContactMessagesCubit, SingleContactMessagesState)?>(
(x) => x.tryOperate(remoteConversationRecordKey,
closure: (cubit) => (cubit, cubit.state)));