From 5d89de9bfec6c8021d7602e280d3544fad3dccd2 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 25 May 2024 22:46:43 -0400 Subject: [PATCH 01/19] tabledb array work --- .../models/active_account_info.dart | 6 +- .../cubits/single_contact_messages_cubit.dart | 220 ++- lib/chat/models/message_state.dart | 9 +- lib/chat/models/message_state.freezed.dart | 85 +- lib/chat/models/message_state.g.dart | 6 +- lib/chat_list/cubits/chat_list_cubit.dart | 2 +- .../cubits/contact_invitation_list_cubit.dart | 4 +- .../cubits/contact_request_inbox_cubit.dart | 2 +- lib/contacts/cubits/conversation_cubit.dart | 8 +- lib/proto/extensions.dart | 12 + lib/proto/proto.dart | 2 + lib/proto/veilidchat.pb.dart | 1626 +++++++++++++---- lib/proto/veilidchat.pbenum.dart | 55 +- lib/proto/veilidchat.pbjson.dart | 419 +++-- lib/proto/veilidchat.proto | 385 ++-- lib/theme/views/widget_helpers.dart | 2 +- .../example/integration_test/app_test.dart | 12 + .../integration_test/test_dht_log.dart | 7 +- .../integration_test/test_table_db_array.dart | 134 ++ .../lib/dht_support/src/dht_log/dht_log.dart | 23 +- .../src/dht_log/dht_log_cubit.dart | 8 +- .../dht_support/src/dht_log/dht_log_read.dart | 4 +- ...dht_log_append.dart => dht_log_write.dart} | 42 +- .../dht_support/src/dht_record/barrel.dart | 1 - .../src/dht_record/dht_record.dart | 45 +- .../src/dht_record/dht_record_crypto.dart | 53 - .../src/dht_record/dht_record_pool.dart | 14 +- .../src/dht_short_array/dht_short_array.dart | 15 +- .../dht_short_array_cubit.dart | 7 +- .../dht_short_array/dht_short_array_read.dart | 4 +- .../dht_short_array_write.dart | 10 +- .../src/interfaces/dht_append.dart | 41 + .../src/interfaces/dht_append_truncate.dart | 51 - .../dht_support/src/interfaces/dht_clear.dart | 7 + .../src/interfaces/dht_insert_remove.dart | 60 + .../src/interfaces/dht_random_read.dart | 13 +- .../src/interfaces/dht_random_write.dart | 73 +- .../src/interfaces/dht_truncate.dart | 8 + .../src/interfaces/interfaces.dart | 4 + packages/veilid_support/lib/src/identity.dart | 4 +- .../lib/src/table_db_array.dart | 517 ++++++ .../veilid_support/lib/src/veilid_crypto.dart | 52 + .../veilid_support/lib/veilid_support.dart | 2 + packages/veilid_support/pubspec.lock | 2 +- packages/veilid_support/pubspec.yaml | 1 + 45 files changed, 3022 insertions(+), 1035 deletions(-) create mode 100644 lib/proto/extensions.dart create mode 100644 packages/veilid_support/example/integration_test/test_table_db_array.dart rename packages/veilid_support/lib/dht_support/src/dht_log/{dht_log_append.dart => dht_log_write.dart} (67%) delete mode 100644 packages/veilid_support/lib/dht_support/src/dht_record/dht_record_crypto.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart delete mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_clear.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_truncate.dart create mode 100644 packages/veilid_support/lib/src/table_db_array.dart create mode 100644 packages/veilid_support/lib/src/veilid_crypto.dart diff --git a/lib/account_manager/models/active_account_info.dart b/lib/account_manager/models/active_account_info.dart index 7a1437b..2997434 100644 --- a/lib/account_manager/models/active_account_info.dart +++ b/lib/account_manager/models/active_account_info.dart @@ -24,7 +24,7 @@ class ActiveAccountInfo { return KeyPair(key: identityKey, secret: identitySecret.value); } - Future makeConversationCrypto( + Future makeConversationCrypto( TypedKey remoteIdentityPublicKey) async { final identitySecret = userLogin.identitySecret; final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind); @@ -33,8 +33,8 @@ class ActiveAccountInfo { identitySecret.value, utf8.encode('VeilidChat Conversation')); - final messagesCrypto = await DHTRecordCryptoPrivate.fromSecret( - identitySecret.kind, sharedSecret); + final messagesCrypto = + await VeilidCryptoPrivate.fromSecret(identitySecret.kind, sharedSecret); return messagesCrypto; } diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 72c820e..e018e79 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -120,9 +120,8 @@ class SingleContactMessagesCubit extends Cubit { Future _initSentMessagesCubit() async { final writer = _activeAccountInfo.conversationWriter; - _sentMessagesCubit = DHTShortArrayCubit( - open: () async => DHTShortArray.openWrite( - _localMessagesRecordKey, writer, + _sentMessagesCubit = DHTLogCubit( + open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer, debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::' 'SentMessages', parent: _localConversationRecordKey, @@ -135,8 +134,8 @@ class SingleContactMessagesCubit extends Cubit { // Open remote messages key Future _initRcvdMessagesCubit() async { - _rcvdMessagesCubit = DHTShortArrayCubit( - open: () async => DHTShortArray.openRead(_remoteMessagesRecordKey, + _rcvdMessagesCubit = DHTLogCubit( + open: () async => DHTLog.openRead(_remoteMessagesRecordKey, debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::' 'RcvdMessages', parent: _remoteConversationRecordKey, @@ -152,8 +151,8 @@ class SingleContactMessagesCubit extends Cubit { final accountRecordKey = _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - _reconciledMessagesCubit = DHTShortArrayCubit( - open: () async => DHTShortArray.openOwned(_reconciledChatRecord, + _reconciledMessagesCubit = DHTLogCubit( + open: () async => DHTLog.openOwned(_reconciledChatRecord, debugName: 'SingleContactMessagesCubit::_initReconciledMessagesCubit::' 'ReconciledMessages', @@ -166,10 +165,24 @@ class SingleContactMessagesCubit extends Cubit { //////////////////////////////////////////////////////////////////////////// + // 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 setWindow( + {int? tail, int? count, bool? follow, bool forceRefresh = false}) async { + await _initWait(); + await _reconciledMessagesCubit!.setWindow( + tail: tail, count: count, follow: follow, forceRefresh: forceRefresh); + } + + //////////////////////////////////////////////////////////////////////////// + // Called when the sent messages cubit gets a change // This will re-render when messages are sent from another machine - void _updateSentMessagesState( - DHTShortArrayBusyState avmessages) { + void _updateSentMessagesState(DHTLogBusyState avmessages) { final sentMessages = avmessages.state.asData?.value; if (sentMessages == null) { return; @@ -182,27 +195,52 @@ class SingleContactMessagesCubit extends Cubit { } // Called when the received messages cubit gets a change - void _updateRcvdMessagesState( - DHTShortArrayBusyState avmessages) { + void _updateRcvdMessagesState(DHTLogBusyState avmessages) { final rcvdMessages = avmessages.state.asData?.value; if (rcvdMessages == null) { return; } - // Add remote messages updates to queue to process asynchronously - // Ignore offline state because remote messages are always fully delivered - // This may happen once per client but should be idempotent - _unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value)); + singleFuture(_rcvdMessagesCubit!, () async { + // Get the timestamp of our most recent reconciled message + final lastReconciledMessageTs = + await _reconciledMessagesCubit!.operate((r) async { + final len = r.length; + if (len == 0) { + return null; + } else { + final lastMessage = + await r.getItemProtobuf(proto.Message.fromBuffer, len - 1); + if (lastMessage == null) { + throw StateError('should have gotten last message'); + } + return lastMessage.timestamp; + } + }); - // Update the view - _renderState(); + // Find oldest message we have not yet reconciled + + // // Go through all the ones from the cubit state first since we've already + // // gotten them from the DHT + // for (var rn = rcvdMessages.elements.length; rn >= 0; rn--) { + // // + // } + + // // Add remote messages updates to queue to process asynchronously + // // Ignore offline state because remote messages are always fully delivered + // // This may happen once per client but should be idempotent + // _unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value)); + + // Update the view + _renderState(); + }); } // Called when the reconciled messages list gets a change // This can happen when multiple clients for the same identity are // reading and reconciling the same remote chat void _updateReconciledMessagesState( - DHTShortArrayBusyState avmessages) { + DHTLogBusyState avmessages) { // Update the view _renderState(); } @@ -210,85 +248,85 @@ class SingleContactMessagesCubit extends Cubit { // Async process to reconcile messages sent or received in the background Future _processUnreconciledMessages( IList messages) async { - await _reconciledMessagesCubit! - .operateWrite((reconciledMessagesWriter) async { - await _reconcileMessagesInner( - reconciledMessagesWriter: reconciledMessagesWriter, - messages: messages); - }); + // await _reconciledMessagesCubit! + // .operateAppendEventual((reconciledMessagesWriter) async { + // await _reconcileMessagesInner( + // reconciledMessagesWriter: reconciledMessagesWriter, + // messages: messages); + // }); } // Async process to send messages in the background Future _processSendingMessages(IList messages) async { - for (final message in messages) { - await _sentMessagesCubit!.operateWriteEventual( - (writer) => writer.tryAddItem(message.writeToBuffer())); - } + await _sentMessagesCubit!.operateAppendEventual((writer) => + writer.tryAddItems(messages.map((m) => m.writeToBuffer()).toList())); } Future _reconcileMessagesInner( - {required DHTRandomReadWrite reconciledMessagesWriter, + {required DHTLogWriteOperations reconciledMessagesWriter, required IList messages}) async { - // Ensure remoteMessages is sorted by timestamp - final newMessages = messages - .sort((a, b) => a.timestamp.compareTo(b.timestamp)) - .removeDuplicates(); + // // 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 = await reconciledMessagesWriter - .getItemRangeProtobuf(proto.Message.fromBuffer, 0); - if (existingMessages == null) { - throw Exception( - 'Could not load existing reconciled messages at this time'); - } + // // Existing messages will always be sorted by timestamp so merging is easy + // final existingMessages = await reconciledMessagesWriter + // .getItemRangeProtobuf(proto.Message.fromBuffer, 0); + // if (existingMessages == null) { + // throw Exception( + // 'Could not load existing reconciled messages at this time'); + // } - var ePos = 0; - var nPos = 0; - while (ePos < existingMessages.length && nPos < newMessages.length) { - final existingMessage = existingMessages[ePos]; - final newMessage = newMessages[nPos]; + // 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 + // // 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 reconciledMessagesWriter.tryInsertItem( - ePos, newMessage.writeToBuffer()); - // Insert into local copy as well for this operation - existingMessages.insert(ePos, newMessage); + // // Insert into dht backing array + // await reconciledMessagesWriter.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]; + // // 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 reconciledMessagesWriter.tryAddItem(newMessage.writeToBuffer()); - // Insert into local copy as well for this operation - existingMessages.add(newMessage); + // // Append to dht backing array + // await reconciledMessagesWriter.tryAddItem(newMessage.writeToBuffer()); + // // Insert into local copy as well for this operation + // existingMessages.add(newMessage); - nPos++; - } + // nPos++; + // } } // Produce a state for this cubit from the input cubits and queues void _renderState() { + // xxx move into a singlefuture + // Get all reconciled messages final reconciledMessages = _reconciledMessagesCubit?.state.state.asData?.value; @@ -307,14 +345,14 @@ class SingleContactMessagesCubit extends Cubit { // Generate state for each message final sentMessagesMap = - IMap>.fromValues( + IMap>.fromValues( keyMapper: (x) => x.value.timestamp, - values: sentMessages, + values: sentMessages.elements, ); final reconciledMessagesMap = - IMap>.fromValues( + IMap>.fromValues( keyMapper: (x) => x.value.timestamp, - values: reconciledMessages, + values: reconciledMessages.elements, ); final sendingMessagesMap = IMap.fromValues( keyMapper: (x) => x.timestamp, @@ -372,9 +410,8 @@ class SingleContactMessagesCubit extends Cubit { .sort((x, y) => x.key.compareTo(y.key)); final renderedState = messageKeys .map((x) => MessageState( - author: x.value.message.author.toVeilid(), + content: x.value.message, timestamp: Timestamp.fromInt64(x.key), - text: x.value.message.text, sendState: x.value.sendState)) .toIList(); @@ -400,17 +437,16 @@ class SingleContactMessagesCubit extends Cubit { final TypedKey _remoteMessagesRecordKey; final OwnedDHTRecordPointer _reconciledChatRecord; - late final DHTRecordCrypto _messagesCrypto; + late final VeilidCrypto _messagesCrypto; - DHTShortArrayCubit? _sentMessagesCubit; - DHTShortArrayCubit? _rcvdMessagesCubit; - DHTShortArrayCubit? _reconciledMessagesCubit; + DHTLogCubit? _sentMessagesCubit; + DHTLogCubit? _rcvdMessagesCubit; + DHTLogCubit? _reconciledMessagesCubit; late final PersistentQueue _unreconciledMessagesQueue; late final PersistentQueue _sendingMessagesQueue; - StreamSubscription>? _sentSubscription; - StreamSubscription>? _rcvdSubscription; - StreamSubscription>? - _reconciledSubscription; + StreamSubscription>? _sentSubscription; + StreamSubscription>? _rcvdSubscription; + StreamSubscription>? _reconciledSubscription; } diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart index c4c6ca5..8618054 100644 --- a/lib/chat/models/message_state.dart +++ b/lib/chat/models/message_state.dart @@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../proto/proto.dart' as proto; + part 'message_state.freezed.dart'; part 'message_state.g.dart'; @@ -23,9 +25,12 @@ enum MessageSendState { @freezed class MessageState with _$MessageState { const factory MessageState({ - required TypedKey author, + // Content of the message + @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + required proto.Message content, + // Received or delivered timestamp required Timestamp timestamp, - required String text, + // The state of the mssage required MessageSendState? sendState, }) = _MessageState; diff --git a/lib/chat/models/message_state.freezed.dart b/lib/chat/models/message_state.freezed.dart index 3d76551..ec8195b 100644 --- a/lib/chat/models/message_state.freezed.dart +++ b/lib/chat/models/message_state.freezed.dart @@ -20,9 +20,12 @@ MessageState _$MessageStateFromJson(Map json) { /// @nodoc mixin _$MessageState { - Typed get author => throw _privateConstructorUsedError; - Timestamp get timestamp => throw _privateConstructorUsedError; - String get text => throw _privateConstructorUsedError; +// Content of the message + @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + proto.Message get content => + throw _privateConstructorUsedError; // Received or delivered timestamp + Timestamp get timestamp => + throw _privateConstructorUsedError; // The state of the mssage MessageSendState? get sendState => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @@ -38,9 +41,9 @@ abstract class $MessageStateCopyWith<$Res> { _$MessageStateCopyWithImpl<$Res, MessageState>; @useResult $Res call( - {Typed author, + {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + proto.Message content, Timestamp timestamp, - String text, MessageSendState? sendState}); } @@ -57,24 +60,19 @@ class _$MessageStateCopyWithImpl<$Res, $Val extends MessageState> @pragma('vm:prefer-inline') @override $Res call({ - Object? author = null, + Object? content = null, Object? timestamp = null, - Object? text = null, Object? sendState = freezed, }) { return _then(_value.copyWith( - author: null == author - ? _value.author - : author // ignore: cast_nullable_to_non_nullable - as Typed, + content: null == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as proto.Message, timestamp: null == timestamp ? _value.timestamp : timestamp // ignore: cast_nullable_to_non_nullable as Timestamp, - text: null == text - ? _value.text - : text // ignore: cast_nullable_to_non_nullable - as String, sendState: freezed == sendState ? _value.sendState : sendState // ignore: cast_nullable_to_non_nullable @@ -92,9 +90,9 @@ abstract class _$$MessageStateImplCopyWith<$Res> @override @useResult $Res call( - {Typed author, + {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + proto.Message content, Timestamp timestamp, - String text, MessageSendState? sendState}); } @@ -109,24 +107,19 @@ class __$$MessageStateImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? author = null, + Object? content = null, Object? timestamp = null, - Object? text = null, Object? sendState = freezed, }) { return _then(_$MessageStateImpl( - author: null == author - ? _value.author - : author // ignore: cast_nullable_to_non_nullable - as Typed, + content: null == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as proto.Message, timestamp: null == timestamp ? _value.timestamp : timestamp // ignore: cast_nullable_to_non_nullable as Timestamp, - text: null == text - ? _value.text - : text // ignore: cast_nullable_to_non_nullable - as String, sendState: freezed == sendState ? _value.sendState : sendState // ignore: cast_nullable_to_non_nullable @@ -139,26 +132,28 @@ class __$$MessageStateImplCopyWithImpl<$Res> @JsonSerializable() class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { const _$MessageStateImpl( - {required this.author, + {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + required this.content, required this.timestamp, - required this.text, required this.sendState}); factory _$MessageStateImpl.fromJson(Map json) => _$$MessageStateImplFromJson(json); +// Content of the message @override - final Typed author; + @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + final proto.Message content; +// Received or delivered timestamp @override final Timestamp timestamp; - @override - final String text; +// The state of the mssage @override final MessageSendState? sendState; @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'MessageState(author: $author, timestamp: $timestamp, text: $text, sendState: $sendState)'; + return 'MessageState(content: $content, timestamp: $timestamp, sendState: $sendState)'; } @override @@ -166,9 +161,8 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('type', 'MessageState')) - ..add(DiagnosticsProperty('author', author)) + ..add(DiagnosticsProperty('content', content)) ..add(DiagnosticsProperty('timestamp', timestamp)) - ..add(DiagnosticsProperty('text', text)) ..add(DiagnosticsProperty('sendState', sendState)); } @@ -177,18 +171,16 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { return identical(this, other) || (other.runtimeType == runtimeType && other is _$MessageStateImpl && - (identical(other.author, author) || other.author == author) && + (identical(other.content, content) || other.content == content) && (identical(other.timestamp, timestamp) || other.timestamp == timestamp) && - (identical(other.text, text) || other.text == text) && (identical(other.sendState, sendState) || other.sendState == sendState)); } @JsonKey(ignore: true) @override - int get hashCode => - Object.hash(runtimeType, author, timestamp, text, sendState); + int get hashCode => Object.hash(runtimeType, content, timestamp, sendState); @JsonKey(ignore: true) @override @@ -206,21 +198,20 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { abstract class _MessageState implements MessageState { const factory _MessageState( - {required final Typed author, + {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + required final proto.Message content, required final Timestamp timestamp, - required final String text, required final MessageSendState? sendState}) = _$MessageStateImpl; factory _MessageState.fromJson(Map json) = _$MessageStateImpl.fromJson; - @override - Typed get author; - @override + @override // Content of the message + @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + proto.Message get content; + @override // Received or delivered timestamp Timestamp get timestamp; - @override - String get text; - @override + @override // The state of the mssage MessageSendState? get sendState; @override @JsonKey(ignore: true) diff --git a/lib/chat/models/message_state.g.dart b/lib/chat/models/message_state.g.dart index 5324b93..3471720 100644 --- a/lib/chat/models/message_state.g.dart +++ b/lib/chat/models/message_state.g.dart @@ -8,9 +8,8 @@ part of 'message_state.dart'; _$MessageStateImpl _$$MessageStateImplFromJson(Map json) => _$MessageStateImpl( - author: Typed.fromJson(json['author']), + content: messageFromJson(json['content'] as Map), timestamp: Timestamp.fromJson(json['timestamp']), - text: json['text'] as String, sendState: json['send_state'] == null ? null : MessageSendState.fromJson(json['send_state']), @@ -18,8 +17,7 @@ _$MessageStateImpl _$$MessageStateImplFromJson(Map json) => Map _$$MessageStateImplToJson(_$MessageStateImpl instance) => { - 'author': instance.author.toJson(), + 'content': messageToJson(instance.content), 'timestamp': instance.timestamp.toJson(), - 'text': instance.text, 'send_state': instance.sendState?.toJson(), }; diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 9204a0a..d04b008 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -65,7 +65,7 @@ class ChatListCubit extends DHTShortArrayCubit .userLogin.accountRecordInfo.accountRecord.recordKey; // Make a record that can store the reconciled version of the chat - final reconciledChatRecord = await (await DHTShortArray.create( + final reconciledChatRecord = await (await DHTLog.create( debugName: 'ChatListCubit::getOrCreateChatSingleContact::ReconciledChat', parent: accountRecordKey)) diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index afe91c0..b76eaee 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -121,7 +121,7 @@ class ContactInvitationListCubit schema: DHTSchema.smpl(oCnt: 1, members: [ DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key) ]), - crypto: const DHTRecordCryptoPublic())) + crypto: const VeilidCryptoPublic())) .deleteScope((contactRequestInbox) async { // Store ContactRequest in owner subkey await contactRequestInbox.eventualWriteProtobuf(creq); @@ -129,7 +129,7 @@ class ContactInvitationListCubit await contactRequestInbox.eventualWriteBytes(Uint8List(0), subkey: 1, writer: contactRequestWriter, - crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair( + crypto: await VeilidCryptoPrivate.fromTypedKeyPair( TypedKeyPair.fromKeyPair( contactRequestInbox.key.kind, contactRequestWriter))); diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart index a376881..a4d0b8a 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -37,7 +37,7 @@ class ContactRequestInboxCubit return pool.openRecordRead(recordKey, debugName: 'ContactRequestInboxCubit::_open::' 'ContactRequestInbox', - crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), + crypto: await VeilidCryptoPrivate.fromTypedKeyPair(writer), parent: accountRecordKey, defaultSubkey: 1); } diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index b4d8ee9..ef7e6ec 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -285,13 +285,13 @@ class ConversationCubit extends Cubit> { required ActiveAccountInfo activeAccountInfo, required TypedKey remoteIdentityPublicKey, required TypedKey localConversationKey, - required FutureOr Function(DHTShortArray) callback, + required FutureOr Function(DHTLog) callback, }) async { final crypto = await activeAccountInfo.makeConversationCrypto(remoteIdentityPublicKey); final writer = activeAccountInfo.conversationWriter; - return (await DHTShortArray.create( + return (await DHTLog.create( debugName: 'ConversationCubit::initLocalMessages::LocalMessages', parent: localConversationKey, crypto: crypto, @@ -327,7 +327,7 @@ class ConversationCubit extends Cubit> { return update; } - Future _cachedConversationCrypto() async { + Future _cachedConversationCrypto() async { var conversationCrypto = _conversationCrypto; if (conversationCrypto != null) { return conversationCrypto; @@ -350,6 +350,6 @@ class ConversationCubit extends Cubit> { ConversationState _incrementalState = const ConversationState( localConversation: null, remoteConversation: null); // - DHTRecordCrypto? _conversationCrypto; + VeilidCrypto? _conversationCrypto; final WaitSet _initWait = WaitSet(); } diff --git a/lib/proto/extensions.dart b/lib/proto/extensions.dart new file mode 100644 index 0000000..64fabf6 --- /dev/null +++ b/lib/proto/extensions.dart @@ -0,0 +1,12 @@ +import 'proto.dart' as proto; + +proto.Message messageFromJson(Map j) => + proto.Message.create()..mergeFromJsonMap(j); + +Map messageToJson(proto.Message m) => m.writeToJsonMap(); + +proto.ReconciledMessage reconciledMessageFromJson(Map j) => + proto.ReconciledMessage.create()..mergeFromJsonMap(j); + +Map reconciledMessageToJson(proto.ReconciledMessage m) => + m.writeToJsonMap(); diff --git a/lib/proto/proto.dart b/lib/proto/proto.dart index cfccda3..6ad8432 100644 --- a/lib/proto/proto.dart +++ b/lib/proto/proto.dart @@ -1,5 +1,7 @@ export 'package:veilid_support/dht_support/proto/proto.dart'; export 'package:veilid_support/proto/proto.dart'; + +export 'extensions.dart'; export 'veilidchat.pb.dart'; export 'veilidchat.pbenum.dart'; export 'veilidchat.pbjson.dart'; diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 1503c57..164c117 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -14,24 +14,31 @@ import 'dart:core' as $core; import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; -import 'package:veilid_support/proto/dht.pb.dart' as $0; -import 'package:veilid_support/proto/veilid.pb.dart' as $1; +import 'package:veilid_support/proto/dht.pb.dart' as $1; +import 'package:veilid_support/proto/veilid.pb.dart' as $0; import 'veilidchat.pbenum.dart'; export 'veilidchat.pbenum.dart'; +enum Attachment_Kind { + media, + notSet +} + class Attachment extends $pb.GeneratedMessage { factory Attachment() => create(); Attachment._() : super(); factory Attachment.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Attachment.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + static const $core.Map<$core.int, Attachment_Kind> _Attachment_KindByTag = { + 1 : Attachment_Kind.media, + 0 : Attachment_Kind.notSet + }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Attachment', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..e(1, _omitFieldNames ? '' : 'kind', $pb.PbFieldType.OE, defaultOrMaker: AttachmentKind.ATTACHMENT_KIND_UNSPECIFIED, valueOf: AttachmentKind.valueOf, enumValues: AttachmentKind.values) - ..aOS(2, _omitFieldNames ? '' : 'mime') - ..aOS(3, _omitFieldNames ? '' : 'name') - ..aOM<$0.DataReference>(4, _omitFieldNames ? '' : 'content', subBuilder: $0.DataReference.create) - ..aOM<$1.Signature>(5, _omitFieldNames ? '' : 'signature', subBuilder: $1.Signature.create) + ..oo(0, [1]) + ..aOM(1, _omitFieldNames ? '' : 'media', subBuilder: AttachmentMedia.create) + ..aOM<$0.Signature>(2, _omitFieldNames ? '' : 'signature', subBuilder: $0.Signature.create) ..hasRequiredFields = false ; @@ -56,54 +63,684 @@ class Attachment extends $pb.GeneratedMessage { static Attachment getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Attachment? _defaultInstance; + Attachment_Kind whichKind() => _Attachment_KindByTag[$_whichOneof(0)]!; + void clearKind() => clearField($_whichOneof(0)); + @$pb.TagNumber(1) - AttachmentKind get kind => $_getN(0); + AttachmentMedia get media => $_getN(0); @$pb.TagNumber(1) - set kind(AttachmentKind v) { setField(1, v); } + set media(AttachmentMedia v) { setField(1, v); } @$pb.TagNumber(1) - $core.bool hasKind() => $_has(0); + $core.bool hasMedia() => $_has(0); @$pb.TagNumber(1) - void clearKind() => clearField(1); + void clearMedia() => clearField(1); + @$pb.TagNumber(1) + AttachmentMedia ensureMedia() => $_ensure(0); @$pb.TagNumber(2) - $core.String get mime => $_getSZ(1); + $0.Signature get signature => $_getN(1); @$pb.TagNumber(2) - set mime($core.String v) { $_setString(1, v); } + set signature($0.Signature v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasMime() => $_has(1); + $core.bool hasSignature() => $_has(1); @$pb.TagNumber(2) - void clearMime() => clearField(2); + void clearSignature() => clearField(2); + @$pb.TagNumber(2) + $0.Signature ensureSignature() => $_ensure(1); +} + +class AttachmentMedia extends $pb.GeneratedMessage { + factory AttachmentMedia() => create(); + AttachmentMedia._() : super(); + factory AttachmentMedia.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AttachmentMedia.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AttachmentMedia', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'mime') + ..aOS(2, _omitFieldNames ? '' : 'name') + ..aOM<$1.DataReference>(3, _omitFieldNames ? '' : 'content', subBuilder: $1.DataReference.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AttachmentMedia clone() => AttachmentMedia()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AttachmentMedia copyWith(void Function(AttachmentMedia) updates) => super.copyWith((message) => updates(message as AttachmentMedia)) as AttachmentMedia; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AttachmentMedia create() => AttachmentMedia._(); + AttachmentMedia createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AttachmentMedia getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AttachmentMedia? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get mime => $_getSZ(0); + @$pb.TagNumber(1) + set mime($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasMime() => $_has(0); + @$pb.TagNumber(1) + void clearMime() => clearField(1); + + @$pb.TagNumber(2) + $core.String get name => $_getSZ(1); + @$pb.TagNumber(2) + set name($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasName() => $_has(1); + @$pb.TagNumber(2) + void clearName() => clearField(2); @$pb.TagNumber(3) - $core.String get name => $_getSZ(2); + $1.DataReference get content => $_getN(2); @$pb.TagNumber(3) - set name($core.String v) { $_setString(2, v); } + set content($1.DataReference v) { setField(3, v); } @$pb.TagNumber(3) - $core.bool hasName() => $_has(2); + $core.bool hasContent() => $_has(2); @$pb.TagNumber(3) - void clearName() => clearField(3); + void clearContent() => clearField(3); + @$pb.TagNumber(3) + $1.DataReference ensureContent() => $_ensure(2); +} + +class Permissions extends $pb.GeneratedMessage { + factory Permissions() => create(); + Permissions._() : super(); + factory Permissions.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Permissions.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Permissions', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'canAddMembers', $pb.PbFieldType.OE, defaultOrMaker: Scope.WATCHERS, valueOf: Scope.valueOf, enumValues: Scope.values) + ..e(2, _omitFieldNames ? '' : 'canEditInfo', $pb.PbFieldType.OE, defaultOrMaker: Scope.WATCHERS, valueOf: Scope.valueOf, enumValues: Scope.values) + ..aOB(3, _omitFieldNames ? '' : 'moderated') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Permissions clone() => Permissions()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Permissions copyWith(void Function(Permissions) updates) => super.copyWith((message) => updates(message as Permissions)) as Permissions; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Permissions create() => Permissions._(); + Permissions createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Permissions getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Permissions? _defaultInstance; + + @$pb.TagNumber(1) + Scope get canAddMembers => $_getN(0); + @$pb.TagNumber(1) + set canAddMembers(Scope v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasCanAddMembers() => $_has(0); + @$pb.TagNumber(1) + void clearCanAddMembers() => clearField(1); + + @$pb.TagNumber(2) + Scope get canEditInfo => $_getN(1); + @$pb.TagNumber(2) + set canEditInfo(Scope v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasCanEditInfo() => $_has(1); + @$pb.TagNumber(2) + void clearCanEditInfo() => clearField(2); + + @$pb.TagNumber(3) + $core.bool get moderated => $_getBF(2); + @$pb.TagNumber(3) + set moderated($core.bool v) { $_setBool(2, v); } + @$pb.TagNumber(3) + $core.bool hasModerated() => $_has(2); + @$pb.TagNumber(3) + void clearModerated() => clearField(3); +} + +class Membership extends $pb.GeneratedMessage { + factory Membership() => create(); + Membership._() : super(); + factory Membership.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Membership.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Membership', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..pc<$0.TypedKey>(1, _omitFieldNames ? '' : 'watchers', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..pc<$0.TypedKey>(2, _omitFieldNames ? '' : 'moderated', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..pc<$0.TypedKey>(3, _omitFieldNames ? '' : 'talkers', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..pc<$0.TypedKey>(4, _omitFieldNames ? '' : 'moderators', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..pc<$0.TypedKey>(5, _omitFieldNames ? '' : 'admins', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Membership clone() => Membership()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Membership copyWith(void Function(Membership) updates) => super.copyWith((message) => updates(message as Membership)) as Membership; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Membership create() => Membership._(); + Membership createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Membership getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Membership? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$0.TypedKey> get watchers => $_getList(0); + + @$pb.TagNumber(2) + $core.List<$0.TypedKey> get moderated => $_getList(1); + + @$pb.TagNumber(3) + $core.List<$0.TypedKey> get talkers => $_getList(2); @$pb.TagNumber(4) - $0.DataReference get content => $_getN(3); - @$pb.TagNumber(4) - set content($0.DataReference v) { setField(4, v); } - @$pb.TagNumber(4) - $core.bool hasContent() => $_has(3); - @$pb.TagNumber(4) - void clearContent() => clearField(4); - @$pb.TagNumber(4) - $0.DataReference ensureContent() => $_ensure(3); + $core.List<$0.TypedKey> get moderators => $_getList(3); @$pb.TagNumber(5) - $1.Signature get signature => $_getN(4); + $core.List<$0.TypedKey> get admins => $_getList(4); +} + +class ChatSettings extends $pb.GeneratedMessage { + factory ChatSettings() => create(); + ChatSettings._() : super(); + factory ChatSettings.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ChatSettings.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatSettings', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'title') + ..aOS(2, _omitFieldNames ? '' : 'description') + ..aOM<$1.DataReference>(3, _omitFieldNames ? '' : 'icon', subBuilder: $1.DataReference.create) + ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'defaultExpiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ChatSettings clone() => ChatSettings()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ChatSettings copyWith(void Function(ChatSettings) updates) => super.copyWith((message) => updates(message as ChatSettings)) as ChatSettings; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ChatSettings create() => ChatSettings._(); + ChatSettings createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ChatSettings getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ChatSettings? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get title => $_getSZ(0); + @$pb.TagNumber(1) + set title($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasTitle() => $_has(0); + @$pb.TagNumber(1) + void clearTitle() => clearField(1); + + @$pb.TagNumber(2) + $core.String get description => $_getSZ(1); + @$pb.TagNumber(2) + set description($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasDescription() => $_has(1); + @$pb.TagNumber(2) + void clearDescription() => clearField(2); + + @$pb.TagNumber(3) + $1.DataReference get icon => $_getN(2); + @$pb.TagNumber(3) + set icon($1.DataReference v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasIcon() => $_has(2); + @$pb.TagNumber(3) + void clearIcon() => clearField(3); + @$pb.TagNumber(3) + $1.DataReference ensureIcon() => $_ensure(2); + + @$pb.TagNumber(4) + $fixnum.Int64 get defaultExpiration => $_getI64(3); + @$pb.TagNumber(4) + set defaultExpiration($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasDefaultExpiration() => $_has(3); + @$pb.TagNumber(4) + void clearDefaultExpiration() => clearField(4); +} + +class Message_Text extends $pb.GeneratedMessage { + factory Message_Text() => create(); + Message_Text._() : super(); + factory Message_Text.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_Text.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.Text', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'text') + ..aOS(2, _omitFieldNames ? '' : 'topic') + ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'replyId', subBuilder: $0.TypedKey.create) + ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'viewLimit', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..pc(6, _omitFieldNames ? '' : 'attachments', $pb.PbFieldType.PM, subBuilder: Attachment.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Message_Text clone() => Message_Text()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message_Text copyWith(void Function(Message_Text) updates) => super.copyWith((message) => updates(message as Message_Text)) as Message_Text; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_Text create() => Message_Text._(); + Message_Text createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_Text getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_Text? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get text => $_getSZ(0); + @$pb.TagNumber(1) + set text($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasText() => $_has(0); + @$pb.TagNumber(1) + void clearText() => clearField(1); + + @$pb.TagNumber(2) + $core.String get topic => $_getSZ(1); + @$pb.TagNumber(2) + set topic($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasTopic() => $_has(1); + @$pb.TagNumber(2) + void clearTopic() => clearField(2); + + @$pb.TagNumber(3) + $0.TypedKey get replyId => $_getN(2); + @$pb.TagNumber(3) + set replyId($0.TypedKey v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasReplyId() => $_has(2); + @$pb.TagNumber(3) + void clearReplyId() => clearField(3); + @$pb.TagNumber(3) + $0.TypedKey ensureReplyId() => $_ensure(2); + + @$pb.TagNumber(4) + $fixnum.Int64 get expiration => $_getI64(3); + @$pb.TagNumber(4) + set expiration($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasExpiration() => $_has(3); + @$pb.TagNumber(4) + void clearExpiration() => clearField(4); + @$pb.TagNumber(5) - set signature($1.Signature v) { setField(5, v); } + $fixnum.Int64 get viewLimit => $_getI64(4); @$pb.TagNumber(5) - $core.bool hasSignature() => $_has(4); + set viewLimit($fixnum.Int64 v) { $_setInt64(4, v); } @$pb.TagNumber(5) - void clearSignature() => clearField(5); + $core.bool hasViewLimit() => $_has(4); @$pb.TagNumber(5) - $1.Signature ensureSignature() => $_ensure(4); + void clearViewLimit() => clearField(5); + + @$pb.TagNumber(6) + $core.List get attachments => $_getList(5); +} + +class Message_Secret extends $pb.GeneratedMessage { + factory Message_Secret() => create(); + Message_Secret._() : super(); + factory Message_Secret.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_Secret.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.Secret', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'ciphertext', $pb.PbFieldType.OY) + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Message_Secret clone() => Message_Secret()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message_Secret copyWith(void Function(Message_Secret) updates) => super.copyWith((message) => updates(message as Message_Secret)) as Message_Secret; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_Secret create() => Message_Secret._(); + Message_Secret createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_Secret getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_Secret? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get ciphertext => $_getN(0); + @$pb.TagNumber(1) + set ciphertext($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasCiphertext() => $_has(0); + @$pb.TagNumber(1) + void clearCiphertext() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get expiration => $_getI64(1); + @$pb.TagNumber(2) + set expiration($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasExpiration() => $_has(1); + @$pb.TagNumber(2) + void clearExpiration() => clearField(2); +} + +class Message_ControlDelete extends $pb.GeneratedMessage { + factory Message_ControlDelete() => create(); + Message_ControlDelete._() : super(); + factory Message_ControlDelete.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlDelete.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlDelete', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..pc<$0.TypedKey>(1, _omitFieldNames ? '' : 'ids', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Message_ControlDelete clone() => Message_ControlDelete()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message_ControlDelete copyWith(void Function(Message_ControlDelete) updates) => super.copyWith((message) => updates(message as Message_ControlDelete)) as Message_ControlDelete; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlDelete create() => Message_ControlDelete._(); + Message_ControlDelete createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlDelete getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlDelete? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$0.TypedKey> get ids => $_getList(0); +} + +class Message_ControlClear extends $pb.GeneratedMessage { + factory Message_ControlClear() => create(); + Message_ControlClear._() : super(); + factory Message_ControlClear.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlClear.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlClear', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Message_ControlClear clone() => Message_ControlClear()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message_ControlClear copyWith(void Function(Message_ControlClear) updates) => super.copyWith((message) => updates(message as Message_ControlClear)) as Message_ControlClear; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlClear create() => Message_ControlClear._(); + Message_ControlClear createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlClear getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlClear? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get timestamp => $_getI64(0); + @$pb.TagNumber(1) + set timestamp($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasTimestamp() => $_has(0); + @$pb.TagNumber(1) + void clearTimestamp() => clearField(1); +} + +class Message_ControlSettings extends $pb.GeneratedMessage { + factory Message_ControlSettings() => create(); + Message_ControlSettings._() : super(); + factory Message_ControlSettings.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlSettings.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlSettings', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Message_ControlSettings clone() => Message_ControlSettings()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message_ControlSettings copyWith(void Function(Message_ControlSettings) updates) => super.copyWith((message) => updates(message as Message_ControlSettings)) as Message_ControlSettings; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlSettings create() => Message_ControlSettings._(); + Message_ControlSettings createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlSettings getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlSettings? _defaultInstance; + + @$pb.TagNumber(1) + ChatSettings get settings => $_getN(0); + @$pb.TagNumber(1) + set settings(ChatSettings v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasSettings() => $_has(0); + @$pb.TagNumber(1) + void clearSettings() => clearField(1); + @$pb.TagNumber(1) + ChatSettings ensureSettings() => $_ensure(0); +} + +class Message_ControlPermissions extends $pb.GeneratedMessage { + factory Message_ControlPermissions() => create(); + Message_ControlPermissions._() : super(); + factory Message_ControlPermissions.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlPermissions.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlPermissions', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'permissions', subBuilder: Permissions.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Message_ControlPermissions clone() => Message_ControlPermissions()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message_ControlPermissions copyWith(void Function(Message_ControlPermissions) updates) => super.copyWith((message) => updates(message as Message_ControlPermissions)) as Message_ControlPermissions; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlPermissions create() => Message_ControlPermissions._(); + Message_ControlPermissions createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlPermissions getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlPermissions? _defaultInstance; + + @$pb.TagNumber(1) + Permissions get permissions => $_getN(0); + @$pb.TagNumber(1) + set permissions(Permissions v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasPermissions() => $_has(0); + @$pb.TagNumber(1) + void clearPermissions() => clearField(1); + @$pb.TagNumber(1) + Permissions ensurePermissions() => $_ensure(0); +} + +class Message_ControlMembership extends $pb.GeneratedMessage { + factory Message_ControlMembership() => create(); + Message_ControlMembership._() : super(); + factory Message_ControlMembership.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlMembership.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlMembership', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'membership', subBuilder: Membership.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Message_ControlMembership clone() => Message_ControlMembership()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message_ControlMembership copyWith(void Function(Message_ControlMembership) updates) => super.copyWith((message) => updates(message as Message_ControlMembership)) as Message_ControlMembership; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlMembership create() => Message_ControlMembership._(); + Message_ControlMembership createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlMembership getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlMembership? _defaultInstance; + + @$pb.TagNumber(1) + Membership get membership => $_getN(0); + @$pb.TagNumber(1) + set membership(Membership v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasMembership() => $_has(0); + @$pb.TagNumber(1) + void clearMembership() => clearField(1); + @$pb.TagNumber(1) + Membership ensureMembership() => $_ensure(0); +} + +class Message_ControlModeration extends $pb.GeneratedMessage { + factory Message_ControlModeration() => create(); + Message_ControlModeration._() : super(); + factory Message_ControlModeration.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlModeration.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlModeration', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..pc<$0.TypedKey>(1, _omitFieldNames ? '' : 'acceptedIds', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..pc<$0.TypedKey>(2, _omitFieldNames ? '' : 'rejectedIds', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Message_ControlModeration clone() => Message_ControlModeration()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message_ControlModeration copyWith(void Function(Message_ControlModeration) updates) => super.copyWith((message) => updates(message as Message_ControlModeration)) as Message_ControlModeration; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlModeration create() => Message_ControlModeration._(); + Message_ControlModeration createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlModeration getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlModeration? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$0.TypedKey> get acceptedIds => $_getList(0); + + @$pb.TagNumber(2) + $core.List<$0.TypedKey> get rejectedIds => $_getList(1); +} + +enum Message_Kind { + text, + secret, + delete, + clear_7, + settings, + permissions, + membership, + moderation, + notSet } class Message extends $pb.GeneratedMessage { @@ -112,12 +749,31 @@ class Message extends $pb.GeneratedMessage { factory Message.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Message.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + static const $core.Map<$core.int, Message_Kind> _Message_KindByTag = { + 4 : Message_Kind.text, + 5 : Message_Kind.secret, + 6 : Message_Kind.delete, + 7 : Message_Kind.clear_7, + 8 : Message_Kind.settings, + 9 : Message_Kind.permissions, + 10 : Message_Kind.membership, + 11 : Message_Kind.moderation, + 0 : Message_Kind.notSet + }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM<$1.TypedKey>(1, _omitFieldNames ? '' : 'author', subBuilder: $1.TypedKey.create) - ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) - ..aOS(3, _omitFieldNames ? '' : 'text') - ..aOM<$1.Signature>(4, _omitFieldNames ? '' : 'signature', subBuilder: $1.Signature.create) - ..pc(5, _omitFieldNames ? '' : 'attachments', $pb.PbFieldType.PM, subBuilder: Attachment.create) + ..oo(0, [4, 5, 6, 7, 8, 9, 10, 11]) + ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'id', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'author', subBuilder: $0.TypedKey.create) + ..a<$fixnum.Int64>(3, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..aOM(4, _omitFieldNames ? '' : 'text', subBuilder: Message_Text.create) + ..aOM(5, _omitFieldNames ? '' : 'secret', subBuilder: Message_Secret.create) + ..aOM(6, _omitFieldNames ? '' : 'delete', subBuilder: Message_ControlDelete.create) + ..aOM(7, _omitFieldNames ? '' : 'clear', subBuilder: Message_ControlClear.create) + ..aOM(8, _omitFieldNames ? '' : 'settings', subBuilder: Message_ControlSettings.create) + ..aOM(9, _omitFieldNames ? '' : 'permissions', subBuilder: Message_ControlPermissions.create) + ..aOM(10, _omitFieldNames ? '' : 'membership', subBuilder: Message_ControlMembership.create) + ..aOM(11, _omitFieldNames ? '' : 'moderation', subBuilder: Message_ControlModeration.create) + ..aOM<$0.Signature>(12, _omitFieldNames ? '' : 'signature', subBuilder: $0.Signature.create) ..hasRequiredFields = false ; @@ -142,48 +798,192 @@ class Message extends $pb.GeneratedMessage { static Message getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Message? _defaultInstance; + Message_Kind whichKind() => _Message_KindByTag[$_whichOneof(0)]!; + void clearKind() => clearField($_whichOneof(0)); + @$pb.TagNumber(1) - $1.TypedKey get author => $_getN(0); + $0.TypedKey get id => $_getN(0); @$pb.TagNumber(1) - set author($1.TypedKey v) { setField(1, v); } + set id($0.TypedKey v) { setField(1, v); } @$pb.TagNumber(1) - $core.bool hasAuthor() => $_has(0); + $core.bool hasId() => $_has(0); @$pb.TagNumber(1) - void clearAuthor() => clearField(1); + void clearId() => clearField(1); @$pb.TagNumber(1) - $1.TypedKey ensureAuthor() => $_ensure(0); + $0.TypedKey ensureId() => $_ensure(0); @$pb.TagNumber(2) - $fixnum.Int64 get timestamp => $_getI64(1); + $0.TypedKey get author => $_getN(1); @$pb.TagNumber(2) - set timestamp($fixnum.Int64 v) { $_setInt64(1, v); } + set author($0.TypedKey v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasTimestamp() => $_has(1); + $core.bool hasAuthor() => $_has(1); @$pb.TagNumber(2) - void clearTimestamp() => clearField(2); + void clearAuthor() => clearField(2); + @$pb.TagNumber(2) + $0.TypedKey ensureAuthor() => $_ensure(1); @$pb.TagNumber(3) - $core.String get text => $_getSZ(2); + $fixnum.Int64 get timestamp => $_getI64(2); @$pb.TagNumber(3) - set text($core.String v) { $_setString(2, v); } + set timestamp($fixnum.Int64 v) { $_setInt64(2, v); } @$pb.TagNumber(3) - $core.bool hasText() => $_has(2); + $core.bool hasTimestamp() => $_has(2); @$pb.TagNumber(3) - void clearText() => clearField(3); + void clearTimestamp() => clearField(3); @$pb.TagNumber(4) - $1.Signature get signature => $_getN(3); + Message_Text get text => $_getN(3); @$pb.TagNumber(4) - set signature($1.Signature v) { setField(4, v); } + set text(Message_Text v) { setField(4, v); } @$pb.TagNumber(4) - $core.bool hasSignature() => $_has(3); + $core.bool hasText() => $_has(3); @$pb.TagNumber(4) - void clearSignature() => clearField(4); + void clearText() => clearField(4); @$pb.TagNumber(4) - $1.Signature ensureSignature() => $_ensure(3); + Message_Text ensureText() => $_ensure(3); @$pb.TagNumber(5) - $core.List get attachments => $_getList(4); + Message_Secret get secret => $_getN(4); + @$pb.TagNumber(5) + set secret(Message_Secret v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasSecret() => $_has(4); + @$pb.TagNumber(5) + void clearSecret() => clearField(5); + @$pb.TagNumber(5) + Message_Secret ensureSecret() => $_ensure(4); + + @$pb.TagNumber(6) + Message_ControlDelete get delete => $_getN(5); + @$pb.TagNumber(6) + set delete(Message_ControlDelete v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasDelete() => $_has(5); + @$pb.TagNumber(6) + void clearDelete() => clearField(6); + @$pb.TagNumber(6) + Message_ControlDelete ensureDelete() => $_ensure(5); + + @$pb.TagNumber(7) + Message_ControlClear get clear_7 => $_getN(6); + @$pb.TagNumber(7) + set clear_7(Message_ControlClear v) { setField(7, v); } + @$pb.TagNumber(7) + $core.bool hasClear_7() => $_has(6); + @$pb.TagNumber(7) + void clearClear_7() => clearField(7); + @$pb.TagNumber(7) + Message_ControlClear ensureClear_7() => $_ensure(6); + + @$pb.TagNumber(8) + Message_ControlSettings get settings => $_getN(7); + @$pb.TagNumber(8) + set settings(Message_ControlSettings v) { setField(8, v); } + @$pb.TagNumber(8) + $core.bool hasSettings() => $_has(7); + @$pb.TagNumber(8) + void clearSettings() => clearField(8); + @$pb.TagNumber(8) + Message_ControlSettings ensureSettings() => $_ensure(7); + + @$pb.TagNumber(9) + Message_ControlPermissions get permissions => $_getN(8); + @$pb.TagNumber(9) + set permissions(Message_ControlPermissions v) { setField(9, v); } + @$pb.TagNumber(9) + $core.bool hasPermissions() => $_has(8); + @$pb.TagNumber(9) + void clearPermissions() => clearField(9); + @$pb.TagNumber(9) + Message_ControlPermissions ensurePermissions() => $_ensure(8); + + @$pb.TagNumber(10) + Message_ControlMembership get membership => $_getN(9); + @$pb.TagNumber(10) + set membership(Message_ControlMembership v) { setField(10, v); } + @$pb.TagNumber(10) + $core.bool hasMembership() => $_has(9); + @$pb.TagNumber(10) + void clearMembership() => clearField(10); + @$pb.TagNumber(10) + Message_ControlMembership ensureMembership() => $_ensure(9); + + @$pb.TagNumber(11) + Message_ControlModeration get moderation => $_getN(10); + @$pb.TagNumber(11) + set moderation(Message_ControlModeration v) { setField(11, v); } + @$pb.TagNumber(11) + $core.bool hasModeration() => $_has(10); + @$pb.TagNumber(11) + void clearModeration() => clearField(11); + @$pb.TagNumber(11) + Message_ControlModeration ensureModeration() => $_ensure(10); + + @$pb.TagNumber(12) + $0.Signature get signature => $_getN(11); + @$pb.TagNumber(12) + set signature($0.Signature v) { setField(12, v); } + @$pb.TagNumber(12) + $core.bool hasSignature() => $_has(11); + @$pb.TagNumber(12) + void clearSignature() => clearField(12); + @$pb.TagNumber(12) + $0.Signature ensureSignature() => $_ensure(11); +} + +class ReconciledMessage extends $pb.GeneratedMessage { + factory ReconciledMessage() => create(); + ReconciledMessage._() : super(); + factory ReconciledMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ReconciledMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ReconciledMessage', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'content', subBuilder: Message.create) + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'reconciledTime', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ReconciledMessage clone() => ReconciledMessage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ReconciledMessage copyWith(void Function(ReconciledMessage) updates) => super.copyWith((message) => updates(message as ReconciledMessage)) as ReconciledMessage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ReconciledMessage create() => ReconciledMessage._(); + ReconciledMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ReconciledMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ReconciledMessage? _defaultInstance; + + @$pb.TagNumber(1) + Message get content => $_getN(0); + @$pb.TagNumber(1) + set content(Message v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasContent() => $_has(0); + @$pb.TagNumber(1) + void clearContent() => clearField(1); + @$pb.TagNumber(1) + Message ensureContent() => $_ensure(0); + + @$pb.TagNumber(2) + $fixnum.Int64 get reconciledTime => $_getI64(1); + @$pb.TagNumber(2) + set reconciledTime($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasReconciledTime() => $_has(1); + @$pb.TagNumber(2) + void clearReconciledTime() => clearField(2); } class Conversation extends $pb.GeneratedMessage { @@ -195,7 +995,7 @@ class Conversation extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Conversation', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..aOM(1, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) ..aOS(2, _omitFieldNames ? '' : 'identityMasterJson') - ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'messages', subBuilder: $1.TypedKey.create) + ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'messages', subBuilder: $0.TypedKey.create) ..hasRequiredFields = false ; @@ -241,15 +1041,349 @@ class Conversation extends $pb.GeneratedMessage { void clearIdentityMasterJson() => clearField(2); @$pb.TagNumber(3) - $1.TypedKey get messages => $_getN(2); + $0.TypedKey get messages => $_getN(2); @$pb.TagNumber(3) - set messages($1.TypedKey v) { setField(3, v); } + set messages($0.TypedKey v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasMessages() => $_has(2); @$pb.TagNumber(3) void clearMessages() => clearField(3); @$pb.TagNumber(3) - $1.TypedKey ensureMessages() => $_ensure(2); + $0.TypedKey ensureMessages() => $_ensure(2); +} + +class Chat extends $pb.GeneratedMessage { + factory Chat() => create(); + Chat._() : super(); + factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Chat clone() => Chat()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Chat create() => Chat._(); + Chat createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Chat? _defaultInstance; + + @$pb.TagNumber(1) + ChatSettings get settings => $_getN(0); + @$pb.TagNumber(1) + set settings(ChatSettings v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasSettings() => $_has(0); + @$pb.TagNumber(1) + void clearSettings() => clearField(1); + @$pb.TagNumber(1) + ChatSettings ensureSettings() => $_ensure(0); + + @$pb.TagNumber(2) + $0.TypedKey get localConversationRecordKey => $_getN(1); + @$pb.TagNumber(2) + set localConversationRecordKey($0.TypedKey v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasLocalConversationRecordKey() => $_has(1); + @$pb.TagNumber(2) + void clearLocalConversationRecordKey() => clearField(2); + @$pb.TagNumber(2) + $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1); + + @$pb.TagNumber(3) + $0.TypedKey get remoteConversationRecordKey => $_getN(2); + @$pb.TagNumber(3) + set remoteConversationRecordKey($0.TypedKey v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasRemoteConversationRecordKey() => $_has(2); + @$pb.TagNumber(3) + void clearRemoteConversationRecordKey() => clearField(3); + @$pb.TagNumber(3) + $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2); +} + +class GroupChat extends $pb.GeneratedMessage { + factory GroupChat() => create(); + GroupChat._() : super(); + factory GroupChat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory GroupChat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GroupChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) + ..pc<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKeys', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + GroupChat clone() => GroupChat()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + GroupChat copyWith(void Function(GroupChat) updates) => super.copyWith((message) => updates(message as GroupChat)) as GroupChat; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GroupChat create() => GroupChat._(); + GroupChat createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GroupChat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static GroupChat? _defaultInstance; + + @$pb.TagNumber(1) + ChatSettings get settings => $_getN(0); + @$pb.TagNumber(1) + set settings(ChatSettings v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasSettings() => $_has(0); + @$pb.TagNumber(1) + void clearSettings() => clearField(1); + @$pb.TagNumber(1) + ChatSettings ensureSettings() => $_ensure(0); + + @$pb.TagNumber(2) + $0.TypedKey get localConversationRecordKey => $_getN(1); + @$pb.TagNumber(2) + set localConversationRecordKey($0.TypedKey v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasLocalConversationRecordKey() => $_has(1); + @$pb.TagNumber(2) + void clearLocalConversationRecordKey() => clearField(2); + @$pb.TagNumber(2) + $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1); + + @$pb.TagNumber(3) + $core.List<$0.TypedKey> get remoteConversationRecordKeys => $_getList(2); +} + +class Profile extends $pb.GeneratedMessage { + factory Profile() => create(); + Profile._() : super(); + factory Profile.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Profile.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Profile', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'name') + ..aOS(2, _omitFieldNames ? '' : 'pronouns') + ..aOS(3, _omitFieldNames ? '' : 'about') + ..aOS(4, _omitFieldNames ? '' : 'status') + ..e(5, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values) + ..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'avatar', subBuilder: $0.TypedKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Profile clone() => Profile()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Profile copyWith(void Function(Profile) updates) => super.copyWith((message) => updates(message as Profile)) as Profile; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Profile create() => Profile._(); + Profile createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Profile getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Profile? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get name => $_getSZ(0); + @$pb.TagNumber(1) + set name($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasName() => $_has(0); + @$pb.TagNumber(1) + void clearName() => clearField(1); + + @$pb.TagNumber(2) + $core.String get pronouns => $_getSZ(1); + @$pb.TagNumber(2) + set pronouns($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasPronouns() => $_has(1); + @$pb.TagNumber(2) + void clearPronouns() => clearField(2); + + @$pb.TagNumber(3) + $core.String get about => $_getSZ(2); + @$pb.TagNumber(3) + set about($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasAbout() => $_has(2); + @$pb.TagNumber(3) + void clearAbout() => clearField(3); + + @$pb.TagNumber(4) + $core.String get status => $_getSZ(3); + @$pb.TagNumber(4) + set status($core.String v) { $_setString(3, v); } + @$pb.TagNumber(4) + $core.bool hasStatus() => $_has(3); + @$pb.TagNumber(4) + void clearStatus() => clearField(4); + + @$pb.TagNumber(5) + Availability get availability => $_getN(4); + @$pb.TagNumber(5) + set availability(Availability v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasAvailability() => $_has(4); + @$pb.TagNumber(5) + void clearAvailability() => clearField(5); + + @$pb.TagNumber(6) + $0.TypedKey get avatar => $_getN(5); + @$pb.TagNumber(6) + set avatar($0.TypedKey v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasAvatar() => $_has(5); + @$pb.TagNumber(6) + void clearAvatar() => clearField(6); + @$pb.TagNumber(6) + $0.TypedKey ensureAvatar() => $_ensure(5); +} + +class Account extends $pb.GeneratedMessage { + factory Account() => create(); + Account._() : super(); + factory Account.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Account.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Account', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) + ..aOB(2, _omitFieldNames ? '' : 'invisible') + ..a<$core.int>(3, _omitFieldNames ? '' : 'autoAwayTimeoutSec', $pb.PbFieldType.OU3) + ..aOM<$1.OwnedDHTRecordPointer>(4, _omitFieldNames ? '' : 'contactList', subBuilder: $1.OwnedDHTRecordPointer.create) + ..aOM<$1.OwnedDHTRecordPointer>(5, _omitFieldNames ? '' : 'contactInvitationRecords', subBuilder: $1.OwnedDHTRecordPointer.create) + ..aOM<$1.OwnedDHTRecordPointer>(6, _omitFieldNames ? '' : 'chatList', subBuilder: $1.OwnedDHTRecordPointer.create) + ..aOM<$1.OwnedDHTRecordPointer>(7, _omitFieldNames ? '' : 'groupChatList', subBuilder: $1.OwnedDHTRecordPointer.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Account clone() => Account()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Account copyWith(void Function(Account) updates) => super.copyWith((message) => updates(message as Account)) as Account; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Account create() => Account._(); + Account createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Account getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Account? _defaultInstance; + + @$pb.TagNumber(1) + Profile get profile => $_getN(0); + @$pb.TagNumber(1) + set profile(Profile v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasProfile() => $_has(0); + @$pb.TagNumber(1) + void clearProfile() => clearField(1); + @$pb.TagNumber(1) + Profile ensureProfile() => $_ensure(0); + + @$pb.TagNumber(2) + $core.bool get invisible => $_getBF(1); + @$pb.TagNumber(2) + set invisible($core.bool v) { $_setBool(1, v); } + @$pb.TagNumber(2) + $core.bool hasInvisible() => $_has(1); + @$pb.TagNumber(2) + void clearInvisible() => clearField(2); + + @$pb.TagNumber(3) + $core.int get autoAwayTimeoutSec => $_getIZ(2); + @$pb.TagNumber(3) + set autoAwayTimeoutSec($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasAutoAwayTimeoutSec() => $_has(2); + @$pb.TagNumber(3) + void clearAutoAwayTimeoutSec() => clearField(3); + + @$pb.TagNumber(4) + $1.OwnedDHTRecordPointer get contactList => $_getN(3); + @$pb.TagNumber(4) + set contactList($1.OwnedDHTRecordPointer v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasContactList() => $_has(3); + @$pb.TagNumber(4) + void clearContactList() => clearField(4); + @$pb.TagNumber(4) + $1.OwnedDHTRecordPointer ensureContactList() => $_ensure(3); + + @$pb.TagNumber(5) + $1.OwnedDHTRecordPointer get contactInvitationRecords => $_getN(4); + @$pb.TagNumber(5) + set contactInvitationRecords($1.OwnedDHTRecordPointer v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasContactInvitationRecords() => $_has(4); + @$pb.TagNumber(5) + void clearContactInvitationRecords() => clearField(5); + @$pb.TagNumber(5) + $1.OwnedDHTRecordPointer ensureContactInvitationRecords() => $_ensure(4); + + @$pb.TagNumber(6) + $1.OwnedDHTRecordPointer get chatList => $_getN(5); + @$pb.TagNumber(6) + set chatList($1.OwnedDHTRecordPointer v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasChatList() => $_has(5); + @$pb.TagNumber(6) + void clearChatList() => clearField(6); + @$pb.TagNumber(6) + $1.OwnedDHTRecordPointer ensureChatList() => $_ensure(5); + + @$pb.TagNumber(7) + $1.OwnedDHTRecordPointer get groupChatList => $_getN(6); + @$pb.TagNumber(7) + set groupChatList($1.OwnedDHTRecordPointer v) { setField(7, v); } + @$pb.TagNumber(7) + $core.bool hasGroupChatList() => $_has(6); + @$pb.TagNumber(7) + void clearGroupChatList() => clearField(7); + @$pb.TagNumber(7) + $1.OwnedDHTRecordPointer ensureGroupChatList() => $_ensure(6); } class Contact extends $pb.GeneratedMessage { @@ -262,9 +1396,9 @@ class Contact extends $pb.GeneratedMessage { ..aOM(1, _omitFieldNames ? '' : 'editedProfile', subBuilder: Profile.create) ..aOM(2, _omitFieldNames ? '' : 'remoteProfile', subBuilder: Profile.create) ..aOS(3, _omitFieldNames ? '' : 'identityMasterJson') - ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $1.TypedKey.create) - ..aOM<$1.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $1.TypedKey.create) - ..aOM<$1.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $1.TypedKey.create) + ..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) ..aOB(7, _omitFieldNames ? '' : 'showAvailability') ..hasRequiredFields = false ; @@ -322,37 +1456,37 @@ class Contact extends $pb.GeneratedMessage { void clearIdentityMasterJson() => clearField(3); @$pb.TagNumber(4) - $1.TypedKey get identityPublicKey => $_getN(3); + $0.TypedKey get identityPublicKey => $_getN(3); @$pb.TagNumber(4) - set identityPublicKey($1.TypedKey v) { setField(4, v); } + set identityPublicKey($0.TypedKey v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasIdentityPublicKey() => $_has(3); @$pb.TagNumber(4) void clearIdentityPublicKey() => clearField(4); @$pb.TagNumber(4) - $1.TypedKey ensureIdentityPublicKey() => $_ensure(3); + $0.TypedKey ensureIdentityPublicKey() => $_ensure(3); @$pb.TagNumber(5) - $1.TypedKey get remoteConversationRecordKey => $_getN(4); + $0.TypedKey get remoteConversationRecordKey => $_getN(4); @$pb.TagNumber(5) - set remoteConversationRecordKey($1.TypedKey v) { setField(5, v); } + set remoteConversationRecordKey($0.TypedKey v) { setField(5, v); } @$pb.TagNumber(5) $core.bool hasRemoteConversationRecordKey() => $_has(4); @$pb.TagNumber(5) void clearRemoteConversationRecordKey() => clearField(5); @$pb.TagNumber(5) - $1.TypedKey ensureRemoteConversationRecordKey() => $_ensure(4); + $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(4); @$pb.TagNumber(6) - $1.TypedKey get localConversationRecordKey => $_getN(5); + $0.TypedKey get localConversationRecordKey => $_getN(5); @$pb.TagNumber(6) - set localConversationRecordKey($1.TypedKey v) { setField(6, v); } + set localConversationRecordKey($0.TypedKey v) { setField(6, v); } @$pb.TagNumber(6) $core.bool hasLocalConversationRecordKey() => $_has(5); @$pb.TagNumber(6) void clearLocalConversationRecordKey() => clearField(6); @$pb.TagNumber(6) - $1.TypedKey ensureLocalConversationRecordKey() => $_ensure(5); + $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(5); @$pb.TagNumber(7) $core.bool get showAvailability => $_getBF(6); @@ -364,256 +1498,6 @@ class Contact extends $pb.GeneratedMessage { void clearShowAvailability() => clearField(7); } -class Profile extends $pb.GeneratedMessage { - factory Profile() => create(); - Profile._() : super(); - factory Profile.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Profile.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Profile', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'name') - ..aOS(2, _omitFieldNames ? '' : 'pronouns') - ..aOS(3, _omitFieldNames ? '' : 'status') - ..e(4, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values) - ..aOM<$1.TypedKey>(5, _omitFieldNames ? '' : 'avatar', subBuilder: $1.TypedKey.create) - ..hasRequiredFields = false - ; - - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Profile clone() => Profile()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Profile copyWith(void Function(Profile) updates) => super.copyWith((message) => updates(message as Profile)) as Profile; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Profile create() => Profile._(); - Profile createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Profile getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Profile? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get name => $_getSZ(0); - @$pb.TagNumber(1) - set name($core.String v) { $_setString(0, v); } - @$pb.TagNumber(1) - $core.bool hasName() => $_has(0); - @$pb.TagNumber(1) - void clearName() => clearField(1); - - @$pb.TagNumber(2) - $core.String get pronouns => $_getSZ(1); - @$pb.TagNumber(2) - set pronouns($core.String v) { $_setString(1, v); } - @$pb.TagNumber(2) - $core.bool hasPronouns() => $_has(1); - @$pb.TagNumber(2) - void clearPronouns() => clearField(2); - - @$pb.TagNumber(3) - $core.String get status => $_getSZ(2); - @$pb.TagNumber(3) - set status($core.String v) { $_setString(2, v); } - @$pb.TagNumber(3) - $core.bool hasStatus() => $_has(2); - @$pb.TagNumber(3) - void clearStatus() => clearField(3); - - @$pb.TagNumber(4) - Availability get availability => $_getN(3); - @$pb.TagNumber(4) - set availability(Availability v) { setField(4, v); } - @$pb.TagNumber(4) - $core.bool hasAvailability() => $_has(3); - @$pb.TagNumber(4) - void clearAvailability() => clearField(4); - - @$pb.TagNumber(5) - $1.TypedKey get avatar => $_getN(4); - @$pb.TagNumber(5) - set avatar($1.TypedKey v) { setField(5, v); } - @$pb.TagNumber(5) - $core.bool hasAvatar() => $_has(4); - @$pb.TagNumber(5) - void clearAvatar() => clearField(5); - @$pb.TagNumber(5) - $1.TypedKey ensureAvatar() => $_ensure(4); -} - -class Chat extends $pb.GeneratedMessage { - factory Chat() => create(); - Chat._() : super(); - factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: ChatType.CHAT_TYPE_UNSPECIFIED, valueOf: ChatType.valueOf, enumValues: ChatType.values) - ..aOM<$1.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $1.TypedKey.create) - ..aOM<$0.OwnedDHTRecordPointer>(3, _omitFieldNames ? '' : 'reconciledChatRecord', subBuilder: $0.OwnedDHTRecordPointer.create) - ..hasRequiredFields = false - ; - - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Chat clone() => Chat()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Chat create() => Chat._(); - Chat createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Chat? _defaultInstance; - - @$pb.TagNumber(1) - ChatType get type => $_getN(0); - @$pb.TagNumber(1) - set type(ChatType v) { setField(1, v); } - @$pb.TagNumber(1) - $core.bool hasType() => $_has(0); - @$pb.TagNumber(1) - void clearType() => clearField(1); - - @$pb.TagNumber(2) - $1.TypedKey get remoteConversationRecordKey => $_getN(1); - @$pb.TagNumber(2) - set remoteConversationRecordKey($1.TypedKey v) { setField(2, v); } - @$pb.TagNumber(2) - $core.bool hasRemoteConversationRecordKey() => $_has(1); - @$pb.TagNumber(2) - void clearRemoteConversationRecordKey() => clearField(2); - @$pb.TagNumber(2) - $1.TypedKey ensureRemoteConversationRecordKey() => $_ensure(1); - - @$pb.TagNumber(3) - $0.OwnedDHTRecordPointer get reconciledChatRecord => $_getN(2); - @$pb.TagNumber(3) - set reconciledChatRecord($0.OwnedDHTRecordPointer v) { setField(3, v); } - @$pb.TagNumber(3) - $core.bool hasReconciledChatRecord() => $_has(2); - @$pb.TagNumber(3) - void clearReconciledChatRecord() => clearField(3); - @$pb.TagNumber(3) - $0.OwnedDHTRecordPointer ensureReconciledChatRecord() => $_ensure(2); -} - -class Account extends $pb.GeneratedMessage { - factory Account() => create(); - Account._() : super(); - factory Account.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Account.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Account', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM(1, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) - ..aOB(2, _omitFieldNames ? '' : 'invisible') - ..a<$core.int>(3, _omitFieldNames ? '' : 'autoAwayTimeoutSec', $pb.PbFieldType.OU3) - ..aOM<$0.OwnedDHTRecordPointer>(4, _omitFieldNames ? '' : 'contactList', subBuilder: $0.OwnedDHTRecordPointer.create) - ..aOM<$0.OwnedDHTRecordPointer>(5, _omitFieldNames ? '' : 'contactInvitationRecords', subBuilder: $0.OwnedDHTRecordPointer.create) - ..aOM<$0.OwnedDHTRecordPointer>(6, _omitFieldNames ? '' : 'chatList', subBuilder: $0.OwnedDHTRecordPointer.create) - ..hasRequiredFields = false - ; - - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - Account clone() => Account()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Account copyWith(void Function(Account) updates) => super.copyWith((message) => updates(message as Account)) as Account; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Account create() => Account._(); - Account createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Account getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Account? _defaultInstance; - - @$pb.TagNumber(1) - Profile get profile => $_getN(0); - @$pb.TagNumber(1) - set profile(Profile v) { setField(1, v); } - @$pb.TagNumber(1) - $core.bool hasProfile() => $_has(0); - @$pb.TagNumber(1) - void clearProfile() => clearField(1); - @$pb.TagNumber(1) - Profile ensureProfile() => $_ensure(0); - - @$pb.TagNumber(2) - $core.bool get invisible => $_getBF(1); - @$pb.TagNumber(2) - set invisible($core.bool v) { $_setBool(1, v); } - @$pb.TagNumber(2) - $core.bool hasInvisible() => $_has(1); - @$pb.TagNumber(2) - void clearInvisible() => clearField(2); - - @$pb.TagNumber(3) - $core.int get autoAwayTimeoutSec => $_getIZ(2); - @$pb.TagNumber(3) - set autoAwayTimeoutSec($core.int v) { $_setUnsignedInt32(2, v); } - @$pb.TagNumber(3) - $core.bool hasAutoAwayTimeoutSec() => $_has(2); - @$pb.TagNumber(3) - void clearAutoAwayTimeoutSec() => clearField(3); - - @$pb.TagNumber(4) - $0.OwnedDHTRecordPointer get contactList => $_getN(3); - @$pb.TagNumber(4) - set contactList($0.OwnedDHTRecordPointer v) { setField(4, v); } - @$pb.TagNumber(4) - $core.bool hasContactList() => $_has(3); - @$pb.TagNumber(4) - void clearContactList() => clearField(4); - @$pb.TagNumber(4) - $0.OwnedDHTRecordPointer ensureContactList() => $_ensure(3); - - @$pb.TagNumber(5) - $0.OwnedDHTRecordPointer get contactInvitationRecords => $_getN(4); - @$pb.TagNumber(5) - set contactInvitationRecords($0.OwnedDHTRecordPointer v) { setField(5, v); } - @$pb.TagNumber(5) - $core.bool hasContactInvitationRecords() => $_has(4); - @$pb.TagNumber(5) - void clearContactInvitationRecords() => clearField(5); - @$pb.TagNumber(5) - $0.OwnedDHTRecordPointer ensureContactInvitationRecords() => $_ensure(4); - - @$pb.TagNumber(6) - $0.OwnedDHTRecordPointer get chatList => $_getN(5); - @$pb.TagNumber(6) - set chatList($0.OwnedDHTRecordPointer v) { setField(6, v); } - @$pb.TagNumber(6) - $core.bool hasChatList() => $_has(5); - @$pb.TagNumber(6) - void clearChatList() => clearField(6); - @$pb.TagNumber(6) - $0.OwnedDHTRecordPointer ensureChatList() => $_ensure(5); -} - class ContactInvitation extends $pb.GeneratedMessage { factory ContactInvitation() => create(); ContactInvitation._() : super(); @@ -621,7 +1505,7 @@ class ContactInvitation extends $pb.GeneratedMessage { factory ContactInvitation.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactInvitation', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM<$1.TypedKey>(1, _omitFieldNames ? '' : 'contactRequestInboxKey', subBuilder: $1.TypedKey.create) + ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'contactRequestInboxKey', subBuilder: $0.TypedKey.create) ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'writerSecret', $pb.PbFieldType.OY) ..hasRequiredFields = false ; @@ -648,15 +1532,15 @@ class ContactInvitation extends $pb.GeneratedMessage { static ContactInvitation? _defaultInstance; @$pb.TagNumber(1) - $1.TypedKey get contactRequestInboxKey => $_getN(0); + $0.TypedKey get contactRequestInboxKey => $_getN(0); @$pb.TagNumber(1) - set contactRequestInboxKey($1.TypedKey v) { setField(1, v); } + set contactRequestInboxKey($0.TypedKey v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasContactRequestInboxKey() => $_has(0); @$pb.TagNumber(1) void clearContactRequestInboxKey() => clearField(1); @$pb.TagNumber(1) - $1.TypedKey ensureContactRequestInboxKey() => $_ensure(0); + $0.TypedKey ensureContactRequestInboxKey() => $_ensure(0); @$pb.TagNumber(2) $core.List<$core.int> get writerSecret => $_getN(1); @@ -676,7 +1560,7 @@ class SignedContactInvitation extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SignedContactInvitation', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'contactInvitation', $pb.PbFieldType.OY) - ..aOM<$1.Signature>(2, _omitFieldNames ? '' : 'identitySignature', subBuilder: $1.Signature.create) + ..aOM<$0.Signature>(2, _omitFieldNames ? '' : 'identitySignature', subBuilder: $0.Signature.create) ..hasRequiredFields = false ; @@ -711,15 +1595,15 @@ class SignedContactInvitation extends $pb.GeneratedMessage { void clearContactInvitation() => clearField(1); @$pb.TagNumber(2) - $1.Signature get identitySignature => $_getN(1); + $0.Signature get identitySignature => $_getN(1); @$pb.TagNumber(2) - set identitySignature($1.Signature v) { setField(2, v); } + set identitySignature($0.Signature v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasIdentitySignature() => $_has(1); @$pb.TagNumber(2) void clearIdentitySignature() => clearField(2); @$pb.TagNumber(2) - $1.Signature ensureIdentitySignature() => $_ensure(1); + $0.Signature ensureIdentitySignature() => $_ensure(1); } class ContactRequest extends $pb.GeneratedMessage { @@ -781,10 +1665,10 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { factory ContactRequestPrivate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactRequestPrivate', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM<$1.CryptoKey>(1, _omitFieldNames ? '' : 'writerKey', subBuilder: $1.CryptoKey.create) + ..aOM<$0.CryptoKey>(1, _omitFieldNames ? '' : 'writerKey', subBuilder: $0.CryptoKey.create) ..aOM(2, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) - ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $1.TypedKey.create) - ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'chatRecordKey', subBuilder: $1.TypedKey.create) + ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'chatRecordKey', subBuilder: $0.TypedKey.create) ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..hasRequiredFields = false ; @@ -811,15 +1695,15 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { static ContactRequestPrivate? _defaultInstance; @$pb.TagNumber(1) - $1.CryptoKey get writerKey => $_getN(0); + $0.CryptoKey get writerKey => $_getN(0); @$pb.TagNumber(1) - set writerKey($1.CryptoKey v) { setField(1, v); } + set writerKey($0.CryptoKey v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasWriterKey() => $_has(0); @$pb.TagNumber(1) void clearWriterKey() => clearField(1); @$pb.TagNumber(1) - $1.CryptoKey ensureWriterKey() => $_ensure(0); + $0.CryptoKey ensureWriterKey() => $_ensure(0); @$pb.TagNumber(2) Profile get profile => $_getN(1); @@ -833,26 +1717,26 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { Profile ensureProfile() => $_ensure(1); @$pb.TagNumber(3) - $1.TypedKey get identityMasterRecordKey => $_getN(2); + $0.TypedKey get identityMasterRecordKey => $_getN(2); @$pb.TagNumber(3) - set identityMasterRecordKey($1.TypedKey v) { setField(3, v); } + set identityMasterRecordKey($0.TypedKey v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasIdentityMasterRecordKey() => $_has(2); @$pb.TagNumber(3) void clearIdentityMasterRecordKey() => clearField(3); @$pb.TagNumber(3) - $1.TypedKey ensureIdentityMasterRecordKey() => $_ensure(2); + $0.TypedKey ensureIdentityMasterRecordKey() => $_ensure(2); @$pb.TagNumber(4) - $1.TypedKey get chatRecordKey => $_getN(3); + $0.TypedKey get chatRecordKey => $_getN(3); @$pb.TagNumber(4) - set chatRecordKey($1.TypedKey v) { setField(4, v); } + set chatRecordKey($0.TypedKey v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasChatRecordKey() => $_has(3); @$pb.TagNumber(4) void clearChatRecordKey() => clearField(4); @$pb.TagNumber(4) - $1.TypedKey ensureChatRecordKey() => $_ensure(3); + $0.TypedKey ensureChatRecordKey() => $_ensure(3); @$pb.TagNumber(5) $fixnum.Int64 get expiration => $_getI64(4); @@ -872,8 +1756,8 @@ class ContactResponse extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..aOB(1, _omitFieldNames ? '' : 'accept') - ..aOM<$1.TypedKey>(2, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $1.TypedKey.create) - ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $1.TypedKey.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) ..hasRequiredFields = false ; @@ -908,26 +1792,26 @@ class ContactResponse extends $pb.GeneratedMessage { void clearAccept() => clearField(1); @$pb.TagNumber(2) - $1.TypedKey get identityMasterRecordKey => $_getN(1); + $0.TypedKey get identityMasterRecordKey => $_getN(1); @$pb.TagNumber(2) - set identityMasterRecordKey($1.TypedKey v) { setField(2, v); } + set identityMasterRecordKey($0.TypedKey v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasIdentityMasterRecordKey() => $_has(1); @$pb.TagNumber(2) void clearIdentityMasterRecordKey() => clearField(2); @$pb.TagNumber(2) - $1.TypedKey ensureIdentityMasterRecordKey() => $_ensure(1); + $0.TypedKey ensureIdentityMasterRecordKey() => $_ensure(1); @$pb.TagNumber(3) - $1.TypedKey get remoteConversationRecordKey => $_getN(2); + $0.TypedKey get remoteConversationRecordKey => $_getN(2); @$pb.TagNumber(3) - set remoteConversationRecordKey($1.TypedKey v) { setField(3, v); } + set remoteConversationRecordKey($0.TypedKey v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasRemoteConversationRecordKey() => $_has(2); @$pb.TagNumber(3) void clearRemoteConversationRecordKey() => clearField(3); @$pb.TagNumber(3) - $1.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2); + $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2); } class SignedContactResponse extends $pb.GeneratedMessage { @@ -938,7 +1822,7 @@ class SignedContactResponse extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SignedContactResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'contactResponse', $pb.PbFieldType.OY) - ..aOM<$1.Signature>(2, _omitFieldNames ? '' : 'identitySignature', subBuilder: $1.Signature.create) + ..aOM<$0.Signature>(2, _omitFieldNames ? '' : 'identitySignature', subBuilder: $0.Signature.create) ..hasRequiredFields = false ; @@ -973,15 +1857,15 @@ class SignedContactResponse extends $pb.GeneratedMessage { void clearContactResponse() => clearField(1); @$pb.TagNumber(2) - $1.Signature get identitySignature => $_getN(1); + $0.Signature get identitySignature => $_getN(1); @$pb.TagNumber(2) - set identitySignature($1.Signature v) { setField(2, v); } + set identitySignature($0.Signature v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasIdentitySignature() => $_has(1); @$pb.TagNumber(2) void clearIdentitySignature() => clearField(2); @$pb.TagNumber(2) - $1.Signature ensureIdentitySignature() => $_ensure(1); + $0.Signature ensureIdentitySignature() => $_ensure(1); } class ContactInvitationRecord extends $pb.GeneratedMessage { @@ -991,10 +1875,10 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { factory ContactInvitationRecord.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactInvitationRecord', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM<$0.OwnedDHTRecordPointer>(1, _omitFieldNames ? '' : 'contactRequestInbox', subBuilder: $0.OwnedDHTRecordPointer.create) - ..aOM<$1.CryptoKey>(2, _omitFieldNames ? '' : 'writerKey', subBuilder: $1.CryptoKey.create) - ..aOM<$1.CryptoKey>(3, _omitFieldNames ? '' : 'writerSecret', subBuilder: $1.CryptoKey.create) - ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $1.TypedKey.create) + ..aOM<$1.OwnedDHTRecordPointer>(1, _omitFieldNames ? '' : 'contactRequestInbox', subBuilder: $1.OwnedDHTRecordPointer.create) + ..aOM<$0.CryptoKey>(2, _omitFieldNames ? '' : 'writerKey', subBuilder: $0.CryptoKey.create) + ..aOM<$0.CryptoKey>(3, _omitFieldNames ? '' : 'writerSecret', subBuilder: $0.CryptoKey.create) + ..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'invitation', $pb.PbFieldType.OY) ..aOS(7, _omitFieldNames ? '' : 'message') @@ -1023,48 +1907,48 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { static ContactInvitationRecord? _defaultInstance; @$pb.TagNumber(1) - $0.OwnedDHTRecordPointer get contactRequestInbox => $_getN(0); + $1.OwnedDHTRecordPointer get contactRequestInbox => $_getN(0); @$pb.TagNumber(1) - set contactRequestInbox($0.OwnedDHTRecordPointer v) { setField(1, v); } + set contactRequestInbox($1.OwnedDHTRecordPointer v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasContactRequestInbox() => $_has(0); @$pb.TagNumber(1) void clearContactRequestInbox() => clearField(1); @$pb.TagNumber(1) - $0.OwnedDHTRecordPointer ensureContactRequestInbox() => $_ensure(0); + $1.OwnedDHTRecordPointer ensureContactRequestInbox() => $_ensure(0); @$pb.TagNumber(2) - $1.CryptoKey get writerKey => $_getN(1); + $0.CryptoKey get writerKey => $_getN(1); @$pb.TagNumber(2) - set writerKey($1.CryptoKey v) { setField(2, v); } + set writerKey($0.CryptoKey v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasWriterKey() => $_has(1); @$pb.TagNumber(2) void clearWriterKey() => clearField(2); @$pb.TagNumber(2) - $1.CryptoKey ensureWriterKey() => $_ensure(1); + $0.CryptoKey ensureWriterKey() => $_ensure(1); @$pb.TagNumber(3) - $1.CryptoKey get writerSecret => $_getN(2); + $0.CryptoKey get writerSecret => $_getN(2); @$pb.TagNumber(3) - set writerSecret($1.CryptoKey v) { setField(3, v); } + set writerSecret($0.CryptoKey v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasWriterSecret() => $_has(2); @$pb.TagNumber(3) void clearWriterSecret() => clearField(3); @$pb.TagNumber(3) - $1.CryptoKey ensureWriterSecret() => $_ensure(2); + $0.CryptoKey ensureWriterSecret() => $_ensure(2); @$pb.TagNumber(4) - $1.TypedKey get localConversationRecordKey => $_getN(3); + $0.TypedKey get localConversationRecordKey => $_getN(3); @$pb.TagNumber(4) - set localConversationRecordKey($1.TypedKey v) { setField(4, v); } + set localConversationRecordKey($0.TypedKey v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasLocalConversationRecordKey() => $_has(3); @$pb.TagNumber(4) void clearLocalConversationRecordKey() => clearField(4); @$pb.TagNumber(4) - $1.TypedKey ensureLocalConversationRecordKey() => $_ensure(3); + $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(3); @$pb.TagNumber(5) $fixnum.Int64 get expiration => $_getI64(4); diff --git a/lib/proto/veilidchat.pbenum.dart b/lib/proto/veilidchat.pbenum.dart index 7bef00f..9133788 100644 --- a/lib/proto/veilidchat.pbenum.dart +++ b/lib/proto/veilidchat.pbenum.dart @@ -13,23 +13,6 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; -class AttachmentKind extends $pb.ProtobufEnum { - static const AttachmentKind ATTACHMENT_KIND_UNSPECIFIED = AttachmentKind._(0, _omitEnumNames ? '' : 'ATTACHMENT_KIND_UNSPECIFIED'); - static const AttachmentKind ATTACHMENT_KIND_FILE = AttachmentKind._(1, _omitEnumNames ? '' : 'ATTACHMENT_KIND_FILE'); - static const AttachmentKind ATTACHMENT_KIND_IMAGE = AttachmentKind._(2, _omitEnumNames ? '' : 'ATTACHMENT_KIND_IMAGE'); - - static const $core.List values = [ - ATTACHMENT_KIND_UNSPECIFIED, - ATTACHMENT_KIND_FILE, - ATTACHMENT_KIND_IMAGE, - ]; - - static final $core.Map<$core.int, AttachmentKind> _byValue = $pb.ProtobufEnum.initByValue(values); - static AttachmentKind? valueOf($core.int value) => _byValue[value]; - - const AttachmentKind._($core.int v, $core.String n) : super(v, n); -} - class Availability extends $pb.ProtobufEnum { static const Availability AVAILABILITY_UNSPECIFIED = Availability._(0, _omitEnumNames ? '' : 'AVAILABILITY_UNSPECIFIED'); static const Availability AVAILABILITY_OFFLINE = Availability._(1, _omitEnumNames ? '' : 'AVAILABILITY_OFFLINE'); @@ -51,23 +34,6 @@ class Availability extends $pb.ProtobufEnum { const Availability._($core.int v, $core.String n) : super(v, n); } -class ChatType extends $pb.ProtobufEnum { - static const ChatType CHAT_TYPE_UNSPECIFIED = ChatType._(0, _omitEnumNames ? '' : 'CHAT_TYPE_UNSPECIFIED'); - static const ChatType SINGLE_CONTACT = ChatType._(1, _omitEnumNames ? '' : 'SINGLE_CONTACT'); - static const ChatType GROUP = ChatType._(2, _omitEnumNames ? '' : 'GROUP'); - - static const $core.List values = [ - CHAT_TYPE_UNSPECIFIED, - SINGLE_CONTACT, - GROUP, - ]; - - static final $core.Map<$core.int, ChatType> _byValue = $pb.ProtobufEnum.initByValue(values); - static ChatType? valueOf($core.int value) => _byValue[value]; - - const ChatType._($core.int v, $core.String n) : super(v, n); -} - class EncryptionKeyType extends $pb.ProtobufEnum { static const EncryptionKeyType ENCRYPTION_KEY_TYPE_UNSPECIFIED = EncryptionKeyType._(0, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_UNSPECIFIED'); static const EncryptionKeyType ENCRYPTION_KEY_TYPE_NONE = EncryptionKeyType._(1, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_NONE'); @@ -87,5 +53,26 @@ class EncryptionKeyType extends $pb.ProtobufEnum { const EncryptionKeyType._($core.int v, $core.String n) : super(v, n); } +class Scope extends $pb.ProtobufEnum { + static const Scope WATCHERS = Scope._(0, _omitEnumNames ? '' : 'WATCHERS'); + static const Scope MODERATED = Scope._(1, _omitEnumNames ? '' : 'MODERATED'); + static const Scope TALKERS = Scope._(2, _omitEnumNames ? '' : 'TALKERS'); + static const Scope MODERATORS = Scope._(3, _omitEnumNames ? '' : 'MODERATORS'); + static const Scope ADMINS = Scope._(4, _omitEnumNames ? '' : 'ADMINS'); + + static const $core.List values = [ + WATCHERS, + MODERATED, + TALKERS, + MODERATORS, + ADMINS, + ]; + + static final $core.Map<$core.int, Scope> _byValue = $pb.ProtobufEnum.initByValue(values); + static Scope? valueOf($core.int value) => _byValue[value]; + + const Scope._($core.int v, $core.String n) : super(v, n); +} + const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index 7aea1fb..2aaecb2 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -13,21 +13,6 @@ import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; -@$core.Deprecated('Use attachmentKindDescriptor instead') -const AttachmentKind$json = { - '1': 'AttachmentKind', - '2': [ - {'1': 'ATTACHMENT_KIND_UNSPECIFIED', '2': 0}, - {'1': 'ATTACHMENT_KIND_FILE', '2': 1}, - {'1': 'ATTACHMENT_KIND_IMAGE', '2': 2}, - ], -}; - -/// Descriptor for `AttachmentKind`. Decode as a `google.protobuf.EnumDescriptorProto`. -final $typed_data.Uint8List attachmentKindDescriptor = $convert.base64Decode( - 'Cg5BdHRhY2htZW50S2luZBIfChtBVFRBQ0hNRU5UX0tJTkRfVU5TUEVDSUZJRUQQABIYChRBVF' - 'RBQ0hNRU5UX0tJTkRfRklMRRABEhkKFUFUVEFDSE1FTlRfS0lORF9JTUFHRRAC'); - @$core.Deprecated('Use availabilityDescriptor instead') const Availability$json = { '1': 'Availability', @@ -46,21 +31,6 @@ final $typed_data.Uint8List availabilityDescriptor = $convert.base64Decode( 'lMSVRZX09GRkxJTkUQARIVChFBVkFJTEFCSUxJVFlfRlJFRRACEhUKEUFWQUlMQUJJTElUWV9C' 'VVNZEAMSFQoRQVZBSUxBQklMSVRZX0FXQVkQBA=='); -@$core.Deprecated('Use chatTypeDescriptor instead') -const ChatType$json = { - '1': 'ChatType', - '2': [ - {'1': 'CHAT_TYPE_UNSPECIFIED', '2': 0}, - {'1': 'SINGLE_CONTACT', '2': 1}, - {'1': 'GROUP', '2': 2}, - ], -}; - -/// Descriptor for `ChatType`. Decode as a `google.protobuf.EnumDescriptorProto`. -final $typed_data.Uint8List chatTypeDescriptor = $convert.base64Decode( - 'CghDaGF0VHlwZRIZChVDSEFUX1RZUEVfVU5TUEVDSUZJRUQQABISCg5TSU5HTEVfQ09OVEFDVB' - 'ABEgkKBUdST1VQEAI='); - @$core.Deprecated('Use encryptionKeyTypeDescriptor instead') const EncryptionKeyType$json = { '1': 'EncryptionKeyType', @@ -78,43 +48,249 @@ final $typed_data.Uint8List encryptionKeyTypeDescriptor = $convert.base64Decode( 'ASHAoYRU5DUllQVElPTl9LRVlfVFlQRV9OT05FEAESGwoXRU5DUllQVElPTl9LRVlfVFlQRV9Q' 'SU4QAhIgChxFTkNSWVBUSU9OX0tFWV9UWVBFX1BBU1NXT1JEEAM='); +@$core.Deprecated('Use scopeDescriptor instead') +const Scope$json = { + '1': 'Scope', + '2': [ + {'1': 'WATCHERS', '2': 0}, + {'1': 'MODERATED', '2': 1}, + {'1': 'TALKERS', '2': 2}, + {'1': 'MODERATORS', '2': 3}, + {'1': 'ADMINS', '2': 4}, + ], +}; + +/// Descriptor for `Scope`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List scopeDescriptor = $convert.base64Decode( + 'CgVTY29wZRIMCghXQVRDSEVSUxAAEg0KCU1PREVSQVRFRBABEgsKB1RBTEtFUlMQAhIOCgpNT0' + 'RFUkFUT1JTEAMSCgoGQURNSU5TEAQ='); + @$core.Deprecated('Use attachmentDescriptor instead') const Attachment$json = { '1': 'Attachment', '2': [ - {'1': 'kind', '3': 1, '4': 1, '5': 14, '6': '.veilidchat.AttachmentKind', '10': 'kind'}, - {'1': 'mime', '3': 2, '4': 1, '5': 9, '10': 'mime'}, - {'1': 'name', '3': 3, '4': 1, '5': 9, '10': 'name'}, - {'1': 'content', '3': 4, '4': 1, '5': 11, '6': '.dht.DataReference', '10': 'content'}, - {'1': 'signature', '3': 5, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, + {'1': 'media', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.AttachmentMedia', '9': 0, '10': 'media'}, + {'1': 'signature', '3': 2, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, + ], + '8': [ + {'1': 'kind'}, ], }; /// Descriptor for `Attachment`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List attachmentDescriptor = $convert.base64Decode( - 'CgpBdHRhY2htZW50Ei4KBGtpbmQYASABKA4yGi52ZWlsaWRjaGF0LkF0dGFjaG1lbnRLaW5kUg' - 'RraW5kEhIKBG1pbWUYAiABKAlSBG1pbWUSEgoEbmFtZRgDIAEoCVIEbmFtZRIsCgdjb250ZW50' - 'GAQgASgLMhIuZGh0LkRhdGFSZWZlcmVuY2VSB2NvbnRlbnQSLwoJc2lnbmF0dXJlGAUgASgLMh' - 'EudmVpbGlkLlNpZ25hdHVyZVIJc2lnbmF0dXJl'); + 'CgpBdHRhY2htZW50EjMKBW1lZGlhGAEgASgLMhsudmVpbGlkY2hhdC5BdHRhY2htZW50TWVkaW' + 'FIAFIFbWVkaWESLwoJc2lnbmF0dXJlGAIgASgLMhEudmVpbGlkLlNpZ25hdHVyZVIJc2lnbmF0' + 'dXJlQgYKBGtpbmQ='); + +@$core.Deprecated('Use attachmentMediaDescriptor instead') +const AttachmentMedia$json = { + '1': 'AttachmentMedia', + '2': [ + {'1': 'mime', '3': 1, '4': 1, '5': 9, '10': 'mime'}, + {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'}, + {'1': 'content', '3': 3, '4': 1, '5': 11, '6': '.dht.DataReference', '10': 'content'}, + ], +}; + +/// Descriptor for `AttachmentMedia`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List attachmentMediaDescriptor = $convert.base64Decode( + 'Cg9BdHRhY2htZW50TWVkaWESEgoEbWltZRgBIAEoCVIEbWltZRISCgRuYW1lGAIgASgJUgRuYW' + '1lEiwKB2NvbnRlbnQYAyABKAsyEi5kaHQuRGF0YVJlZmVyZW5jZVIHY29udGVudA=='); + +@$core.Deprecated('Use permissionsDescriptor instead') +const Permissions$json = { + '1': 'Permissions', + '2': [ + {'1': 'can_add_members', '3': 1, '4': 1, '5': 14, '6': '.veilidchat.Scope', '10': 'canAddMembers'}, + {'1': 'can_edit_info', '3': 2, '4': 1, '5': 14, '6': '.veilidchat.Scope', '10': 'canEditInfo'}, + {'1': 'moderated', '3': 3, '4': 1, '5': 8, '10': 'moderated'}, + ], +}; + +/// Descriptor for `Permissions`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List permissionsDescriptor = $convert.base64Decode( + 'CgtQZXJtaXNzaW9ucxI5Cg9jYW5fYWRkX21lbWJlcnMYASABKA4yES52ZWlsaWRjaGF0LlNjb3' + 'BlUg1jYW5BZGRNZW1iZXJzEjUKDWNhbl9lZGl0X2luZm8YAiABKA4yES52ZWlsaWRjaGF0LlNj' + 'b3BlUgtjYW5FZGl0SW5mbxIcCgltb2RlcmF0ZWQYAyABKAhSCW1vZGVyYXRlZA=='); + +@$core.Deprecated('Use membershipDescriptor instead') +const Membership$json = { + '1': 'Membership', + '2': [ + {'1': 'watchers', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'watchers'}, + {'1': 'moderated', '3': 2, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'moderated'}, + {'1': 'talkers', '3': 3, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'talkers'}, + {'1': 'moderators', '3': 4, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'moderators'}, + {'1': 'admins', '3': 5, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'admins'}, + ], +}; + +/// Descriptor for `Membership`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List membershipDescriptor = $convert.base64Decode( + 'CgpNZW1iZXJzaGlwEiwKCHdhdGNoZXJzGAEgAygLMhAudmVpbGlkLlR5cGVkS2V5Ugh3YXRjaG' + 'VycxIuCgltb2RlcmF0ZWQYAiADKAsyEC52ZWlsaWQuVHlwZWRLZXlSCW1vZGVyYXRlZBIqCgd0' + 'YWxrZXJzGAMgAygLMhAudmVpbGlkLlR5cGVkS2V5Ugd0YWxrZXJzEjAKCm1vZGVyYXRvcnMYBC' + 'ADKAsyEC52ZWlsaWQuVHlwZWRLZXlSCm1vZGVyYXRvcnMSKAoGYWRtaW5zGAUgAygLMhAudmVp' + 'bGlkLlR5cGVkS2V5UgZhZG1pbnM='); + +@$core.Deprecated('Use chatSettingsDescriptor instead') +const ChatSettings$json = { + '1': 'ChatSettings', + '2': [ + {'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'}, + {'1': 'description', '3': 2, '4': 1, '5': 9, '10': 'description'}, + {'1': 'icon', '3': 3, '4': 1, '5': 11, '6': '.dht.DataReference', '9': 0, '10': 'icon', '17': true}, + {'1': 'default_expiration', '3': 4, '4': 1, '5': 4, '10': 'defaultExpiration'}, + ], + '8': [ + {'1': '_icon'}, + ], +}; + +/// Descriptor for `ChatSettings`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List chatSettingsDescriptor = $convert.base64Decode( + 'CgxDaGF0U2V0dGluZ3MSFAoFdGl0bGUYASABKAlSBXRpdGxlEiAKC2Rlc2NyaXB0aW9uGAIgAS' + 'gJUgtkZXNjcmlwdGlvbhIrCgRpY29uGAMgASgLMhIuZGh0LkRhdGFSZWZlcmVuY2VIAFIEaWNv' + 'bogBARItChJkZWZhdWx0X2V4cGlyYXRpb24YBCABKARSEWRlZmF1bHRFeHBpcmF0aW9uQgcKBV' + '9pY29u'); @$core.Deprecated('Use messageDescriptor instead') const Message$json = { '1': 'Message', '2': [ - {'1': 'author', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'author'}, - {'1': 'timestamp', '3': 2, '4': 1, '5': 4, '10': 'timestamp'}, - {'1': 'text', '3': 3, '4': 1, '5': 9, '10': 'text'}, - {'1': 'signature', '3': 4, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, - {'1': 'attachments', '3': 5, '4': 3, '5': 11, '6': '.veilidchat.Attachment', '10': 'attachments'}, + {'1': 'id', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'id'}, + {'1': 'author', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'author'}, + {'1': 'timestamp', '3': 3, '4': 1, '5': 4, '10': 'timestamp'}, + {'1': 'text', '3': 4, '4': 1, '5': 11, '6': '.veilidchat.Message.Text', '9': 0, '10': 'text'}, + {'1': 'secret', '3': 5, '4': 1, '5': 11, '6': '.veilidchat.Message.Secret', '9': 0, '10': 'secret'}, + {'1': 'delete', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlDelete', '9': 0, '10': 'delete'}, + {'1': 'clear', '3': 7, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlClear', '9': 0, '10': 'clear'}, + {'1': 'settings', '3': 8, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlSettings', '9': 0, '10': 'settings'}, + {'1': 'permissions', '3': 9, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlPermissions', '9': 0, '10': 'permissions'}, + {'1': 'membership', '3': 10, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlMembership', '9': 0, '10': 'membership'}, + {'1': 'moderation', '3': 11, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlModeration', '9': 0, '10': 'moderation'}, + {'1': 'signature', '3': 12, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, + ], + '3': [Message_Text$json, Message_Secret$json, Message_ControlDelete$json, Message_ControlClear$json, Message_ControlSettings$json, Message_ControlPermissions$json, Message_ControlMembership$json, Message_ControlModeration$json], + '8': [ + {'1': 'kind'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_Text$json = { + '1': 'Text', + '2': [ + {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'}, + {'1': 'topic', '3': 2, '4': 1, '5': 9, '10': 'topic'}, + {'1': 'reply_id', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'replyId'}, + {'1': 'expiration', '3': 4, '4': 1, '5': 4, '10': 'expiration'}, + {'1': 'view_limit', '3': 5, '4': 1, '5': 4, '10': 'viewLimit'}, + {'1': 'attachments', '3': 6, '4': 3, '5': 11, '6': '.veilidchat.Attachment', '10': 'attachments'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_Secret$json = { + '1': 'Secret', + '2': [ + {'1': 'ciphertext', '3': 1, '4': 1, '5': 12, '10': 'ciphertext'}, + {'1': 'expiration', '3': 2, '4': 1, '5': 4, '10': 'expiration'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlDelete$json = { + '1': 'ControlDelete', + '2': [ + {'1': 'ids', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'ids'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlClear$json = { + '1': 'ControlClear', + '2': [ + {'1': 'timestamp', '3': 1, '4': 1, '5': 4, '10': 'timestamp'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlSettings$json = { + '1': 'ControlSettings', + '2': [ + {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlPermissions$json = { + '1': 'ControlPermissions', + '2': [ + {'1': 'permissions', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Permissions', '10': 'permissions'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlMembership$json = { + '1': 'ControlMembership', + '2': [ + {'1': 'membership', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Membership', '10': 'membership'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlModeration$json = { + '1': 'ControlModeration', + '2': [ + {'1': 'accepted_ids', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'acceptedIds'}, + {'1': 'rejected_ids', '3': 2, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'rejectedIds'}, ], }; /// Descriptor for `Message`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List messageDescriptor = $convert.base64Decode( - 'CgdNZXNzYWdlEigKBmF1dGhvchgBIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIGYXV0aG9yEhwKCX' - 'RpbWVzdGFtcBgCIAEoBFIJdGltZXN0YW1wEhIKBHRleHQYAyABKAlSBHRleHQSLwoJc2lnbmF0' - 'dXJlGAQgASgLMhEudmVpbGlkLlNpZ25hdHVyZVIJc2lnbmF0dXJlEjgKC2F0dGFjaG1lbnRzGA' - 'UgAygLMhYudmVpbGlkY2hhdC5BdHRhY2htZW50UgthdHRhY2htZW50cw=='); + 'CgdNZXNzYWdlEiAKAmlkGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5UgJpZBIoCgZhdXRob3IYAi' + 'ABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBmF1dGhvchIcCgl0aW1lc3RhbXAYAyABKARSCXRpbWVz' + 'dGFtcBIuCgR0ZXh0GAQgASgLMhgudmVpbGlkY2hhdC5NZXNzYWdlLlRleHRIAFIEdGV4dBI0Cg' + 'ZzZWNyZXQYBSABKAsyGi52ZWlsaWRjaGF0Lk1lc3NhZ2UuU2VjcmV0SABSBnNlY3JldBI7CgZk' + 'ZWxldGUYBiABKAsyIS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbERlbGV0ZUgAUgZkZWxldG' + 'USOAoFY2xlYXIYByABKAsyIC52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbENsZWFySABSBWNs' + 'ZWFyEkEKCHNldHRpbmdzGAggASgLMiMudmVpbGlkY2hhdC5NZXNzYWdlLkNvbnRyb2xTZXR0aW' + '5nc0gAUghzZXR0aW5ncxJKCgtwZXJtaXNzaW9ucxgJIAEoCzImLnZlaWxpZGNoYXQuTWVzc2Fn' + 'ZS5Db250cm9sUGVybWlzc2lvbnNIAFILcGVybWlzc2lvbnMSRwoKbWVtYmVyc2hpcBgKIAEoCz' + 'IlLnZlaWxpZGNoYXQuTWVzc2FnZS5Db250cm9sTWVtYmVyc2hpcEgAUgptZW1iZXJzaGlwEkcK' + 'Cm1vZGVyYXRpb24YCyABKAsyJS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbE1vZGVyYXRpb2' + '5IAFIKbW9kZXJhdGlvbhIvCglzaWduYXR1cmUYDCABKAsyES52ZWlsaWQuU2lnbmF0dXJlUglz' + 'aWduYXR1cmUa1gEKBFRleHQSEgoEdGV4dBgBIAEoCVIEdGV4dBIUCgV0b3BpYxgCIAEoCVIFdG' + '9waWMSKwoIcmVwbHlfaWQYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSB3JlcGx5SWQSHgoKZXhw' + 'aXJhdGlvbhgEIAEoBFIKZXhwaXJhdGlvbhIdCgp2aWV3X2xpbWl0GAUgASgEUgl2aWV3TGltaX' + 'QSOAoLYXR0YWNobWVudHMYBiADKAsyFi52ZWlsaWRjaGF0LkF0dGFjaG1lbnRSC2F0dGFjaG1l' + 'bnRzGkgKBlNlY3JldBIeCgpjaXBoZXJ0ZXh0GAEgASgMUgpjaXBoZXJ0ZXh0Eh4KCmV4cGlyYX' + 'Rpb24YAiABKARSCmV4cGlyYXRpb24aMwoNQ29udHJvbERlbGV0ZRIiCgNpZHMYASADKAsyEC52' + 'ZWlsaWQuVHlwZWRLZXlSA2lkcxosCgxDb250cm9sQ2xlYXISHAoJdGltZXN0YW1wGAEgASgEUg' + 'l0aW1lc3RhbXAaRwoPQ29udHJvbFNldHRpbmdzEjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlk' + 'Y2hhdC5DaGF0U2V0dGluZ3NSCHNldHRpbmdzGk8KEkNvbnRyb2xQZXJtaXNzaW9ucxI5CgtwZX' + 'JtaXNzaW9ucxgBIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbnNSC3Blcm1pc3Npb25zGksK' + 'EUNvbnRyb2xNZW1iZXJzaGlwEjYKCm1lbWJlcnNoaXAYASABKAsyFi52ZWlsaWRjaGF0Lk1lbW' + 'JlcnNoaXBSCm1lbWJlcnNoaXAafQoRQ29udHJvbE1vZGVyYXRpb24SMwoMYWNjZXB0ZWRfaWRz' + 'GAEgAygLMhAudmVpbGlkLlR5cGVkS2V5UgthY2NlcHRlZElkcxIzCgxyZWplY3RlZF9pZHMYAi' + 'ADKAsyEC52ZWlsaWQuVHlwZWRLZXlSC3JlamVjdGVkSWRzQgYKBGtpbmQ='); + +@$core.Deprecated('Use reconciledMessageDescriptor instead') +const ReconciledMessage$json = { + '1': 'ReconciledMessage', + '2': [ + {'1': 'content', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Message', '10': 'content'}, + {'1': 'reconciled_time', '3': 2, '4': 1, '5': 4, '10': 'reconciledTime'}, + ], +}; + +/// Descriptor for `ReconciledMessage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List reconciledMessageDescriptor = $convert.base64Decode( + 'ChFSZWNvbmNpbGVkTWVzc2FnZRItCgdjb250ZW50GAEgASgLMhMudmVpbGlkY2hhdC5NZXNzYW' + 'dlUgdjb250ZW50EicKD3JlY29uY2lsZWRfdGltZRgCIAEoBFIOcmVjb25jaWxlZFRpbWU='); @$core.Deprecated('Use conversationDescriptor instead') const Conversation$json = { @@ -132,6 +308,91 @@ final $typed_data.Uint8List conversationDescriptor = $convert.base64Decode( 'JvZmlsZRIwChRpZGVudGl0eV9tYXN0ZXJfanNvbhgCIAEoCVISaWRlbnRpdHlNYXN0ZXJKc29u' 'EiwKCG1lc3NhZ2VzGAMgASgLMhAudmVpbGlkLlR5cGVkS2V5UghtZXNzYWdlcw=='); +@$core.Deprecated('Use chatDescriptor instead') +const Chat$json = { + '1': 'Chat', + '2': [ + {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, + {'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, + {'1': 'remote_conversation_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, + ], +}; + +/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List chatDescriptor = $convert.base64Decode( + 'CgRDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3NSCHNldH' + 'RpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVpbGlkLlR5' + 'cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRJVCh5yZW1vdGVfY29udmVyc2F0aW' + '9uX3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdGlv' + 'blJlY29yZEtleQ=='); + +@$core.Deprecated('Use groupChatDescriptor instead') +const GroupChat$json = { + '1': 'GroupChat', + '2': [ + {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, + {'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, + {'1': 'remote_conversation_record_keys', '3': 3, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKeys'}, + ], +}; + +/// Descriptor for `GroupChat`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List groupChatDescriptor = $convert.base64Decode( + 'CglHcm91cENoYXQSNAoIc2V0dGluZ3MYASABKAsyGC52ZWlsaWRjaGF0LkNoYXRTZXR0aW5nc1' + 'IIc2V0dGluZ3MSUwodbG9jYWxfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWls' + 'aWQuVHlwZWRLZXlSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5ElcKH3JlbW90ZV9jb252ZX' + 'JzYXRpb25fcmVjb3JkX2tleXMYAyADKAsyEC52ZWlsaWQuVHlwZWRLZXlSHHJlbW90ZUNvbnZl' + 'cnNhdGlvblJlY29yZEtleXM='); + +@$core.Deprecated('Use profileDescriptor instead') +const Profile$json = { + '1': 'Profile', + '2': [ + {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'}, + {'1': 'pronouns', '3': 2, '4': 1, '5': 9, '10': 'pronouns'}, + {'1': 'about', '3': 3, '4': 1, '5': 9, '10': 'about'}, + {'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'}, + {'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'}, + {'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '9': 0, '10': 'avatar', '17': true}, + ], + '8': [ + {'1': '_avatar'}, + ], +}; + +/// Descriptor for `Profile`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List profileDescriptor = $convert.base64Decode( + 'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW' + '5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp' + 'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ei' + '0KBmF2YXRhchgGIAEoCzIQLnZlaWxpZC5UeXBlZEtleUgAUgZhdmF0YXKIAQFCCQoHX2F2YXRh' + 'cg=='); + +@$core.Deprecated('Use accountDescriptor instead') +const Account$json = { + '1': 'Account', + '2': [ + {'1': 'profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'}, + {'1': 'invisible', '3': 2, '4': 1, '5': 8, '10': 'invisible'}, + {'1': 'auto_away_timeout_sec', '3': 3, '4': 1, '5': 13, '10': 'autoAwayTimeoutSec'}, + {'1': 'contact_list', '3': 4, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactList'}, + {'1': 'contact_invitation_records', '3': 5, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactInvitationRecords'}, + {'1': 'chat_list', '3': 6, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'chatList'}, + {'1': 'group_chat_list', '3': 7, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'groupChatList'}, + ], +}; + +/// Descriptor for `Account`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List accountDescriptor = $convert.base64Decode( + 'CgdBY2NvdW50Ei0KB3Byb2ZpbGUYASABKAsyEy52ZWlsaWRjaGF0LlByb2ZpbGVSB3Byb2ZpbG' + 'USHAoJaW52aXNpYmxlGAIgASgIUglpbnZpc2libGUSMQoVYXV0b19hd2F5X3RpbWVvdXRfc2Vj' + 'GAMgASgNUhJhdXRvQXdheVRpbWVvdXRTZWMSPQoMY29udGFjdF9saXN0GAQgASgLMhouZGh0Lk' + '93bmVkREhUUmVjb3JkUG9pbnRlclILY29udGFjdExpc3QSWAoaY29udGFjdF9pbnZpdGF0aW9u' + 'X3JlY29yZHMYBSABKAsyGi5kaHQuT3duZWRESFRSZWNvcmRQb2ludGVyUhhjb250YWN0SW52aX' + 'RhdGlvblJlY29yZHMSNwoJY2hhdF9saXN0GAYgASgLMhouZGh0Lk93bmVkREhUUmVjb3JkUG9p' + 'bnRlclIIY2hhdExpc3QSQgoPZ3JvdXBfY2hhdF9saXN0GAcgASgLMhouZGh0Lk93bmVkREhUUm' + 'Vjb3JkUG9pbnRlclINZ3JvdXBDaGF0TGlzdA=='); + @$core.Deprecated('Use contactDescriptor instead') const Contact$json = { '1': 'Contact', @@ -158,68 +419,6 @@ final $typed_data.Uint8List contactDescriptor = $convert.base64Decode( 'lSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5EisKEXNob3dfYXZhaWxhYmlsaXR5GAcgASgI' 'UhBzaG93QXZhaWxhYmlsaXR5'); -@$core.Deprecated('Use profileDescriptor instead') -const Profile$json = { - '1': 'Profile', - '2': [ - {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'}, - {'1': 'pronouns', '3': 2, '4': 1, '5': 9, '10': 'pronouns'}, - {'1': 'status', '3': 3, '4': 1, '5': 9, '10': 'status'}, - {'1': 'availability', '3': 4, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'}, - {'1': 'avatar', '3': 5, '4': 1, '5': 11, '6': '.veilid.TypedKey', '9': 0, '10': 'avatar', '17': true}, - ], - '8': [ - {'1': '_avatar'}, - ], -}; - -/// Descriptor for `Profile`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List profileDescriptor = $convert.base64Decode( - 'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW' - '5zEhYKBnN0YXR1cxgDIAEoCVIGc3RhdHVzEjwKDGF2YWlsYWJpbGl0eRgEIAEoDjIYLnZlaWxp' - 'ZGNoYXQuQXZhaWxhYmlsaXR5UgxhdmFpbGFiaWxpdHkSLQoGYXZhdGFyGAUgASgLMhAudmVpbG' - 'lkLlR5cGVkS2V5SABSBmF2YXRhcogBAUIJCgdfYXZhdGFy'); - -@$core.Deprecated('Use chatDescriptor instead') -const Chat$json = { - '1': 'Chat', - '2': [ - {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.veilidchat.ChatType', '10': 'type'}, - {'1': 'remote_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, - {'1': 'reconciled_chat_record', '3': 3, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'reconciledChatRecord'}, - ], -}; - -/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List chatDescriptor = $convert.base64Decode( - 'CgRDaGF0EigKBHR5cGUYASABKA4yFC52ZWlsaWRjaGF0LkNoYXRUeXBlUgR0eXBlElUKHnJlbW' - '90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleRgCIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVt' - 'b3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5ElAKFnJlY29uY2lsZWRfY2hhdF9yZWNvcmQYAyABKA' - 'syGi5kaHQuT3duZWRESFRSZWNvcmRQb2ludGVyUhRyZWNvbmNpbGVkQ2hhdFJlY29yZA=='); - -@$core.Deprecated('Use accountDescriptor instead') -const Account$json = { - '1': 'Account', - '2': [ - {'1': 'profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'}, - {'1': 'invisible', '3': 2, '4': 1, '5': 8, '10': 'invisible'}, - {'1': 'auto_away_timeout_sec', '3': 3, '4': 1, '5': 13, '10': 'autoAwayTimeoutSec'}, - {'1': 'contact_list', '3': 4, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactList'}, - {'1': 'contact_invitation_records', '3': 5, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactInvitationRecords'}, - {'1': 'chat_list', '3': 6, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'chatList'}, - ], -}; - -/// Descriptor for `Account`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List accountDescriptor = $convert.base64Decode( - 'CgdBY2NvdW50Ei0KB3Byb2ZpbGUYASABKAsyEy52ZWlsaWRjaGF0LlByb2ZpbGVSB3Byb2ZpbG' - 'USHAoJaW52aXNpYmxlGAIgASgIUglpbnZpc2libGUSMQoVYXV0b19hd2F5X3RpbWVvdXRfc2Vj' - 'GAMgASgNUhJhdXRvQXdheVRpbWVvdXRTZWMSPQoMY29udGFjdF9saXN0GAQgASgLMhouZGh0Lk' - '93bmVkREhUUmVjb3JkUG9pbnRlclILY29udGFjdExpc3QSWAoaY29udGFjdF9pbnZpdGF0aW9u' - 'X3JlY29yZHMYBSABKAsyGi5kaHQuT3duZWRESFRSZWNvcmRQb2ludGVyUhhjb250YWN0SW52aX' - 'RhdGlvblJlY29yZHMSNwoJY2hhdF9saXN0GAYgASgLMhouZGh0Lk93bmVkREhUUmVjb3JkUG9p' - 'bnRlclIIY2hhdExpc3Q='); - @$core.Deprecated('Use contactInvitationDescriptor instead') const ContactInvitation$json = { '1': 'ContactInvitation', diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 42692ac..eb6d08a 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -1,51 +1,230 @@ +//////////////////////////////////////////////////////////////////////////////////// +// VeilidChat Protocol Buffer Definitions +// +// * Timestamps are in microseconds (us) since epoch +// * Durations are in microseconds (us) +//////////////////////////////////////////////////////////////////////////////////// + syntax = "proto3"; package veilidchat; import "veilid.proto"; import "dht.proto"; -// AttachmentKind -// Enumeration of well-known attachment types -enum AttachmentKind { - ATTACHMENT_KIND_UNSPECIFIED = 0; - ATTACHMENT_KIND_FILE = 1; - ATTACHMENT_KIND_IMAGE = 2; +//////////////////////////////////////////////////////////////////////////////////// +// Enumerations +//////////////////////////////////////////////////////////////////////////////////// + +// Contact availability +enum Availability { + AVAILABILITY_UNSPECIFIED = 0; + AVAILABILITY_OFFLINE = 1; + AVAILABILITY_FREE = 2; + AVAILABILITY_BUSY = 3; + AVAILABILITY_AWAY = 4; } +// Encryption used on secret keys +enum EncryptionKeyType { + ENCRYPTION_KEY_TYPE_UNSPECIFIED = 0; + ENCRYPTION_KEY_TYPE_NONE = 1; + ENCRYPTION_KEY_TYPE_PIN = 2; + ENCRYPTION_KEY_TYPE_PASSWORD = 3; +} + +// Scope of a chat +enum Scope { + // Can read chats but not send messages + WATCHERS = 0; + // Can send messages subject to moderation + // If moderation is disabled, this is equivalent to WATCHERS + MODERATED = 1; + // Can send messages without moderation + TALKERS = 2; + // Can moderate messages sent my members if moderation is enabled + MODERATORS = 3; + // Can perform all actions + ADMINS = 4; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Attachments +//////////////////////////////////////////////////////////////////////////////////// + // A single attachment message Attachment { - // Type of the data - AttachmentKind kind = 1; - // MIME type of the data - string mime = 2; - // Title or filename - string name = 3; - // Pointer to the data content - dht.DataReference content = 4; + oneof kind { + AttachmentMedia media = 1; + } // Author signature over all attachment fields and content fields and bytes - veilid.Signature signature = 5; + veilid.Signature signature = 2; } +// A file, audio, image, or video attachment +message AttachmentMedia { + // MIME type of the data + string mime = 1; + // Title or filename + string name = 2; + // Pointer to the data content + dht.DataReference content = 3; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// Chat room controls +//////////////////////////////////////////////////////////////////////////////////// + +// Permissions of a chat +message Permissions { + // Parties in this scope or higher can add members to their own group or lower + Scope can_add_members = 1; + // Parties in this scope or higher can change the 'info' of a group + Scope can_edit_info = 2; + // If moderation is enabled or not. + bool moderated = 3; +} + +// The membership of a chat +message Membership { + // Conversation keys for parties in the 'watchers' group + repeated veilid.TypedKey watchers = 1; + // Conversation keys for parties in the 'moderated' group + repeated veilid.TypedKey moderated = 2; + // Conversation keys for parties in the 'talkers' group + repeated veilid.TypedKey talkers = 3; + // Conversation keys for parties in the 'moderators' group + repeated veilid.TypedKey moderators = 4; + // Conversation keys for parties in the 'admins' group + repeated veilid.TypedKey admins = 5; +} + +// The chat settings +message ChatSettings { + // Title for the chat + string title = 1; + // Description for the chat + string description = 2; + // Icon for the chat + optional dht.DataReference icon = 3; + // Default message expiration duration (in us) + uint64 default_expiration = 4; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Messages +//////////////////////////////////////////////////////////////////////////////////// + // A single message as part of a series of messages message Message { - // Author of the message - veilid.TypedKey author = 1; - // Time the message was sent (us since epoch) - uint64 timestamp = 2; - // Text of the message - string text = 3; + + // A text message + message Text { + // Text of the message + string text = 1; + // Topic of the message / Content warning + string topic = 2; + // Message id replied to + veilid.TypedKey reply_id = 3; + // Message expiration timestamp + uint64 expiration = 4; + // Message view limit before deletion + uint64 view_limit = 5; + // Attachments on the message + repeated Attachment attachments = 6; + } + + // A secret message + message Secret { + // Text message protobuf encrypted by a key + bytes ciphertext = 1; + // Secret expiration timestamp + // This is the time after which an un-revealed secret will get deleted + uint64 expiration = 2; + } + + // A 'delete' control message + // Deletes a set of messages by their ids + message ControlDelete { + repeated veilid.TypedKey ids = 1; + } + // A 'clear' control message + // Deletes a set of messages from before some timestamp + message ControlClear { + // The latest timestamp to delete messages before + // If this is zero then all messages are cleared + uint64 timestamp = 1; + } + // A 'change settings' control message + message ControlSettings { + ChatSettings settings = 1; + } + + // A 'change permissions' control message + // Changes the permissions of a chat + message ControlPermissions { + Permissions permissions = 1; + } + + // A 'change membership' control message + // Changes the + message ControlMembership { + Membership membership = 1; + } + + // A 'moderation' control message + // Accepts or rejects a set of messages + message ControlModeration { + repeated veilid.TypedKey accepted_ids = 1; + repeated veilid.TypedKey rejected_ids = 2; + } + + ////////////////////////////////////////////////////////////////////////// + + // Hash of previous message from the same author, + // including its previous hash. + // Also serves as a unique key for the message. + veilid.TypedKey id = 1; + // Author of the message (identity public key) + veilid.TypedKey author = 2; + // Time the message was sent according to sender + uint64 timestamp = 3; + + // Message kind + oneof kind { + Text text = 4; + Secret secret = 5; + ControlDelete delete = 6; + ControlClear clear = 7; + ControlSettings settings = 8; + ControlPermissions permissions = 9; + ControlMembership membership = 10; + ControlModeration moderation = 11; + } + // Author signature over all of the fields and attachment signatures - veilid.Signature signature = 4; - // Attachments on the message - repeated Attachment attachments = 5; + veilid.Signature signature = 12; } +// Locally stored messages for chats +message ReconciledMessage { + // The message as sent + Message content = 1; + // The timestamp the message was reconciled + uint64 reconciled_time = 2; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Chats +//////////////////////////////////////////////////////////////////////////////////// + // The means of direct communications that is synchronized between // two users. Visible and encrypted for the other party. // Includes communications for: // * Profile changes // * Identity changes // * 1-1 chat messages +// * Group chat messages // // DHT Schema: SMPL(0,1,[identityPublicKey]) // DHT Key (UnicastOutbox): localConversation @@ -54,12 +233,84 @@ message Message { message Conversation { // Profile to publish to friend Profile profile = 1; - // Identity master (JSON) to publish to friend + // Identity master (JSON) to publish to friend or chat room string identity_master_json = 2; - // Messages DHTLog (xxx for now DHTShortArray) + // Messages DHTLog veilid.TypedKey messages = 3; } +// Either a 1-1 conversation or a group chat +// Privately encrypted, this is the local user's copy of the chat +message Chat { + // Settings + ChatSettings settings = 1; + // Conversation key for this user + veilid.TypedKey local_conversation_record_key = 2; + // Conversation key for the other party + veilid.TypedKey remote_conversation_record_key = 3; +} + +// A group chat +// Privately encrypted, this is the local user's copy of the chat +message GroupChat { + // Settings + ChatSettings settings = 1; + // Conversation key for this user + veilid.TypedKey local_conversation_record_key = 2; + // Conversation keys for the other parties + repeated veilid.TypedKey remote_conversation_record_keys = 3; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Accounts +//////////////////////////////////////////////////////////////////////////////////// + +// Publicly shared profile information for both contacts and accounts +// Contains: +// Name - Friendly name +// Pronouns - Pronouns of user +// Icon - Little picture to represent user in contact list +message Profile { + // Friendy name + string name = 1; + // Pronouns of user + string pronouns = 2; + // Description of the user + string about = 3; + // Status/away message + string status = 4; + // Availability + Availability availability = 5; + // Avatar DHTData + optional veilid.TypedKey avatar = 6; +} + +// A record of an individual account +// Pointed to by the identity account map in the identity key +// +// DHT Schema: DFLT(1) +// DHT Private: accountSecretKey +message Account { + // The user's profile that gets shared with contacts + Profile profile = 1; + // Invisibility makes you always look 'Offline' + bool invisible = 2; + // Auto-away sets 'away' mode after an inactivity time + uint32 auto_away_timeout_sec = 3; + // The contacts DHTList for this account + // DHT Private + dht.OwnedDHTRecordPointer contact_list = 4; + // The ContactInvitationRecord DHTShortArray for this account + // DHT Private + dht.OwnedDHTRecordPointer contact_invitation_records = 5; + // The Chats DHTList for this account + // DHT Private + dht.OwnedDHTRecordPointer chat_list = 6; + // The GroupChats DHTList for this account + // DHT Private + dht.OwnedDHTRecordPointer group_chat_list = 7; +} + // A record of a contact that has accepted a contact invitation // Contains a copy of the most recent remote profile as well as // a locally edited profile. @@ -80,87 +331,13 @@ message Contact { veilid.TypedKey remote_conversation_record_key = 5; // Our conversation key for friend to sync veilid.TypedKey local_conversation_record_key = 6; - // Show availability + // Show availability to this contact bool show_availability = 7; } -// Contact availability -enum Availability { - AVAILABILITY_UNSPECIFIED = 0; - AVAILABILITY_OFFLINE = 1; - AVAILABILITY_FREE = 2; - AVAILABILITY_BUSY = 3; - AVAILABILITY_AWAY = 4; -} - -// Publicly shared profile information for both contacts and accounts -// Contains: -// Name - Friendly name -// Pronouns - Pronouns of user -// Icon - Little picture to represent user in contact list -message Profile { - // Friendy name - string name = 1; - // Pronouns of user - string pronouns = 2; - // Status/away message - string status = 3; - // Availability - Availability availability = 4; - // Avatar DHTData - optional veilid.TypedKey avatar = 5; -} - - -enum ChatType { - CHAT_TYPE_UNSPECIFIED = 0; - SINGLE_CONTACT = 1; - GROUP = 2; -} - -// Either a 1-1 conversation or a group chat (eventually) -// Privately encrypted, this is the local user's copy of the chat -message Chat { - // What kind of chat is this - ChatType type = 1; - // Conversation key for the other party - veilid.TypedKey remote_conversation_record_key = 2; - // Reconciled chat record DHTLog (xxx for now DHTShortArray) - dht.OwnedDHTRecordPointer reconciled_chat_record = 3; -} - -// A record of an individual account -// Pointed to by the identity account map in the identity key -// -// DHT Schema: DFLT(1) -// DHT Private: accountSecretKey -message Account { - // The user's profile that gets shared with contacts - Profile profile = 1; - // Invisibility makes you always look 'Offline' - bool invisible = 2; - // Auto-away sets 'away' mode after an inactivity time - uint32 auto_away_timeout_sec = 3; - // The contacts DHTList for this account - // DHT Private - dht.OwnedDHTRecordPointer contact_list = 4; - // The ContactInvitationRecord DHTShortArray for this account - // DHT Private - dht.OwnedDHTRecordPointer contact_invitation_records = 5; - // The chats DHTList for this account - // DHT Private - dht.OwnedDHTRecordPointer chat_list = 6; - -} - -// EncryptionKeyType -// Encryption of secret -enum EncryptionKeyType { - ENCRYPTION_KEY_TYPE_UNSPECIFIED = 0; - ENCRYPTION_KEY_TYPE_NONE = 1; - ENCRYPTION_KEY_TYPE_PIN = 2; - ENCRYPTION_KEY_TYPE_PASSWORD = 3; -} +//////////////////////////////////////////////////////////////////////////////////// +// Invitations +//////////////////////////////////////////////////////////////////////////////////// // Invitation that is shared for VeilidChat contact connections // serialized to QR code or data blob, not send over DHT, out of band. diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 6edca24..52f26ac 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -44,7 +44,7 @@ Widget waitingPage({String? text}) => Builder(builder: (context) { final theme = Theme.of(context); final scale = theme.extension()!; return ColoredBox( - color: scale.tertiaryScale.primaryText, + color: scale.tertiaryScale.appBackground, child: Center( child: Column(children: [ buildProgressIndicator().expanded(), diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index 83e3dc8..1577d7a 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -7,6 +7,7 @@ import 'fixtures/fixtures.dart'; import 'test_dht_log.dart'; import 'test_dht_record_pool.dart'; import 'test_dht_short_array.dart'; +import 'test_table_db_array.dart'; void main() { final startTime = DateTime.now(); @@ -34,6 +35,17 @@ void main() { setUpAll(veilidFixture.attach); tearDownAll(veilidFixture.detach); + group('TableDB Tests', () { + group('TableDBArray Tests', () { + test('create TableDBArray', makeTestTableDBArrayCreateDelete()); + test( + timeout: const Timeout(Duration(seconds: 480)), + 'add/truncate TableDBArray', + makeTestDHTLogAddTruncate(), + ); + }); + }); + group('DHT Support Tests', () { setUpAll(updateProcessorFixture.setUp); setUpAll(tickerFixture.setUp); diff --git a/packages/veilid_support/example/integration_test/test_dht_log.dart b/packages/veilid_support/example/integration_test/test_dht_log.dart index 0e5829c..0c06c87 100644 --- a/packages/veilid_support/example/integration_test/test_dht_log.dart +++ b/packages/veilid_support/example/integration_test/test_dht_log.dart @@ -64,8 +64,7 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => const chunk = 25; for (var n = 0; n < dataset.length; n += chunk) { print('$n-${n + chunk - 1} '); - final success = - await w.tryAppendItems(dataset.sublist(n, n + chunk)); + final success = await w.tryAddItems(dataset.sublist(n, n + chunk)); expect(success, isTrue); } }); @@ -94,7 +93,7 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => } print('truncate\n'); { - await dlog.operateAppend((w) async => w.truncate(5)); + await dlog.operateAppend((w) async => w.truncate(w.length - 5)); } { final dataset6 = await dlog @@ -103,7 +102,7 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => } print('truncate 2\n'); { - await dlog.operateAppend((w) async => w.truncate(251)); + await dlog.operateAppend((w) async => w.truncate(w.length - 251)); } { final dataset7 = await dlog diff --git a/packages/veilid_support/example/integration_test/test_table_db_array.dart b/packages/veilid_support/example/integration_test/test_table_db_array.dart new file mode 100644 index 0000000..e9087f6 --- /dev/null +++ b/packages/veilid_support/example/integration_test/test_table_db_array.dart @@ -0,0 +1,134 @@ +import 'dart:convert'; + +import 'package:test/test.dart'; +import 'package:veilid_support/veilid_support.dart'; + +Future Function() makeTestTableDBArrayCreateDelete() => () async { + // Close before delete + { + final arr = await TableDBArray( + table: 'test', crypto: const VeilidCryptoPublic()); + + expect(await arr.operate((r) async => r.length), isZero); + expect(arr.isOpen, isTrue); + await arr.close(); + expect(arr.isOpen, isFalse); + await arr.delete(); + // Operate should fail + await expectLater(() async => arr.operate((r) async => r.length), + throwsA(isA())); + } + + // Close after delete + { + final arr = await DHTShortArray.create( + debugName: 'sa_create_delete 2 stride $stride', stride: stride); + await arr.delete(); + // Operate should still succeed because things aren't closed + expect(await arr.operate((r) async => r.length), isZero); + await arr.close(); + // Operate should fail + await expectLater(() async => arr.operate((r) async => r.length), + throwsA(isA())); + } + + // Close after delete multiple + // Okay to request delete multiple times before close + { + final arr = await DHTShortArray.create( + debugName: 'sa_create_delete 3 stride $stride', stride: stride); + await arr.delete(); + await arr.delete(); + // Operate should still succeed because things aren't closed + expect(await arr.operate((r) async => r.length), isZero); + await arr.close(); + await expectLater(() async => arr.close(), throwsA(isA())); + // Operate should fail + await expectLater(() async => arr.operate((r) async => r.length), + throwsA(isA())); + } + }; + +Future Function() makeTestTableDBArrayAdd({required int stride}) => + () async { + final arr = await DHTShortArray.create( + debugName: 'sa_add 1 stride $stride', stride: stride); + + final dataset = Iterable.generate(256) + .map((n) => utf8.encode('elem $n')) + .toList(); + + print('adding singles\n'); + { + final res = await arr.operateWrite((w) async { + for (var n = 4; n < 8; n++) { + print('$n '); + final success = await w.tryAddItem(dataset[n]); + expect(success, isTrue); + } + }); + expect(res, isNull); + } + + print('adding batch\n'); + { + final res = await arr.operateWrite((w) async { + print('${dataset.length ~/ 2}-${dataset.length}'); + final success = await w.tryAddItems( + dataset.sublist(dataset.length ~/ 2, dataset.length)); + expect(success, isTrue); + }); + expect(res, isNull); + } + + print('inserting singles\n'); + { + final res = await arr.operateWrite((w) async { + for (var n = 0; n < 4; n++) { + print('$n '); + final success = await w.tryInsertItem(n, dataset[n]); + expect(success, isTrue); + } + }); + expect(res, isNull); + } + + print('inserting batch\n'); + { + final res = await arr.operateWrite((w) async { + print('8-${dataset.length ~/ 2}'); + final success = await w.tryInsertItems( + 8, dataset.sublist(8, dataset.length ~/ 2)); + expect(success, isTrue); + }); + expect(res, isNull); + } + + //print('get all\n'); + { + final dataset2 = await arr.operate((r) async => r.getItemRange(0)); + expect(dataset2, equals(dataset)); + } + { + final dataset3 = + await arr.operate((r) async => r.getItemRange(64, length: 128)); + expect(dataset3, equals(dataset.sublist(64, 64 + 128))); + } + + //print('clear\n'); + { + await arr.operateWriteEventual((w) async { + await w.clear(); + return true; + }); + } + + //print('get all\n'); + { + final dataset4 = await arr.operate((r) async => r.getItemRange(0)); + expect(dataset4, isEmpty); + } + + await arr.delete(); + await arr.close(); + }; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index 3f561ff..cba15f4 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -9,11 +9,11 @@ import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; import '../../proto/proto.dart' as proto; -import '../interfaces/dht_append_truncate.dart'; +import '../interfaces/dht_append.dart'; part 'dht_log_spine.dart'; part 'dht_log_read.dart'; -part 'dht_log_append.dart'; +part 'dht_log_write.dart'; /////////////////////////////////////////////////////////////////////// @@ -60,7 +60,7 @@ class DHTLog implements DHTDeleteable { int stride = DHTShortArray.maxElements, VeilidRoutingContext? routingContext, TypedKey? parent, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer}) async { assert(stride <= DHTShortArray.maxElements, 'stride too long'); final pool = DHTRecordPool.instance; @@ -102,7 +102,7 @@ class DHTLog implements DHTDeleteable { {required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, - DHTRecordCrypto? crypto}) async { + VeilidCrypto? crypto}) async { final spineRecord = await DHTRecordPool.instance.openRecordRead( logRecordKey, debugName: debugName, @@ -125,7 +125,7 @@ class DHTLog implements DHTDeleteable { required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) async { final spineRecord = await DHTRecordPool.instance.openRecordWrite( logRecordKey, writer, @@ -148,7 +148,7 @@ class DHTLog implements DHTDeleteable { required String debugName, required TypedKey parent, VeilidRoutingContext? routingContext, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) => openWrite( ownedLogRecordPointer.recordKey, @@ -209,7 +209,8 @@ class DHTLog implements DHTDeleteable { OwnedDHTRecordPointer get recordPointer => _spine.recordPointer; /// Runs a closure allowing read-only access to the log - Future operate(Future Function(DHTRandomRead) closure) async { + Future operate( + Future Function(DHTLogReadOperations) closure) async { if (!isOpen) { throw StateError('log is not open"'); } @@ -226,13 +227,13 @@ class DHTLog implements DHTDeleteable { /// Throws DHTOperateException if the write could not be performed /// at this time Future operateAppend( - Future Function(DHTAppendTruncateRandomRead) closure) async { + Future Function(DHTLogWriteOperations) closure) async { if (!isOpen) { throw StateError('log is not open"'); } return _spine.operateAppend((spine) async { - final writer = _DHTLogAppend._(spine); + final writer = _DHTLogWrite._(spine); return closure(writer); }); } @@ -244,14 +245,14 @@ class DHTLog implements DHTDeleteable { /// succeeded, returning false will trigger another eventual consistency /// attempt. Future operateAppendEventual( - Future Function(DHTAppendTruncateRandomRead) closure, + Future Function(DHTLogWriteOperations) closure, {Duration? timeout}) async { if (!isOpen) { throw StateError('log is not open"'); } return _spine.operateAppendEventual((spine) async { - final writer = _DHTLogAppend._(spine); + final writer = _DHTLogWrite._(spine); return closure(writer); }, timeout: timeout); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index 30bac27..3c054fc 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -8,7 +8,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; -import '../interfaces/dht_append_truncate.dart'; @immutable class DHTLogElementState extends Equatable { @@ -184,19 +183,20 @@ class DHTLogCubit extends Cubit> await super.close(); } - Future operate(Future Function(DHTRandomRead) closure) async { + Future operate( + Future Function(DHTLogReadOperations) closure) async { await _initWait(); return _log.operate(closure); } Future operateAppend( - Future Function(DHTAppendTruncateRandomRead) closure) async { + Future Function(DHTLogWriteOperations) closure) async { await _initWait(); return _log.operateAppend(closure); } Future operateAppendEventual( - Future Function(DHTAppendTruncateRandomRead) closure, + Future Function(DHTLogWriteOperations) closure, {Duration? timeout}) async { await _initWait(); return _log.operateAppendEventual(closure, timeout: timeout); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart index 3618abd..0a66a01 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart @@ -3,7 +3,9 @@ part of 'dht_log.dart'; //////////////////////////////////////////////////////////////////////////// // Reader-only implementation -class _DHTLogRead implements DHTRandomRead { +abstract class DHTLogReadOperations implements DHTRandomRead {} + +class _DHTLogRead implements DHTLogReadOperations { _DHTLogRead._(_DHTLogSpine spine) : _spine = spine; @override diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart similarity index 67% rename from packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart rename to packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index c184032..5503051 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -1,13 +1,32 @@ part of 'dht_log.dart'; //////////////////////////////////////////////////////////////////////////// -// Append/truncate implementation +// Writer implementation -class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { - _DHTLogAppend._(super.spine) : super._(); +abstract class DHTLogWriteOperations + implements DHTRandomRead, DHTRandomWrite, DHTAdd, DHTTruncate, DHTClear {} + +class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { + _DHTLogWrite._(super.spine) : super._(); @override - Future tryAppendItem(Uint8List value) async { + Future tryWriteItem(int pos, Uint8List newValue, + {Output? output}) async { + if (pos < 0 || pos >= _spine.length) { + throw IndexError.withLength(pos, _spine.length); + } + final lookup = await _spine.lookupPosition(pos); + if (lookup == null) { + throw StateError("can't write to dht log"); + } + + // Write item to the segment + return lookup.scope((sa) => sa.operateWrite((write) async => + write.tryWriteItem(lookup.pos, newValue, output: output))); + } + + @override + Future tryAddItem(Uint8List value) async { // Allocate empty index at the end of the list final insertPos = _spine.length; _spine.allocateTail(1); @@ -30,7 +49,7 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { } @override - Future tryAppendItems(List values) async { + Future tryAddItems(List values) async { // Allocate empty index at the end of the list final insertPos = _spine.length; _spine.allocateTail(values.length); @@ -76,15 +95,14 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { } @override - Future truncate(int count) async { - count = min(count, _spine.length); - if (count == 0) { + Future truncate(int newLength) async { + if (newLength < 0) { + throw StateError('can not truncate to negative length'); + } + if (newLength >= _spine.length) { return; } - if (count < 0) { - throw StateError('can not remove negative items'); - } - await _spine.releaseHead(count); + await _spine.releaseHead(_spine.length - newLength); } @override diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart b/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart index 2b6736e..06933be 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart @@ -1,4 +1,3 @@ export 'default_dht_record_cubit.dart'; -export 'dht_record_crypto.dart'; export 'dht_record_cubit.dart'; export 'dht_record_pool.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart index 80b68ad..521bf1f 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart @@ -42,7 +42,7 @@ class DHTRecord implements DHTDeleteable { required SharedDHTRecordData sharedDHTRecordData, required int defaultSubkey, required KeyPair? writer, - required DHTRecordCrypto crypto, + required VeilidCrypto crypto, required this.debugName}) : _crypto = crypto, _routingContext = routingContext, @@ -104,7 +104,7 @@ class DHTRecord implements DHTDeleteable { int get subkeyCount => _sharedDHTRecordData.recordDescriptor.schema.subkeyCount(); KeyPair? get writer => _writer; - DHTRecordCrypto get crypto => _crypto; + VeilidCrypto get crypto => _crypto; OwnedDHTRecordPointer get ownedDHTRecordPointer => OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!); int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey; @@ -118,7 +118,7 @@ class DHTRecord implements DHTDeleteable { /// returned if one was returned. Future get( {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached, Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); @@ -146,7 +146,7 @@ class DHTRecord implements DHTDeleteable { return null; } // If we're returning a value, decrypt it - final out = (crypto ?? _crypto).decrypt(valueData.data, subkey); + final out = (crypto ?? _crypto).decrypt(valueData.data); if (outSeqNum != null) { outSeqNum.save(valueData.seq); } @@ -163,7 +163,7 @@ class DHTRecord implements DHTDeleteable { /// returned if one was returned. Future getJson(T Function(dynamic) fromJson, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached, Output? outSeqNum}) async { final data = await get( @@ -189,7 +189,7 @@ class DHTRecord implements DHTDeleteable { Future getProtobuf( T Function(List i) fromBuffer, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached, Output? outSeqNum}) async { final data = await get( @@ -208,13 +208,12 @@ class DHTRecord implements DHTDeleteable { /// If the value was succesfully written, null is returned Future tryWriteBytes(Uint8List newValue, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); final lastSeq = await _localSubkeySeq(subkey); - final encryptedNewValue = - await (crypto ?? _crypto).encrypt(newValue, subkey); + final encryptedNewValue = await (crypto ?? _crypto).encrypt(newValue); // Set the new data if possible var newValueData = await _routingContext @@ -246,7 +245,7 @@ class DHTRecord implements DHTDeleteable { // Decrypt value to return it final decryptedNewValue = - await (crypto ?? _crypto).decrypt(newValueData.data, subkey); + await (crypto ?? _crypto).decrypt(newValueData.data); if (isUpdated) { DHTRecordPool.instance .processLocalValueChange(key, decryptedNewValue, subkey); @@ -259,13 +258,12 @@ class DHTRecord implements DHTDeleteable { /// will be made to write the subkey until this succeeds Future eventualWriteBytes(Uint8List newValue, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); final lastSeq = await _localSubkeySeq(subkey); - final encryptedNewValue = - await (crypto ?? _crypto).encrypt(newValue, subkey); + final encryptedNewValue = await (crypto ?? _crypto).encrypt(newValue); ValueData? newValueData; do { @@ -309,7 +307,7 @@ class DHTRecord implements DHTDeleteable { Future eventualUpdateBytes( Future Function(Uint8List? oldValue) update, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); @@ -334,7 +332,7 @@ class DHTRecord implements DHTDeleteable { /// Like 'tryWriteBytes' but with JSON marshal/unmarshal of the value Future tryWriteJson(T Function(dynamic) fromJson, T newValue, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) => tryWriteBytes(jsonEncodeBytes(newValue), @@ -353,7 +351,7 @@ class DHTRecord implements DHTDeleteable { Future tryWriteProtobuf( T Function(List) fromBuffer, T newValue, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) => tryWriteBytes(newValue.writeToBuffer(), @@ -371,7 +369,7 @@ class DHTRecord implements DHTDeleteable { /// Like 'eventualWriteBytes' but with JSON marshal/unmarshal of the value Future eventualWriteJson(T newValue, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) => eventualWriteBytes(jsonEncodeBytes(newValue), @@ -380,7 +378,7 @@ class DHTRecord implements DHTDeleteable { /// Like 'eventualWriteBytes' but with protobuf marshal/unmarshal of the value Future eventualWriteProtobuf(T newValue, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) => eventualWriteBytes(newValue.writeToBuffer(), @@ -390,7 +388,7 @@ class DHTRecord implements DHTDeleteable { Future eventualUpdateJson( T Function(dynamic) fromJson, Future Function(T?) update, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) => eventualUpdateBytes(jsonUpdate(fromJson, update), @@ -400,7 +398,7 @@ class DHTRecord implements DHTDeleteable { Future eventualUpdateProtobuf( T Function(List) fromBuffer, Future Function(T?) update, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) => eventualUpdateBytes(protobufUpdate(fromBuffer, update), @@ -433,7 +431,7 @@ class DHTRecord implements DHTDeleteable { DHTRecord record, Uint8List? data, List subkeys) onUpdate, { bool localChanges = true, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) async { // Set up watch requirements _watchController ??= @@ -457,8 +455,7 @@ class DHTRecord implements DHTDeleteable { final changeData = change.data; data = changeData == null ? null - : await (crypto ?? _crypto) - .decrypt(changeData, change.subkeys.first.low); + : await (crypto ?? _crypto).decrypt(changeData); } await onUpdate(this, data, change.subkeys); }); @@ -544,7 +541,7 @@ class DHTRecord implements DHTDeleteable { final VeilidRoutingContext _routingContext; final int _defaultSubkey; final KeyPair? _writer; - final DHTRecordCrypto _crypto; + final VeilidCrypto _crypto; final String debugName; final _mutex = Mutex(); int _openCount; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_crypto.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_crypto.dart deleted file mode 100644 index 0e69078..0000000 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_crypto.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; -import '../../../../../veilid_support.dart'; - -abstract class DHTRecordCrypto { - Future encrypt(Uint8List data, int subkey); - Future decrypt(Uint8List data, int subkey); -} - -//////////////////////////////////// -/// Private DHT Record: Encrypted for a specific symmetric key -class DHTRecordCryptoPrivate implements DHTRecordCrypto { - DHTRecordCryptoPrivate._( - VeilidCryptoSystem cryptoSystem, SharedSecret secretKey) - : _cryptoSystem = cryptoSystem, - _secretKey = secretKey; - final VeilidCryptoSystem _cryptoSystem; - final SharedSecret _secretKey; - - static Future fromTypedKeyPair( - TypedKeyPair typedKeyPair) async { - final cryptoSystem = - await Veilid.instance.getCryptoSystem(typedKeyPair.kind); - final secretKey = typedKeyPair.secret; - return DHTRecordCryptoPrivate._(cryptoSystem, secretKey); - } - - static Future fromSecret( - CryptoKind kind, SharedSecret secretKey) async { - final cryptoSystem = await Veilid.instance.getCryptoSystem(kind); - return DHTRecordCryptoPrivate._(cryptoSystem, secretKey); - } - - @override - Future encrypt(Uint8List data, int subkey) => - _cryptoSystem.encryptNoAuthWithNonce(data, _secretKey); - - @override - Future decrypt(Uint8List data, int subkey) => - _cryptoSystem.decryptNoAuthWithNonce(data, _secretKey); -} - -//////////////////////////////////// -/// Public DHT Record: No encryption -class DHTRecordCryptoPublic implements DHTRecordCrypto { - const DHTRecordCryptoPublic(); - - @override - Future encrypt(Uint8List data, int subkey) async => data; - - @override - Future decrypt(Uint8List data, int subkey) async => data; -} diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart index a8e86a1..440698a 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart @@ -526,7 +526,7 @@ class DHTRecordPool with TableDBBackedJson { TypedKey? parent, DHTSchema schema = const DHTSchema.dflt(oCnt: 1), int defaultSubkey = 0, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, }) async => _mutex.protect(() async { @@ -547,7 +547,7 @@ class DHTRecordPool with TableDBBackedJson { writer: writer ?? openedRecordInfo.shared.recordDescriptor.ownerKeyPair(), crypto: crypto ?? - await DHTRecordCryptoPrivate.fromTypedKeyPair(openedRecordInfo + await VeilidCryptoPrivate.fromTypedKeyPair(openedRecordInfo .shared.recordDescriptor .ownerTypedKeyPair()!)); @@ -562,7 +562,7 @@ class DHTRecordPool with TableDBBackedJson { VeilidRoutingContext? routingContext, TypedKey? parent, int defaultSubkey = 0, - DHTRecordCrypto? crypto}) async => + VeilidCrypto? crypto}) async => _mutex.protect(() async { final dhtctx = routingContext ?? _routingContext; @@ -578,7 +578,7 @@ class DHTRecordPool with TableDBBackedJson { defaultSubkey: defaultSubkey, sharedDHTRecordData: openedRecordInfo.shared, writer: null, - crypto: crypto ?? const DHTRecordCryptoPublic()); + crypto: crypto ?? const VeilidCryptoPublic()); openedRecordInfo.records.add(rec); @@ -593,7 +593,7 @@ class DHTRecordPool with TableDBBackedJson { VeilidRoutingContext? routingContext, TypedKey? parent, int defaultSubkey = 0, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) async => _mutex.protect(() async { final dhtctx = routingContext ?? _routingContext; @@ -612,7 +612,7 @@ class DHTRecordPool with TableDBBackedJson { writer: writer, sharedDHTRecordData: openedRecordInfo.shared, crypto: crypto ?? - await DHTRecordCryptoPrivate.fromTypedKeyPair( + await VeilidCryptoPrivate.fromTypedKeyPair( TypedKeyPair.fromKeyPair(recordKey.kind, writer))); openedRecordInfo.records.add(rec); @@ -632,7 +632,7 @@ class DHTRecordPool with TableDBBackedJson { required TypedKey parent, VeilidRoutingContext? routingContext, int defaultSubkey = 0, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) => openRecordWrite( ownedDHTRecordPointer.recordKey, diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart index daf3061..0732255 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart @@ -33,7 +33,7 @@ class DHTShortArray implements DHTDeleteable { int stride = maxElements, VeilidRoutingContext? routingContext, TypedKey? parent, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer}) async { assert(stride <= maxElements, 'stride too long'); final pool = DHTRecordPool.instance; @@ -79,7 +79,7 @@ class DHTShortArray implements DHTDeleteable { {required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, - DHTRecordCrypto? crypto}) async { + VeilidCrypto? crypto}) async { final dhtRecord = await DHTRecordPool.instance.openRecordRead(headRecordKey, debugName: debugName, parent: parent, @@ -101,7 +101,7 @@ class DHTShortArray implements DHTDeleteable { required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) async { final dhtRecord = await DHTRecordPool.instance.openRecordWrite( headRecordKey, writer, @@ -124,7 +124,7 @@ class DHTShortArray implements DHTDeleteable { required String debugName, required TypedKey parent, VeilidRoutingContext? routingContext, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) => openWrite( ownedShortArrayRecordPointer.recordKey, @@ -186,7 +186,8 @@ class DHTShortArray implements DHTDeleteable { OwnedDHTRecordPointer get recordPointer => _head.recordPointer; /// Runs a closure allowing read-only access to the shortarray - Future operate(Future Function(DHTRandomRead) closure) async { + Future operate( + Future Function(DHTShortArrayReadOperations) closure) async { if (!isOpen) { throw StateError('short array is not open"'); } @@ -203,7 +204,7 @@ class DHTShortArray implements DHTDeleteable { /// Throws DHTOperateException if the write could not be performed /// at this time Future operateWrite( - Future Function(DHTRandomReadWrite) closure) async { + Future Function(DHTShortArrayWriteOperations) closure) async { if (!isOpen) { throw StateError('short array is not open"'); } @@ -221,7 +222,7 @@ class DHTShortArray implements DHTDeleteable { /// succeeded, returning false will trigger another eventual consistency /// attempt. Future operateWriteEventual( - Future Function(DHTRandomReadWrite) closure, + Future Function(DHTShortArrayWriteOperations) closure, {Duration? timeout}) async { if (!isOpen) { throw StateError('short array is not open"'); diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart index e0b2504..f4b806e 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart @@ -91,19 +91,20 @@ class DHTShortArrayCubit extends Cubit> await super.close(); } - Future operate(Future Function(DHTRandomRead) closure) async { + Future operate( + Future Function(DHTShortArrayReadOperations) closure) async { await _initWait(); return _shortArray.operate(closure); } Future operateWrite( - Future Function(DHTRandomReadWrite) closure) async { + Future Function(DHTShortArrayWriteOperations) closure) async { await _initWait(); return _shortArray.operateWrite(closure); } Future operateWriteEventual( - Future Function(DHTRandomReadWrite) closure, + Future Function(DHTShortArrayWriteOperations) closure, {Duration? timeout}) async { await _initWait(); return _shortArray.operateWriteEventual(closure, timeout: timeout); diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_read.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_read.dart index 6485c02..5da8cf8 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_read.dart @@ -3,7 +3,9 @@ part of 'dht_short_array.dart'; //////////////////////////////////////////////////////////////////////////// // Reader-only implementation -class _DHTShortArrayRead implements DHTRandomRead { +abstract class DHTShortArrayReadOperations implements DHTRandomRead {} + +class _DHTShortArrayRead implements DHTShortArrayReadOperations { _DHTShortArrayRead._(_DHTShortArrayHead head) : _head = head; @override diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_write.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_write.dart index df93b59..c336e47 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_write.dart @@ -3,8 +3,16 @@ part of 'dht_short_array.dart'; //////////////////////////////////////////////////////////////////////////// // Writer implementation +abstract class DHTShortArrayWriteOperations + implements + DHTRandomRead, + DHTRandomWrite, + DHTInsertRemove, + DHTAdd, + DHTClear {} + class _DHTShortArrayWrite extends _DHTShortArrayRead - implements DHTRandomReadWrite { + implements DHTShortArrayWriteOperations { _DHTShortArrayWrite._(super.head) : super._(); @override diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart new file mode 100644 index 0000000..a1f47ee --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart @@ -0,0 +1,41 @@ +import 'dart:typed_data'; + +import 'package:protobuf/protobuf.dart'; + +import '../../../veilid_support.dart'; + +//////////////////////////////////////////////////////////////////////////// +// Add +abstract class DHTAdd { + /// Try to add an item to the DHT container. + /// Return true if the element was successfully added, and false if the state + /// changed before the element could be added or a newer value was found on + /// the network. + /// Throws a StateError if the container exceeds its maximum size. + Future tryAddItem(Uint8List value); + + /// Try to add a list of items to the DHT container. + /// Return true if the elements were successfully added, and false if the + /// state changed before the element could be added or a newer value was found + /// on the network. + /// Throws a StateError if the container exceeds its maximum size. + Future tryAddItems(List values); +} + +extension DHTAddExt on DHTAdd { + /// Convenience function: + /// Like tryAddItem but also encodes the input value as JSON and parses the + /// returned element as JSON + Future tryAppendItemJson( + T newValue, + ) => + tryAddItem(jsonEncodeBytes(newValue)); + + /// Convenience function: + /// Like tryAddItem but also encodes the input value as a protobuf object + /// and parses the returned element as a protobuf object + Future tryAddItemProtobuf( + T newValue, + ) => + tryAddItem(newValue.writeToBuffer()); +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart deleted file mode 100644 index d98037c..0000000 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:typed_data'; - -import 'package:protobuf/protobuf.dart'; - -import '../../../veilid_support.dart'; - -//////////////////////////////////////////////////////////////////////////// -// Append/truncate interface -abstract class DHTAppendTruncate { - /// Try to add an item to the end of the DHT data structure. - /// Return true if the element was successfully added, and false if the state - /// changed before the element could be added or a newer value was found on - /// the network. - /// This may throw an exception if the number elements added exceeds limits. - Future tryAppendItem(Uint8List value); - - /// Try to add a list of items to the end of the DHT data structure. - /// Return true if the elements were successfully added, and false if the - /// state changed before the element could be added or a newer value was found - /// on the network. - /// This may throw an exception if the number elements added exceeds limits. - Future tryAppendItems(List values); - - /// Try to remove a number of items from the head of the DHT data structure. - /// Throws StateError if count < 0 - Future truncate(int count); - - /// Remove all items in the DHT data structure. - Future clear(); -} - -abstract class DHTAppendTruncateRandomRead - implements DHTAppendTruncate, DHTRandomRead {} - -extension DHTAppendTruncateExt on DHTAppendTruncate { - /// Convenience function: - /// Like tryAppendItem but also encodes the input value as JSON and parses the - /// returned element as JSON - Future tryAppendItemJson( - T newValue, - ) => - tryAppendItem(jsonEncodeBytes(newValue)); - - /// Convenience function: - /// Like tryAppendItem but also encodes the input value as a protobuf object - /// and parses the returned element as a protobuf object - Future tryAppendItemProtobuf( - T newValue, - ) => - tryAppendItem(newValue.writeToBuffer()); -} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_clear.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_clear.dart new file mode 100644 index 0000000..f7ac9dd --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_clear.dart @@ -0,0 +1,7 @@ +//////////////////////////////////////////////////////////////////////////// +// Clear interface +// ignore: one_member_abstracts +abstract class DHTClear { + /// Remove all items in the DHT container. + Future clear(); +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart new file mode 100644 index 0000000..1f98a22 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart @@ -0,0 +1,60 @@ +import 'dart:typed_data'; + +import 'package:protobuf/protobuf.dart'; + +import '../../../veilid_support.dart'; + +//////////////////////////////////////////////////////////////////////////// +// Insert/Remove interface +abstract class DHTInsertRemove { + /// Try to insert an item as position 'pos' of the DHT container. + /// Return true if the element was successfully inserted, and false if the + /// state changed before the element could be inserted or a newer value was + /// found on the network. + /// Throws an IndexError if the position removed exceeds the length of + /// the container. + /// Throws a StateError if the container exceeds its maximum size. + Future tryInsertItem(int pos, Uint8List value); + + /// Try to insert items at position 'pos' of the DHT container. + /// Return true if the elements were successfully inserted, and false if the + /// state changed before the elements could be inserted or a newer value was + /// found on the network. + /// Throws an IndexError if the position removed exceeds the length of + /// the container. + /// Throws a StateError if the container exceeds its maximum size. + Future tryInsertItems(int pos, List values); + + /// Swap items at position 'aPos' and 'bPos' in the DHTArray. + /// Throws an IndexError if either of the positions swapped exceeds the length + /// of the container + Future swapItem(int aPos, int bPos); + + /// Remove an item at position 'pos' in the DHT container. + /// If the remove was successful this returns: + /// * outValue will return the prior contents of the element + /// Throws an IndexError if the position removed exceeds the length of + /// the container. + Future removeItem(int pos, {Output? output}); +} + +extension DHTInsertRemoveExt on DHTInsertRemove { + /// Convenience function: + /// Like removeItem but also parses the returned element as JSON + Future removeItemJson(T Function(dynamic) fromJson, int pos, + {Output? output}) async { + final outValueBytes = output == null ? null : Output(); + await removeItem(pos, output: outValueBytes); + output.mapSave(outValueBytes, (b) => jsonDecodeBytes(fromJson, b)); + } + + /// Convenience function: + /// Like removeItem but also parses the returned element as JSON + Future removeItemProtobuf( + T Function(List) fromBuffer, int pos, + {Output? output}) async { + final outValueBytes = output == null ? null : Output(); + await removeItem(pos, output: outValueBytes); + output.mapSave(outValueBytes, fromBuffer); + } +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart index d52676e..39d49e6 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart @@ -7,22 +7,21 @@ import '../../../veilid_support.dart'; //////////////////////////////////////////////////////////////////////////// // Reader interface abstract class DHTRandomRead { - /// Returns the number of elements in the DHTArray - /// This number will be >= 0 and <= DHTShortArray.maxElements (256) + /// Returns the number of elements in the DHT container int get length; - /// Return the item at position 'pos' in the DHTArray. If 'forceRefresh' + /// Return the item at position 'pos' in the DHT container. If 'forceRefresh' /// is specified, the network will always be checked for newer values /// rather than returning the existing locally stored copy of the elements. - /// * 'pos' must be >= 0 and < 'length' + /// Throws an IndexError if the 'pos' is not within the length + /// of the container. Future getItem(int pos, {bool forceRefresh = false}); /// Return a list of a range of items in the DHTArray. If 'forceRefresh' /// is specified, the network will always be checked for newer values /// rather than returning the existing locally stored copy of the elements. - /// * 'start' must be >= 0 - /// * 'len' must be >= 0 and <= DHTShortArray.maxElements (256) and defaults - /// to the maximum length + /// Throws an IndexError if either 'start' or '(start+length)' is not within + /// the length of the container. Future?> getItemRange(int start, {int? length, bool forceRefresh = false}); diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart index 17a450e..0d8f3ac 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart @@ -6,8 +6,9 @@ import '../../../veilid_support.dart'; //////////////////////////////////////////////////////////////////////////// // Writer interface +// ignore: one_member_abstracts abstract class DHTRandomWrite { - /// Try to set an item at position 'pos' of the DHTArray. + /// Try to set an item at position 'pos' of the DHT container. /// If the set was successful this returns: /// * A boolean true /// * outValue will return the prior contents of the element, @@ -18,55 +19,10 @@ abstract class DHTRandomWrite { /// * outValue will return the newer value of the element, /// or null if the head record changed. /// - /// This may throw an exception if the position exceeds the built-in limit of - /// 'maxElements = 256' entries. + /// Throws an IndexError if the position is not within the length + /// of the container. Future tryWriteItem(int pos, Uint8List newValue, {Output? output}); - - /// Try to add an item to the end of the DHTArray. Return true if the - /// element was successfully added, and false if the state changed before - /// the element could be added or a newer value was found on the network. - /// This may throw an exception if the number elements added exceeds the - /// built-in limit of 'maxElements = 256' entries. - Future tryAddItem(Uint8List value); - - /// Try to add a list of items to the end of the DHTArray. Return true if the - /// elements were successfully added, and false if the state changed before - /// the elements could be added or a newer value was found on the network. - /// This may throw an exception if the number elements added exceeds the - /// built-in limit of 'maxElements = 256' entries. - Future tryAddItems(List values); - - /// Try to insert an item as position 'pos' of the DHTArray. - /// Return true if the element was successfully inserted, and false if the - /// state changed before the element could be inserted or a newer value was - /// found on the network. - /// This may throw an exception if the number elements added exceeds the - /// built-in limit of 'maxElements = 256' entries. - Future tryInsertItem(int pos, Uint8List value); - - /// Try to insert items at position 'pos' of the DHTArray. - /// Return true if the elements were successfully inserted, and false if the - /// state changed before the elements could be inserted or a newer value was - /// found on the network. - /// This may throw an exception if the number elements added exceeds the - /// built-in limit of 'maxElements = 256' entries. - Future tryInsertItems(int pos, List values); - - /// Swap items at position 'aPos' and 'bPos' in the DHTArray. - /// Throws IndexError if either of the positions swapped exceed - /// the length of the list - Future swapItem(int aPos, int bPos); - - /// Remove an item at position 'pos' in the DHTArray. - /// If the remove was successful this returns: - /// * outValue will return the prior contents of the element - /// Throws IndexError if the position removed exceeds the length of - /// the list. - Future removeItem(int pos, {Output? output}); - - /// Remove all items in the DHTShortArray. - Future clear(); } extension DHTRandomWriteExt on DHTRandomWrite { @@ -95,25 +51,4 @@ extension DHTRandomWriteExt on DHTRandomWrite { output.mapSave(outValueBytes, fromBuffer); return out; } - - /// Convenience function: - /// Like removeItem but also parses the returned element as JSON - Future removeItemJson(T Function(dynamic) fromJson, int pos, - {Output? output}) async { - final outValueBytes = output == null ? null : Output(); - await removeItem(pos, output: outValueBytes); - output.mapSave(outValueBytes, (b) => jsonDecodeBytes(fromJson, b)); - } - - /// Convenience function: - /// Like removeItem but also parses the returned element as JSON - Future removeItemProtobuf( - T Function(List) fromBuffer, int pos, - {Output? output}) async { - final outValueBytes = output == null ? null : Output(); - await removeItem(pos, output: outValueBytes); - output.mapSave(outValueBytes, fromBuffer); - } } - -abstract class DHTRandomReadWrite implements DHTRandomRead, DHTRandomWrite {} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_truncate.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_truncate.dart new file mode 100644 index 0000000..cbda00f --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_truncate.dart @@ -0,0 +1,8 @@ +//////////////////////////////////////////////////////////////////////////// +// Truncate interface +// ignore: one_member_abstracts +abstract class DHTTruncate { + /// Remove items from the DHT container to shrink its size to 'newLength' + /// Throws StateError if newLength < 0 + Future truncate(int newLength); +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart index 16f9970..dd95cac 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart @@ -1,4 +1,8 @@ +export 'dht_append.dart'; +export 'dht_clear.dart'; export 'dht_closeable.dart'; +export 'dht_insert_remove.dart'; export 'dht_random_read.dart'; export 'dht_random_write.dart'; +export 'dht_truncate.dart'; export 'exceptions.dart'; diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index 5721461..400d68b 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -130,7 +130,7 @@ extension IdentityMasterExtension on IdentityMaster { // Read the identity key to get the account keys final pool = DHTRecordPool.instance; - final identityRecordCrypto = await DHTRecordCryptoPrivate.fromSecret( + final identityRecordCrypto = await VeilidCryptoPrivate.fromSecret( identityRecordKey.kind, identitySecret); late final List accountRecordInfo; @@ -234,7 +234,7 @@ class IdentityMasterWithSecrets { return (await pool.createRecord( debugName: 'IdentityMasterWithSecrets::create::IdentityMasterRecord', - crypto: const DHTRecordCryptoPublic())) + crypto: const VeilidCryptoPublic())) .deleteScope((masterRec) async { veilidLoggy.debug('Creating identity record'); // Identity record is private diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart new file mode 100644 index 0000000..504fb16 --- /dev/null +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -0,0 +1,517 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:async_tools/async_tools.dart'; +import 'package:charcode/charcode.dart'; + +import '../veilid_support.dart'; + +class TableDBArray { + TableDBArray({ + required String table, + required VeilidCrypto crypto, + }) : _table = table, + _crypto = crypto { + _initWait.add(_init); + } + + Future _init() async { + // Load the array details + await _mutex.protect(() async { + _tableDB = await Veilid.instance.openTableDB(_table, 1); + }); + } + + Future close({bool delete = false}) async { + // Ensure the init finished + await _initWait(); + + await _mutex.acquire(); + + await _changeStream.close(); + _tableDB.close(); + + if (delete) { + await Veilid.instance.deleteTableDB(_table); + } + } + + Future> listen(void Function() onChanged) async => + _changeStream.stream.listen((_) => onChanged()); + + //////////////////////////////////////////////////////////// + // Public interface + + int get length => _length; + + Future add(Uint8List value) async { + await _initWait(); + return _writeTransaction((t) async => _addInner(t, value)); + } + + Future addAll(List values) async { + await _initWait(); + return _writeTransaction((t) async => _addAllInner(t, values)); + } + + Future insert(int pos, Uint8List value) async { + await _initWait(); + return _writeTransaction((t) async => _insertInner(t, pos, value)); + } + + Future insertAll(int pos, List values) async { + await _initWait(); + return _writeTransaction((t) async => _insertAllInner(t, pos, values)); + } + + Future get(int pos) async { + await _initWait(); + return _mutex.protect(() async => _getInner(pos)); + } + + Future> getAll(int start, int length) async { + await _initWait(); + return _mutex.protect(() async => _getAllInner(start, length)); + } + + Future remove(int pos, {Output? out}) async { + await _initWait(); + return _writeTransaction((t) async => _removeInner(t, pos, out: out)); + } + + Future removeRange(int start, int length, + {Output>? out}) async { + await _initWait(); + return _writeTransaction( + (t) async => _removeRangeInner(t, start, length, out: out)); + } + + Future clear() async { + await _initWait(); + return _writeTransaction((t) async { + final keys = await _tableDB.getKeys(0); + for (final key in keys) { + await t.delete(0, key); + } + _length = 0; + _nextFree = 0; + _dirtyChunks.clear(); + _chunkCache.clear(); + }); + } + + //////////////////////////////////////////////////////////// + // Inner interface + + Future _addInner(VeilidTableDBTransaction t, Uint8List value) async { + // Allocate an entry to store the value + final entry = await _allocateEntry(); + await _storeEntry(t, entry, value); + + // Put the entry in the index + final pos = _length; + _length++; + await _setIndexEntry(pos, entry); + } + + Future _addAllInner( + VeilidTableDBTransaction t, List values) async { + var pos = _length; + _length += values.length; + for (final value in values) { + // Allocate an entry to store the value + final entry = await _allocateEntry(); + await _storeEntry(t, entry, value); + + // Put the entry in the index + await _setIndexEntry(pos, entry); + pos++; + } + } + + Future _insertInner( + VeilidTableDBTransaction t, int pos, Uint8List value) async { + if (pos == _length) { + return _addInner(t, value); + } + if (pos < 0 || pos >= _length) { + throw IndexError.withLength(pos, _length); + } + // Allocate an entry to store the value + final entry = await _allocateEntry(); + await _storeEntry(t, entry, value); + + // Put the entry in the index + await _insertIndexEntry(pos); + await _setIndexEntry(pos, entry); + } + + Future _insertAllInner( + VeilidTableDBTransaction t, int pos, List values) async { + if (pos == _length) { + return _addAllInner(t, values); + } + if (pos < 0 || pos >= _length) { + throw IndexError.withLength(pos, _length); + } + await _insertIndexEntries(pos, values.length); + for (final value in values) { + // Allocate an entry to store the value + final entry = await _allocateEntry(); + await _storeEntry(t, entry, value); + + // Put the entry in the index + await _setIndexEntry(pos, entry); + pos++; + } + } + + Future _getInner(int pos) async { + if (pos < 0 || pos >= _length) { + throw IndexError.withLength(pos, _length); + } + final entry = await _getIndexEntry(pos); + return (await _loadEntry(entry))!; + } + + Future> _getAllInner(int start, int length) async { + if (length < 0) { + throw StateError('length should not be negative'); + } + if (start < 0 || start >= _length) { + throw IndexError.withLength(start, _length); + } + if ((start + length) > _length) { + throw IndexError.withLength(start + length, _length); + } + + final out = []; + for (var pos = start; pos < (start + length); pos++) { + final entry = await _getIndexEntry(pos); + final value = (await _loadEntry(entry))!; + out.add(value); + } + return out; + } + + Future _removeInner(VeilidTableDBTransaction t, int pos, + {Output? out}) async { + if (pos < 0 || pos >= _length) { + throw IndexError.withLength(pos, _length); + } + + final entry = await _getIndexEntry(pos); + if (out != null) { + final value = (await _loadEntry(entry))!; + out.save(value); + } + + await _freeEntry(t, entry); + await _removeIndexEntry(pos); + } + + Future _removeRangeInner( + VeilidTableDBTransaction t, int start, int length, + {Output>? out}) async { + if (length < 0) { + throw StateError('length should not be negative'); + } + if (start < 0 || start >= _length) { + throw IndexError.withLength(start, _length); + } + if ((start + length) > _length) { + throw IndexError.withLength(start + length, _length); + } + + final outList = []; + for (var pos = start; pos < (start + length); pos++) { + final entry = await _getIndexEntry(pos); + if (out != null) { + final value = (await _loadEntry(entry))!; + outList.add(value); + } + await _freeEntry(t, entry); + } + if (out != null) { + out.save(outList); + } + + await _removeIndexEntries(start, length); + } + + //////////////////////////////////////////////////////////// + // Private implementation + + static final Uint8List _headKey = Uint8List.fromList([$_, $H, $E, $A, $D]); + static Uint8List _entryKey(int k) => + (ByteData(4)..setUint32(0, k)).buffer.asUint8List(); + static Uint8List _chunkKey(int n) => + (ByteData(2)..setUint16(0, n)).buffer.asUint8List(); + + Future _writeTransaction( + Future Function(VeilidTableDBTransaction) closure) async => + _mutex.protect(() async { + final _oldLength = _length; + final _oldNextFree = _nextFree; + try { + final out = await transactionScope(_tableDB, (t) async { + final out = closure(t); + await _saveHead(t); + await _flushDirtyChunks(t); + return out; + }); + + return out; + } on Exception { + // restore head + _length = _oldLength; + _nextFree = _oldNextFree; + // invalidate caches because they could have been written to + _chunkCache.clear(); + _dirtyChunks.clear(); + // propagate exception + rethrow; + } + }); + + Future _storeEntry( + VeilidTableDBTransaction t, int entry, Uint8List value) async => + t.store(0, _entryKey(entry), await _crypto.encrypt(value)); + + Future _loadEntry(int entry) async { + final encryptedValue = await _tableDB.load(0, _entryKey(entry)); + return (encryptedValue == null) ? null : _crypto.decrypt(encryptedValue); + } + + Future _getIndexEntry(int pos) async { + if (pos < 0 || pos >= _length) { + throw IndexError.withLength(pos, _length); + } + final chunkNumber = pos ~/ _indexStride; + final chunkOffset = pos % _indexStride; + + final chunk = await _loadIndexChunk(chunkNumber); + + return chunk.buffer.asByteData().getUint32(chunkOffset * 4); + } + + Future _setIndexEntry(int pos, int entry) async { + if (pos < 0 || pos >= _length) { + throw IndexError.withLength(pos, _length); + } + + final chunkNumber = pos ~/ _indexStride; + final chunkOffset = pos % _indexStride; + + final chunk = await _loadIndexChunk(chunkNumber); + chunk.buffer.asByteData().setUint32(chunkOffset * 4, entry); + + _dirtyChunks[chunkNumber] = chunk; + } + + Future _insertIndexEntry(int pos) async => _insertIndexEntries(pos, 1); + + Future _insertIndexEntries(int start, int length) async { + if (length == 0) { + return; + } + if (length < 0) { + throw StateError('length should not be negative'); + } + if (start < 0 || start >= _length) { + throw IndexError.withLength(start, _length); + } + final end = start + length - 1; + + // Slide everything over in reverse + final toCopyTotal = _length - start; + var dest = end + toCopyTotal; + var src = _length - 1; + + (int, Uint8List)? lastSrcChunk; + (int, Uint8List)? lastDestChunk; + while (src >= start) { + final srcChunkNumber = src ~/ _indexStride; + final srcIndex = src % _indexStride; + final srcLength = srcIndex + 1; + + final srcChunk = + (lastSrcChunk != null && (lastSrcChunk.$1 == srcChunkNumber)) + ? lastSrcChunk.$2 + : await _loadIndexChunk(srcChunkNumber); + lastSrcChunk = (srcChunkNumber, srcChunk); + + final destChunkNumber = dest ~/ _indexStride; + final destIndex = dest % _indexStride; + final destLength = destIndex + 1; + + final destChunk = + (lastDestChunk != null && (lastDestChunk.$1 == destChunkNumber)) + ? lastDestChunk.$2 + : await _loadIndexChunk(destChunkNumber); + lastDestChunk = (destChunkNumber, destChunk); + + final toCopy = min(srcLength, destLength); + destChunk.setRange((destIndex - (toCopy - 1)) * 4, (destIndex + 1) * 4, + srcChunk, (srcIndex - (toCopy - 1)) * 4); + + dest -= toCopy; + src -= toCopy; + } + + // Then add to length + _length += length; + } + + Future _removeIndexEntry(int pos) async => _removeIndexEntries(pos, 1); + + Future _removeIndexEntries(int start, int length) async { + if (length == 0) { + return; + } + if (length < 0) { + throw StateError('length should not be negative'); + } + if (start < 0 || start >= _length) { + throw IndexError.withLength(start, _length); + } + final end = start + length - 1; + if (end < 0 || end >= _length) { + throw IndexError.withLength(end, _length); + } + + // Slide everything over + var dest = start; + var src = end + 1; + (int, Uint8List)? lastSrcChunk; + (int, Uint8List)? lastDestChunk; + while (src < _length) { + final srcChunkNumber = src ~/ _indexStride; + final srcIndex = src % _indexStride; + final srcLength = _indexStride - srcIndex; + + final srcChunk = + (lastSrcChunk != null && (lastSrcChunk.$1 == srcChunkNumber)) + ? lastSrcChunk.$2 + : await _loadIndexChunk(srcChunkNumber); + lastSrcChunk = (srcChunkNumber, srcChunk); + + final destChunkNumber = dest ~/ _indexStride; + final destIndex = dest % _indexStride; + final destLength = _indexStride - destIndex; + + final destChunk = + (lastDestChunk != null && (lastDestChunk.$1 == destChunkNumber)) + ? lastDestChunk.$2 + : await _loadIndexChunk(destChunkNumber); + lastDestChunk = (destChunkNumber, destChunk); + + final toCopy = min(srcLength, destLength); + destChunk.setRange( + destIndex * 4, (destIndex + toCopy) * 4, srcChunk, srcIndex * 4); + + dest += toCopy; + src += toCopy; + } + + // Then truncate + _length -= length; + } + + Future _loadIndexChunk(int chunkNumber) async { + // Get it from the dirty chunks if we have it + final dirtyChunk = _dirtyChunks[chunkNumber]; + if (dirtyChunk != null) { + return dirtyChunk; + } + + // Get from cache if we have it + for (var i = 0; i < _chunkCache.length; i++) { + if (_chunkCache[i].$1 == chunkNumber) { + // Touch the element + final x = _chunkCache.removeAt(i); + _chunkCache.add(x); + // Return the chunk for this position + return x.$2; + } + } + + // Get chunk from disk + var chunk = await _tableDB.load(0, _chunkKey(chunkNumber)); + chunk ??= Uint8List(_indexStride * 4); + + // Cache the chunk + _chunkCache.add((chunkNumber, chunk)); + if (_chunkCache.length > _chunkCacheLength) { + // Trim the LRU cache + final (_, _) = _chunkCache.removeAt(0); + } + + return chunk; + } + + Future _flushDirtyChunks(VeilidTableDBTransaction t) async { + for (final ec in _dirtyChunks.entries) { + await _tableDB.store(0, _chunkKey(ec.key), ec.value); + } + _dirtyChunks.clear(); + } + + Future _loadHead() async { + assert(_mutex.isLocked, 'should be locked'); + final headBytes = await _tableDB.load(0, _headKey); + if (headBytes == null) { + _length = 0; + _nextFree = 0; + } else { + final b = headBytes.buffer.asByteData(); + _length = b.getUint32(0); + _nextFree = b.getUint32(4); + } + } + + Future _saveHead(VeilidTableDBTransaction t) async { + assert(_mutex.isLocked, 'should be locked'); + final b = ByteData(8) + ..setUint32(0, _length) + ..setUint32(4, _nextFree); + await t.store(0, _headKey, b.buffer.asUint8List()); + } + + Future _allocateEntry() async { + assert(_mutex.isLocked, 'should be locked'); + if (_nextFree == 0) { + return _length; + } + // pop endogenous free list + final free = _nextFree; + final nextFreeBytes = await _tableDB.load(0, _entryKey(free)); + _nextFree = nextFreeBytes!.buffer.asByteData().getUint8(0); + return free; + } + + Future _freeEntry(VeilidTableDBTransaction t, int entry) async { + assert(_mutex.isLocked, 'should be locked'); + // push endogenous free list + final b = ByteData(4)..setUint32(0, _nextFree); + await t.store(0, _entryKey(entry), b.buffer.asUint8List()); + _nextFree = entry; + } + + final String _table; + late final VeilidTableDB _tableDB; + final VeilidCrypto _crypto; + final WaitSet _initWait = WaitSet(); + final Mutex _mutex = Mutex(); + + // Head state + int _length = 0; + int _nextFree = 0; + static const int _indexStride = 16384; + final List<(int, Uint8List)> _chunkCache = []; + final Map _dirtyChunks = {}; + static const int _chunkCacheLength = 3; + + final StreamController _changeStream = StreamController.broadcast(); +} diff --git a/packages/veilid_support/lib/src/veilid_crypto.dart b/packages/veilid_support/lib/src/veilid_crypto.dart new file mode 100644 index 0000000..6965089 --- /dev/null +++ b/packages/veilid_support/lib/src/veilid_crypto.dart @@ -0,0 +1,52 @@ +import 'dart:async'; +import 'dart:typed_data'; +import '../../../veilid_support.dart'; + +abstract class VeilidCrypto { + Future encrypt(Uint8List data); + Future decrypt(Uint8List data); +} + +//////////////////////////////////// +/// Encrypted for a specific symmetric key +class VeilidCryptoPrivate implements VeilidCrypto { + VeilidCryptoPrivate._(VeilidCryptoSystem cryptoSystem, SharedSecret secretKey) + : _cryptoSystem = cryptoSystem, + _secretKey = secretKey; + final VeilidCryptoSystem _cryptoSystem; + final SharedSecret _secretKey; + + static Future fromTypedKeyPair( + TypedKeyPair typedKeyPair) async { + final cryptoSystem = + await Veilid.instance.getCryptoSystem(typedKeyPair.kind); + final secretKey = typedKeyPair.secret; + return VeilidCryptoPrivate._(cryptoSystem, secretKey); + } + + static Future fromSecret( + CryptoKind kind, SharedSecret secretKey) async { + final cryptoSystem = await Veilid.instance.getCryptoSystem(kind); + return VeilidCryptoPrivate._(cryptoSystem, secretKey); + } + + @override + Future encrypt(Uint8List data) => + _cryptoSystem.encryptNoAuthWithNonce(data, _secretKey); + + @override + Future decrypt(Uint8List data) => + _cryptoSystem.decryptNoAuthWithNonce(data, _secretKey); +} + +//////////////////////////////////// +/// No encryption +class VeilidCryptoPublic implements VeilidCrypto { + const VeilidCryptoPublic(); + + @override + Future encrypt(Uint8List data) async => data; + + @override + Future decrypt(Uint8List data) async => data; +} diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index e741990..1f17da2 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -14,4 +14,6 @@ export 'src/output.dart'; export 'src/persistent_queue.dart'; export 'src/protobuf_tools.dart'; export 'src/table_db.dart'; +export 'src/table_db_array.dart'; +export 'src/veilid_crypto.dart'; export 'src/veilid_log.dart' hide veilidLoggy; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index d58ee4d..db70f07 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -146,7 +146,7 @@ packages: source: hosted version: "1.3.0" charcode: - dependency: transitive + dependency: "direct main" description: name: charcode sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 06403ca..e598f8c 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: async_tools: ^0.1.1 bloc: ^8.1.4 bloc_advanced_tools: ^0.1.1 + charcode: ^1.3.1 collection: ^1.18.0 equatable: ^2.0.5 fast_immutable_collections: ^10.2.3 From ab65956433912207b347ea53771194422a11ca12 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 26 May 2024 20:41:29 -0400 Subject: [PATCH 02/19] table db array testing --- .../example/integration_test/app_test.dart | 187 +++++++--- .../integration_test/test_table_db_array.dart | 331 +++++++++++------- .../lib/src/table_db_array.dart | 103 ++++-- 3 files changed, 432 insertions(+), 189 deletions(-) diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index 1577d7a..b7a807f 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:integration_test/integration_test.dart'; import 'package:test/test.dart'; +import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_test/veilid_test.dart'; import 'fixtures/fixtures.dart'; @@ -37,59 +38,159 @@ void main() { group('TableDB Tests', () { group('TableDBArray Tests', () { - test('create TableDBArray', makeTestTableDBArrayCreateDelete()); - test( - timeout: const Timeout(Duration(seconds: 480)), - 'add/truncate TableDBArray', - makeTestDHTLogAddTruncate(), - ); + // test('create/delete TableDBArray', testTableDBArrayCreateDelete); + + // group('TableDBArray Add/Get Tests', () { + // for (final params in [ + // // + // (99, 3, 15), + // (100, 4, 16), + // (101, 5, 17), + // // + // (511, 3, 127), + // (512, 4, 128), + // (513, 5, 129), + // // + // (4095, 3, 1023), + // (4096, 4, 1024), + // (4097, 5, 1025), + // // + // (65535, 3, 16383), + // (65536, 4, 16384), + // (65537, 5, 16385), + // ]) { + // final count = params.$1; + // final singles = params.$2; + // final batchSize = params.$3; + + // test( + // // timeout: const Timeout(Duration(seconds: 480)), + // 'add/remove TableDBArray count = $count batchSize=$batchSize', + // makeTestTableDBArrayAddGetClear( + // count: count, + // singles: singles, + // batchSize: batchSize, + // crypto: const VeilidCryptoPublic()), + // ); + // } + // }); + + // group('TableDBArray Insert Tests', () { + // for (final params in [ + // // + // (99, 3, 15), + // (100, 4, 16), + // (101, 5, 17), + // // + // (511, 3, 127), + // (512, 4, 128), + // (513, 5, 129), + // // + // (4095, 3, 1023), + // (4096, 4, 1024), + // (4097, 5, 1025), + // // + // (65535, 3, 16383), + // (65536, 4, 16384), + // (65537, 5, 16385), + // ]) { + // final count = params.$1; + // final singles = params.$2; + // final batchSize = params.$3; + + // test( + // // timeout: const Timeout(Duration(seconds: 480)), + // 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', + // makeTestTableDBArrayInsert( + // count: count, + // singles: singles, + // batchSize: batchSize, + // crypto: const VeilidCryptoPublic()), + // ); + // } + // }); + + group('TableDBArray Remove Tests', () { + for (final params in [ + // + (99, 3, 15), + (100, 4, 16), + (101, 5, 17), + // + (511, 3, 127), + (512, 4, 128), + (513, 5, 129), + // + (4095, 3, 1023), + (4096, 4, 1024), + (4097, 5, 1025), + // + (65535, 3, 16383), + (65536, 4, 16384), + (65537, 5, 16385), + ]) { + final count = params.$1; + final singles = params.$2; + final batchSize = params.$3; + + test( + // timeout: const Timeout(Duration(seconds: 480)), + 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', + makeTestTableDBArrayRemove( + count: count, + singles: singles, + batchSize: batchSize, + crypto: const VeilidCryptoPublic()), + ); + } + }); }); }); - group('DHT Support Tests', () { - setUpAll(updateProcessorFixture.setUp); - setUpAll(tickerFixture.setUp); - tearDownAll(tickerFixture.tearDown); - tearDownAll(updateProcessorFixture.tearDown); + // group('DHT Support Tests', () { + // setUpAll(updateProcessorFixture.setUp); + // setUpAll(tickerFixture.setUp); + // tearDownAll(tickerFixture.tearDown); + // tearDownAll(updateProcessorFixture.tearDown); - test('create pool', testDHTRecordPoolCreate); + // test('create pool', testDHTRecordPoolCreate); - group('DHTRecordPool Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); + // group('DHTRecordPool Tests', () { + // setUpAll(dhtRecordPoolFixture.setUp); + // tearDownAll(dhtRecordPoolFixture.tearDown); - test('create/delete record', testDHTRecordCreateDelete); - test('record scopes', testDHTRecordScopes); - test('create/delete deep record', testDHTRecordDeepCreateDelete); - }); + // test('create/delete record', testDHTRecordCreateDelete); + // test('record scopes', testDHTRecordScopes); + // test('create/delete deep record', testDHTRecordDeepCreateDelete); + // }); - group('DHTShortArray Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); + // group('DHTShortArray Tests', () { + // setUpAll(dhtRecordPoolFixture.setUp); + // tearDownAll(dhtRecordPoolFixture.tearDown); - for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - test('create shortarray stride=$stride', - makeTestDHTShortArrayCreateDelete(stride: stride)); - test('add shortarray stride=$stride', - makeTestDHTShortArrayAdd(stride: stride)); - } - }); + // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + // test('create shortarray stride=$stride', + // makeTestDHTShortArrayCreateDelete(stride: stride)); + // test('add shortarray stride=$stride', + // makeTestDHTShortArrayAdd(stride: stride)); + // } + // }); - group('DHTLog Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); + // group('DHTLog Tests', () { + // setUpAll(dhtRecordPoolFixture.setUp); + // tearDownAll(dhtRecordPoolFixture.tearDown); - for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - test('create log stride=$stride', - makeTestDHTLogCreateDelete(stride: stride)); - test( - timeout: const Timeout(Duration(seconds: 480)), - 'add/truncate log stride=$stride', - makeTestDHTLogAddTruncate(stride: stride), - ); - } - }); - }); + // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + // test('create log stride=$stride', + // makeTestDHTLogCreateDelete(stride: stride)); + // test( + // timeout: const Timeout(Duration(seconds: 480)), + // 'add/truncate log stride=$stride', + // makeTestDHTLogAddTruncate(stride: stride), + // ); + // } + // }); + // }); }); }); } diff --git a/packages/veilid_support/example/integration_test/test_table_db_array.dart b/packages/veilid_support/example/integration_test/test_table_db_array.dart index e9087f6..81607a6 100644 --- a/packages/veilid_support/example/integration_test/test_table_db_array.dart +++ b/packages/veilid_support/example/integration_test/test_table_db_array.dart @@ -1,134 +1,213 @@ import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; import 'package:test/test.dart'; import 'package:veilid_support/veilid_support.dart'; -Future Function() makeTestTableDBArrayCreateDelete() => () async { - // Close before delete - { - final arr = await TableDBArray( - table: 'test', crypto: const VeilidCryptoPublic()); - - expect(await arr.operate((r) async => r.length), isZero); - expect(arr.isOpen, isTrue); - await arr.close(); - expect(arr.isOpen, isFalse); - await arr.delete(); - // Operate should fail - await expectLater(() async => arr.operate((r) async => r.length), - throwsA(isA())); - } - - // Close after delete - { - final arr = await DHTShortArray.create( - debugName: 'sa_create_delete 2 stride $stride', stride: stride); - await arr.delete(); - // Operate should still succeed because things aren't closed - expect(await arr.operate((r) async => r.length), isZero); - await arr.close(); - // Operate should fail - await expectLater(() async => arr.operate((r) async => r.length), - throwsA(isA())); - } - - // Close after delete multiple - // Okay to request delete multiple times before close - { - final arr = await DHTShortArray.create( - debugName: 'sa_create_delete 3 stride $stride', stride: stride); - await arr.delete(); - await arr.delete(); - // Operate should still succeed because things aren't closed - expect(await arr.operate((r) async => r.length), isZero); - await arr.close(); - await expectLater(() async => arr.close(), throwsA(isA())); - // Operate should fail - await expectLater(() async => arr.operate((r) async => r.length), - throwsA(isA())); - } - }; - -Future Function() makeTestTableDBArrayAdd({required int stride}) => - () async { - final arr = await DHTShortArray.create( - debugName: 'sa_add 1 stride $stride', stride: stride); - - final dataset = Iterable.generate(256) - .map((n) => utf8.encode('elem $n')) - .toList(); - - print('adding singles\n'); - { - final res = await arr.operateWrite((w) async { - for (var n = 4; n < 8; n++) { - print('$n '); - final success = await w.tryAddItem(dataset[n]); - expect(success, isTrue); - } - }); - expect(res, isNull); - } - - print('adding batch\n'); - { - final res = await arr.operateWrite((w) async { - print('${dataset.length ~/ 2}-${dataset.length}'); - final success = await w.tryAddItems( - dataset.sublist(dataset.length ~/ 2, dataset.length)); - expect(success, isTrue); - }); - expect(res, isNull); - } - - print('inserting singles\n'); - { - final res = await arr.operateWrite((w) async { - for (var n = 0; n < 4; n++) { - print('$n '); - final success = await w.tryInsertItem(n, dataset[n]); - expect(success, isTrue); - } - }); - expect(res, isNull); - } - - print('inserting batch\n'); - { - final res = await arr.operateWrite((w) async { - print('8-${dataset.length ~/ 2}'); - final success = await w.tryInsertItems( - 8, dataset.sublist(8, dataset.length ~/ 2)); - expect(success, isTrue); - }); - expect(res, isNull); - } - - //print('get all\n'); - { - final dataset2 = await arr.operate((r) async => r.getItemRange(0)); - expect(dataset2, equals(dataset)); - } - { - final dataset3 = - await arr.operate((r) async => r.getItemRange(64, length: 128)); - expect(dataset3, equals(dataset.sublist(64, 64 + 128))); - } - - //print('clear\n'); - { - await arr.operateWriteEventual((w) async { - await w.clear(); - return true; - }); - } - - //print('get all\n'); - { - final dataset4 = await arr.operate((r) async => r.getItemRange(0)); - expect(dataset4, isEmpty); - } +Future testTableDBArrayCreateDelete() async { + // Close before delete + { + final arr = + TableDBArray(table: 'testArray', crypto: const VeilidCryptoPublic()); + expect(() => arr.length, throwsA(isA())); + expect(arr.isOpen, isTrue); + await arr.initWait(); + expect(arr.isOpen, isTrue); + expect(arr.length, isZero); + await arr.close(); + expect(arr.isOpen, isFalse); + await arr.delete(); + expect(arr.isOpen, isFalse); + } + // Async create with close after delete and then reopen + { + final arr = await TableDBArray.make( + table: 'testArray', crypto: const VeilidCryptoPublic()); + expect(arr.length, isZero); + expect(arr.isOpen, isTrue); + await expectLater(() async { await arr.delete(); - await arr.close(); + }, throwsA(isA())); + expect(arr.isOpen, isTrue); + await arr.close(); + expect(arr.isOpen, isFalse); + + final arr2 = await TableDBArray.make( + table: 'testArray', crypto: const VeilidCryptoPublic()); + expect(arr2.isOpen, isTrue); + expect(arr.isOpen, isFalse); + await arr2.close(); + expect(arr2.isOpen, isFalse); + await arr2.delete(); + } +} + +Uint8List makeData(int n) => utf8.encode('elem $n'); +List makeDataBatch(int n, int batchSize) => + List.generate(batchSize, (x) => makeData(n + x)); + +Future Function() makeTestTableDBArrayAddGetClear( + {required int count, + required int singles, + required int batchSize, + required VeilidCrypto crypto}) => + () async { + final arr = await TableDBArray.make(table: 'testArray', crypto: crypto); + + print('adding'); + { + for (var n = 0; n < count;) { + var toAdd = min(batchSize, count - n); + for (var s = 0; s < min(singles, toAdd); s++) { + await arr.add(makeData(n)); + toAdd--; + n++; + } + + await arr.addAll(makeDataBatch(n, toAdd)); + n += toAdd; + + print(' $n/$count'); + } + } + + print('get singles'); + { + for (var n = 0; n < batchSize; n++) { + expect(await arr.get(n), equals(makeData(n))); + } + } + + print('get batch'); + { + for (var n = batchSize; n < count; n += batchSize) { + final toGet = min(batchSize, count - n); + expect(await arr.getRange(n, toGet), equals(makeDataBatch(n, toGet))); + } + } + + print('clear'); + { + await arr.clear(); + expect(arr.length, isZero); + } + + await arr.close(delete: true); + }; + +Future Function() makeTestTableDBArrayInsert( + {required int count, + required int singles, + required int batchSize, + required VeilidCrypto crypto}) => + () async { + final arr = await TableDBArray.make(table: 'testArray', crypto: crypto); + + final match = []; + + print('inserting'); + { + for (var n = 0; n < count;) { + final start = n; + var toAdd = min(batchSize, count - n); + for (var s = 0; s < min(singles, toAdd); s++) { + final data = makeData(n); + await arr.insert(start, data); + match.insert(start, data); + toAdd--; + n++; + } + + final data = makeDataBatch(n, toAdd); + await arr.insertAll(start, data); + match.insertAll(start, data); + n += toAdd; + + print(' $n/$count'); + } + } + + print('get singles'); + { + for (var n = 0; n < batchSize; n++) { + expect(await arr.get(n), equals(match[n])); + } + } + + print('get batch'); + { + for (var n = batchSize; n < count; n += batchSize) { + final toGet = min(batchSize, count - n); + expect(await arr.getRange(n, toGet), + equals(match.sublist(n, n + toGet))); + } + } + + print('clear'); + { + await arr.clear(); + expect(arr.length, isZero); + } + + await arr.close(delete: true); + }; + + +Future Function() makeTestTableDBArrayRemove( + {required int count, + required int singles, + required int batchSize, + required VeilidCrypto crypto}) => + () async { + final arr = await TableDBArray.make(table: 'testArray', crypto: crypto); + + final match = []; +xxx removal test + print('inserting'); + { + for (var n = 0; n < count;) { + final start = n; + var toAdd = min(batchSize, count - n); + for (var s = 0; s < min(singles, toAdd); s++) { + final data = makeData(n); + await arr.insert(start, data); + match.insert(start, data); + toAdd--; + n++; + } + + final data = makeDataBatch(n, toAdd); + await arr.insertAll(start, data); + match.insertAll(start, data); + n += toAdd; + + print(' $n/$count'); + } + } + + print('get singles'); + { + for (var n = 0; n < batchSize; n++) { + expect(await arr.get(n), equals(match[n])); + } + } + + print('get batch'); + { + for (var n = batchSize; n < count; n += batchSize) { + final toGet = min(batchSize, count - n); + expect(await arr.getRange(n, toGet), + equals(match.sublist(n, n + toGet))); + } + } + + print('clear'); + { + await arr.clear(); + expect(arr.length, isZero); + } + + await arr.close(delete: true); }; diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index 504fb16..dbabd3a 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -16,10 +16,25 @@ class TableDBArray { _initWait.add(_init); } + static Future make({ + required String table, + required VeilidCrypto crypto, + }) async { + final out = TableDBArray(table: table, crypto: crypto); + await out._initWait(); + return out; + } + + Future initWait() async { + await _initWait(); + } + Future _init() async { // Load the array details await _mutex.protect(() async { _tableDB = await Veilid.instance.openTableDB(_table, 1); + await _loadHead(); + _initDone = true; }); } @@ -27,23 +42,45 @@ class TableDBArray { // Ensure the init finished await _initWait(); - await _mutex.acquire(); - - await _changeStream.close(); - _tableDB.close(); - + // Allow multiple attempts to close + if (_open) { + await _mutex.protect(() async { + await _changeStream.close(); + _tableDB.close(); + _open = false; + }); + } if (delete) { await Veilid.instance.deleteTableDB(_table); } } + Future delete() async { + await _initWait(); + if (_open) { + throw StateError('should be closed first'); + } + await Veilid.instance.deleteTableDB(_table); + } + Future> listen(void Function() onChanged) async => _changeStream.stream.listen((_) => onChanged()); //////////////////////////////////////////////////////////// // Public interface - int get length => _length; + int get length { + if (!_open) { + throw StateError('not open'); + } + if (!_initDone) { + throw StateError('not initialized'); + } + + return _length; + } + + bool get isOpen => _open; Future add(Uint8List value) async { await _initWait(); @@ -67,12 +104,22 @@ class TableDBArray { Future get(int pos) async { await _initWait(); - return _mutex.protect(() async => _getInner(pos)); + return _mutex.protect(() async { + if (!_open) { + throw StateError('not open'); + } + return _getInner(pos); + }); } - Future> getAll(int start, int length) async { + Future> getRange(int start, int length) async { await _initWait(); - return _mutex.protect(() async => _getAllInner(start, length)); + return _mutex.protect(() async { + if (!_open) { + throw StateError('not open'); + } + return _getRangeInner(start, length); + }); } Future remove(int pos, {Output? out}) async { @@ -96,6 +143,7 @@ class TableDBArray { } _length = 0; _nextFree = 0; + _maxEntry = 0; _dirtyChunks.clear(); _chunkCache.clear(); }); @@ -175,7 +223,7 @@ class TableDBArray { return (await _loadEntry(entry))!; } - Future> _getAllInner(int start, int length) async { + Future> _getRangeInner(int start, int length) async { if (length < 0) { throw StateError('length should not be negative'); } @@ -252,11 +300,16 @@ class TableDBArray { Future _writeTransaction( Future Function(VeilidTableDBTransaction) closure) async => _mutex.protect(() async { + if (!_open) { + throw StateError('not open'); + } + final _oldLength = _length; final _oldNextFree = _nextFree; + final _oldMaxEntry = _maxEntry; try { final out = await transactionScope(_tableDB, (t) async { - final out = closure(t); + final out = await closure(t); await _saveHead(t); await _flushDirtyChunks(t); return out; @@ -267,6 +320,7 @@ class TableDBArray { // restore head _length = _oldLength; _nextFree = _oldNextFree; + _maxEntry = _oldMaxEntry; // invalidate caches because they could have been written to _chunkCache.clear(); _dirtyChunks.clear(); @@ -322,34 +376,35 @@ class TableDBArray { if (start < 0 || start >= _length) { throw IndexError.withLength(start, _length); } - final end = start + length - 1; // Slide everything over in reverse - final toCopyTotal = _length - start; - var dest = end + toCopyTotal; var src = _length - 1; + var dest = src + length; (int, Uint8List)? lastSrcChunk; (int, Uint8List)? lastDestChunk; while (src >= start) { + final remaining = (src - start) + 1; final srcChunkNumber = src ~/ _indexStride; final srcIndex = src % _indexStride; - final srcLength = srcIndex + 1; + final srcLength = min(remaining, srcIndex + 1); final srcChunk = (lastSrcChunk != null && (lastSrcChunk.$1 == srcChunkNumber)) ? lastSrcChunk.$2 : await _loadIndexChunk(srcChunkNumber); + _dirtyChunks[srcChunkNumber] = srcChunk; lastSrcChunk = (srcChunkNumber, srcChunk); final destChunkNumber = dest ~/ _indexStride; final destIndex = dest % _indexStride; - final destLength = destIndex + 1; + final destLength = min(remaining, destIndex + 1); final destChunk = (lastDestChunk != null && (lastDestChunk.$1 == destChunkNumber)) ? lastDestChunk.$2 : await _loadIndexChunk(destChunkNumber); + _dirtyChunks[destChunkNumber] = destChunk; lastDestChunk = (destChunkNumber, destChunk); final toCopy = min(srcLength, destLength); @@ -395,6 +450,7 @@ class TableDBArray { (lastSrcChunk != null && (lastSrcChunk.$1 == srcChunkNumber)) ? lastSrcChunk.$2 : await _loadIndexChunk(srcChunkNumber); + _dirtyChunks[srcChunkNumber] = srcChunk; lastSrcChunk = (srcChunkNumber, srcChunk); final destChunkNumber = dest ~/ _indexStride; @@ -405,6 +461,7 @@ class TableDBArray { (lastDestChunk != null && (lastDestChunk.$1 == destChunkNumber)) ? lastDestChunk.$2 : await _loadIndexChunk(destChunkNumber); + _dirtyChunks[destChunkNumber] = destChunk; lastDestChunk = (destChunkNumber, destChunk); final toCopy = min(srcLength, destLength); @@ -453,7 +510,7 @@ class TableDBArray { Future _flushDirtyChunks(VeilidTableDBTransaction t) async { for (final ec in _dirtyChunks.entries) { - await _tableDB.store(0, _chunkKey(ec.key), ec.value); + await t.store(0, _chunkKey(ec.key), ec.value); } _dirtyChunks.clear(); } @@ -464,25 +521,28 @@ class TableDBArray { if (headBytes == null) { _length = 0; _nextFree = 0; + _maxEntry = 0; } else { final b = headBytes.buffer.asByteData(); _length = b.getUint32(0); _nextFree = b.getUint32(4); + _maxEntry = b.getUint32(8); } } Future _saveHead(VeilidTableDBTransaction t) async { assert(_mutex.isLocked, 'should be locked'); - final b = ByteData(8) + final b = ByteData(12) ..setUint32(0, _length) - ..setUint32(4, _nextFree); + ..setUint32(4, _nextFree) + ..setUint32(8, _maxEntry); await t.store(0, _headKey, b.buffer.asUint8List()); } Future _allocateEntry() async { assert(_mutex.isLocked, 'should be locked'); if (_nextFree == 0) { - return _length; + return _maxEntry++; } // pop endogenous free list final free = _nextFree; @@ -501,6 +561,8 @@ class TableDBArray { final String _table; late final VeilidTableDB _tableDB; + var _open = true; + var _initDone = false; final VeilidCrypto _crypto; final WaitSet _initWait = WaitSet(); final Mutex _mutex = Mutex(); @@ -508,6 +570,7 @@ class TableDBArray { // Head state int _length = 0; int _nextFree = 0; + int _maxEntry = 0; static const int _indexStride = 16384; final List<(int, Uint8List)> _chunkCache = []; final Map _dirtyChunks = {}; From 17f6dfce466585062c0e94343c2d5358d26b523e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 27 May 2024 13:17:33 -0400 Subject: [PATCH 03/19] tabledbarray tests pass --- .../example/integration_test/app_test.dart | 218 +++++++++--------- .../integration_test/test_table_db_array.dart | 103 ++++++--- .../lib/src/table_db_array.dart | 45 ++-- 3 files changed, 207 insertions(+), 159 deletions(-) diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index b7a807f..6912fd3 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -40,77 +40,7 @@ void main() { group('TableDBArray Tests', () { // test('create/delete TableDBArray', testTableDBArrayCreateDelete); - // group('TableDBArray Add/Get Tests', () { - // for (final params in [ - // // - // (99, 3, 15), - // (100, 4, 16), - // (101, 5, 17), - // // - // (511, 3, 127), - // (512, 4, 128), - // (513, 5, 129), - // // - // (4095, 3, 1023), - // (4096, 4, 1024), - // (4097, 5, 1025), - // // - // (65535, 3, 16383), - // (65536, 4, 16384), - // (65537, 5, 16385), - // ]) { - // final count = params.$1; - // final singles = params.$2; - // final batchSize = params.$3; - - // test( - // // timeout: const Timeout(Duration(seconds: 480)), - // 'add/remove TableDBArray count = $count batchSize=$batchSize', - // makeTestTableDBArrayAddGetClear( - // count: count, - // singles: singles, - // batchSize: batchSize, - // crypto: const VeilidCryptoPublic()), - // ); - // } - // }); - - // group('TableDBArray Insert Tests', () { - // for (final params in [ - // // - // (99, 3, 15), - // (100, 4, 16), - // (101, 5, 17), - // // - // (511, 3, 127), - // (512, 4, 128), - // (513, 5, 129), - // // - // (4095, 3, 1023), - // (4096, 4, 1024), - // (4097, 5, 1025), - // // - // (65535, 3, 16383), - // (65536, 4, 16384), - // (65537, 5, 16385), - // ]) { - // final count = params.$1; - // final singles = params.$2; - // final batchSize = params.$3; - - // test( - // // timeout: const Timeout(Duration(seconds: 480)), - // 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', - // makeTestTableDBArrayInsert( - // count: count, - // singles: singles, - // batchSize: batchSize, - // crypto: const VeilidCryptoPublic()), - // ); - // } - // }); - - group('TableDBArray Remove Tests', () { + group('TableDBArray Add/Get Tests', () { for (final params in [ // (99, 3, 15), @@ -134,7 +64,77 @@ void main() { final batchSize = params.$3; test( - // timeout: const Timeout(Duration(seconds: 480)), + timeout: const Timeout(Duration(seconds: 480)), + 'add/remove TableDBArray count = $count batchSize=$batchSize', + makeTestTableDBArrayAddGetClear( + count: count, + singles: singles, + batchSize: batchSize, + crypto: const VeilidCryptoPublic()), + ); + } + }); + + group('TableDBArray Insert Tests', () { + for (final params in [ + // + (99, 3, 15), + (100, 4, 16), + (101, 5, 17), + // + (511, 3, 127), + (512, 4, 128), + (513, 5, 129), + // + (4095, 3, 1023), + (4096, 4, 1024), + (4097, 5, 1025), + // + (65535, 3, 16383), + (65536, 4, 16384), + (65537, 5, 16385), + ]) { + final count = params.$1; + final singles = params.$2; + final batchSize = params.$3; + + test( + timeout: const Timeout(Duration(seconds: 480)), + 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', + makeTestTableDBArrayInsert( + count: count, + singles: singles, + batchSize: batchSize, + crypto: const VeilidCryptoPublic()), + ); + } + }); + + group('TableDBArray Remove Tests', () { + for (final params in [ + // + (99, 3, 15), + (100, 4, 16), + (101, 5, 17), + // + (511, 3, 127), + (512, 4, 128), + (513, 5, 129), + // + (4095, 3, 1023), + (4096, 4, 1024), + (4097, 5, 1025), + // + (16383, 3, 4095), + (16384, 4, 4096), + (16385, 5, 4097), + ]) { + final count = params.$1; + final singles = params.$2; + final batchSize = params.$3; + + test( + timeout: const Timeout(Duration(seconds: 480)), 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', makeTestTableDBArrayRemove( count: count, @@ -147,50 +147,50 @@ void main() { }); }); - // group('DHT Support Tests', () { - // setUpAll(updateProcessorFixture.setUp); - // setUpAll(tickerFixture.setUp); - // tearDownAll(tickerFixture.tearDown); - // tearDownAll(updateProcessorFixture.tearDown); + group('DHT Support Tests', () { + setUpAll(updateProcessorFixture.setUp); + setUpAll(tickerFixture.setUp); + tearDownAll(tickerFixture.tearDown); + tearDownAll(updateProcessorFixture.tearDown); - // test('create pool', testDHTRecordPoolCreate); + test('create pool', testDHTRecordPoolCreate); - // group('DHTRecordPool Tests', () { - // setUpAll(dhtRecordPoolFixture.setUp); - // tearDownAll(dhtRecordPoolFixture.tearDown); + group('DHTRecordPool Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); - // test('create/delete record', testDHTRecordCreateDelete); - // test('record scopes', testDHTRecordScopes); - // test('create/delete deep record', testDHTRecordDeepCreateDelete); - // }); + test('create/delete record', testDHTRecordCreateDelete); + test('record scopes', testDHTRecordScopes); + test('create/delete deep record', testDHTRecordDeepCreateDelete); + }); - // group('DHTShortArray Tests', () { - // setUpAll(dhtRecordPoolFixture.setUp); - // tearDownAll(dhtRecordPoolFixture.tearDown); + group('DHTShortArray Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); - // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - // test('create shortarray stride=$stride', - // makeTestDHTShortArrayCreateDelete(stride: stride)); - // test('add shortarray stride=$stride', - // makeTestDHTShortArrayAdd(stride: stride)); - // } - // }); + for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + test('create shortarray stride=$stride', + makeTestDHTShortArrayCreateDelete(stride: stride)); + test('add shortarray stride=$stride', + makeTestDHTShortArrayAdd(stride: stride)); + } + }); - // group('DHTLog Tests', () { - // setUpAll(dhtRecordPoolFixture.setUp); - // tearDownAll(dhtRecordPoolFixture.tearDown); + group('DHTLog Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); - // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - // test('create log stride=$stride', - // makeTestDHTLogCreateDelete(stride: stride)); - // test( - // timeout: const Timeout(Duration(seconds: 480)), - // 'add/truncate log stride=$stride', - // makeTestDHTLogAddTruncate(stride: stride), - // ); - // } - // }); - // }); + for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + test('create log stride=$stride', + makeTestDHTLogCreateDelete(stride: stride)); + test( + timeout: const Timeout(Duration(seconds: 480)), + 'add/truncate log stride=$stride', + makeTestDHTLogAddTruncate(stride: stride), + ); + } + }); + }); }); }); } diff --git a/packages/veilid_support/example/integration_test/test_table_db_array.dart b/packages/veilid_support/example/integration_test/test_table_db_array.dart index 81607a6..e67bc39 100644 --- a/packages/veilid_support/example/integration_test/test_table_db_array.dart +++ b/packages/veilid_support/example/integration_test/test_table_db_array.dart @@ -84,7 +84,8 @@ Future Function() makeTestTableDBArrayAddGetClear( { for (var n = batchSize; n < count; n += batchSize) { final toGet = min(batchSize, count - n); - expect(await arr.getRange(n, toGet), equals(makeDataBatch(n, toGet))); + expect(await arr.getRange(n, n + toGet), + equals(makeDataBatch(n, toGet))); } } @@ -140,7 +141,7 @@ Future Function() makeTestTableDBArrayInsert( { for (var n = batchSize; n < count; n += batchSize) { final toGet = min(batchSize, count - n); - expect(await arr.getRange(n, toGet), + expect(await arr.getRange(n, n + toGet), equals(match.sublist(n, n + toGet))); } } @@ -154,7 +155,6 @@ Future Function() makeTestTableDBArrayInsert( await arr.close(delete: true); }; - Future Function() makeTestTableDBArrayRemove( {required int count, required int singles, @@ -164,42 +164,79 @@ Future Function() makeTestTableDBArrayRemove( final arr = await TableDBArray.make(table: 'testArray', crypto: crypto); final match = []; -xxx removal test - print('inserting'); + { - for (var n = 0; n < count;) { - final start = n; - var toAdd = min(batchSize, count - n); - for (var s = 0; s < min(singles, toAdd); s++) { - final data = makeData(n); - await arr.insert(start, data); - match.insert(start, data); - toAdd--; - n++; + final rems = [ + (0, 0), + (0, 1), + (0, batchSize), + (1, batchSize - 1), + (batchSize, 1), + (batchSize + 1, batchSize), + (batchSize - 1, batchSize + 1) + ]; + for (final rem in rems) { + print('adding '); + { + for (var n = match.length; n < count;) { + final toAdd = min(batchSize, count - n); + final data = makeDataBatch(n, toAdd); + await arr.addAll(data); + match.addAll(data); + n += toAdd; + print(' $n/$count'); + } + expect(arr.length, equals(match.length)); } - final data = makeDataBatch(n, toAdd); - await arr.insertAll(start, data); - match.insertAll(start, data); - n += toAdd; + { + final start = rem.$1; + final length = rem.$2; + print('removing start=$start length=$length'); - print(' $n/$count'); - } - } + final out = Output>(); + await arr.removeRange(start, start + length, out: out); + expect(out.value, equals(match.sublist(start, start + length))); + match.removeRange(start, start + length); + expect(arr.length, equals(match.length)); - print('get singles'); - { - for (var n = 0; n < batchSize; n++) { - expect(await arr.get(n), equals(match[n])); - } - } + print('get batch'); + { + final checkCount = match.length; + for (var n = 0; n < checkCount;) { + final toGet = min(batchSize, checkCount - n); + expect(await arr.getRange(n, n + toGet), + equals(match.sublist(n, n + toGet))); + n += toGet; + print(' $n/$checkCount'); + } + } + } - print('get batch'); - { - for (var n = batchSize; n < count; n += batchSize) { - final toGet = min(batchSize, count - n); - expect(await arr.getRange(n, toGet), - equals(match.sublist(n, n + toGet))); + { + final start = match.length - rem.$1 - rem.$2; + final length = rem.$2; + print('removing from end start=$start length=$length'); + + final out = Output>(); + await arr.removeRange(start, start + length, out: out); + expect(out.value, equals(match.sublist(start, start + length))); + match.removeRange(start, start + length); + expect(arr.length, equals(match.length)); + + print('get batch'); + { + final checkCount = match.length; + for (var n = 0; n < checkCount;) { + final toGet = min(batchSize, checkCount - n); + expect(await arr.getRange(n, n + toGet), + equals(match.sublist(n, n + toGet))); + n += toGet; + print(' $n/$checkCount'); + } + expect(arr.length, equals(match.length)); + } + } } } diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index dbabd3a..51b15b8 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -112,13 +112,13 @@ class TableDBArray { }); } - Future> getRange(int start, int length) async { + Future> getRange(int start, int end) async { await _initWait(); return _mutex.protect(() async { if (!_open) { throw StateError('not open'); } - return _getRangeInner(start, length); + return _getRangeInner(start, end); }); } @@ -127,11 +127,11 @@ class TableDBArray { return _writeTransaction((t) async => _removeInner(t, pos, out: out)); } - Future removeRange(int start, int length, + Future removeRange(int start, int end, {Output>? out}) async { await _initWait(); return _writeTransaction( - (t) async => _removeRangeInner(t, start, length, out: out)); + (t) async => _removeRangeInner(t, start, end, out: out)); } Future clear() async { @@ -223,23 +223,34 @@ class TableDBArray { return (await _loadEntry(entry))!; } - Future> _getRangeInner(int start, int length) async { + Future> _getRangeInner(int start, int end) async { + final length = end - start; if (length < 0) { throw StateError('length should not be negative'); } if (start < 0 || start >= _length) { throw IndexError.withLength(start, _length); } - if ((start + length) > _length) { - throw IndexError.withLength(start + length, _length); + if (end > _length) { + throw IndexError.withLength(end, _length); } final out = []; - for (var pos = start; pos < (start + length); pos++) { - final entry = await _getIndexEntry(pos); - final value = (await _loadEntry(entry))!; - out.add(value); + const batchSize = 16; + + for (var pos = start; pos < end;) { + var batchLen = min(batchSize, end - pos); + final dws = DelayedWaitSet(); + while (batchLen > 0) { + final entry = await _getIndexEntry(pos); + dws.add(() async => (await _loadEntry(entry))!); + pos++; + batchLen--; + } + final batchOut = await dws(); + out.addAll(batchOut); } + return out; } @@ -259,21 +270,21 @@ class TableDBArray { await _removeIndexEntry(pos); } - Future _removeRangeInner( - VeilidTableDBTransaction t, int start, int length, + Future _removeRangeInner(VeilidTableDBTransaction t, int start, int end, {Output>? out}) async { + final length = end - start; if (length < 0) { throw StateError('length should not be negative'); } - if (start < 0 || start >= _length) { + if (start < 0) { throw IndexError.withLength(start, _length); } - if ((start + length) > _length) { - throw IndexError.withLength(start + length, _length); + if (end > _length) { + throw IndexError.withLength(end, _length); } final outList = []; - for (var pos = start; pos < (start + length); pos++) { + for (var pos = start; pos < end; pos++) { final entry = await _getIndexEntry(pos); if (out != null) { final value = (await _loadEntry(entry))!; From 9c5feed732145ed3944d2a640c98bd5bb051dc77 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 27 May 2024 18:04:00 -0400 Subject: [PATCH 04/19] messages wip --- lib/chat/cubits/active_chat_cubit.dart | 4 +- .../cubits/single_contact_messages_cubit.dart | 27 ++++- lib/chat/models/message_state.dart | 20 +++- lib/chat/models/message_state.freezed.dart | 6 +- lib/chat/views/chat_component.dart | 108 ++++++++++++++---- .../active_conversations_bloc_map_cubit.dart | 6 +- ...ve_single_contact_chat_bloc_map_cubit.dart | 9 +- lib/chat_list/cubits/chat_list_cubit.dart | 65 ++++++----- .../chat_single_contact_item_widget.dart | 10 +- .../chat_single_contact_list_widget.dart | 6 +- .../views/contact_invitation_item_widget.dart | 6 +- lib/contacts/cubits/contact_list_cubit.dart | 4 +- lib/contacts/views/contact_item_widget.dart | 11 +- .../home_account_ready_chat.dart | 2 +- .../home_account_ready_main.dart | 6 +- lib/proto/veilidchat.pb.dart | 64 +++++------ lib/proto/veilidchat.pbjson.dart | 72 ++++++------ lib/proto/veilidchat.proto | 19 ++- 18 files changed, 274 insertions(+), 171 deletions(-) diff --git a/lib/chat/cubits/active_chat_cubit.dart b/lib/chat/cubits/active_chat_cubit.dart index e47caec..a1872c2 100644 --- a/lib/chat/cubits/active_chat_cubit.dart +++ b/lib/chat/cubits/active_chat_cubit.dart @@ -4,7 +4,7 @@ import 'package:veilid_support/veilid_support.dart'; class ActiveChatCubit extends Cubit { ActiveChatCubit(super.initialState); - void setActiveChat(TypedKey? activeChatRemoteConversationRecordKey) { - emit(activeChatRemoteConversationRecordKey); + void setActiveChat(TypedKey? activeChatLocalConversationRecordKey) { + emit(activeChatLocalConversationRecordKey); } } diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index e018e79..20c8d22 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -53,14 +53,12 @@ class SingleContactMessagesCubit extends Cubit { 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, super(const AsyncValue.loading()) { // Async Init _initWait.add(_init); @@ -420,7 +418,14 @@ class SingleContactMessagesCubit extends Cubit { emit(AsyncValue.data(renderedState)); } - void addMessage({required proto.Message message}) { + void addTextMessage({required proto.Message_Text messageText}) { + final message = proto.Message() + ..author = _activeAccountInfo.localAccount.identityMaster + .identityPublicTypedKey() + .toProto() + ..timestamp = Veilid.instance.now().toInt64() + ..text = messageText; + _unreconciledMessagesQueue.addSync(message); _sendingMessagesQueue.addSync(message); @@ -428,6 +433,21 @@ class SingleContactMessagesCubit extends Cubit { _renderState(); } + ///////////////////////////////////////////////////////////////////////// + + static Future cleanupAndDeleteMessages( + {required TypedKey localConversationRecordKey}) async { + final recmsgdbname = + _reconciledMessagesTableDBName(localConversationRecordKey); + await Veilid.instance.deleteTableDB(recmsgdbname); + } + + static String _reconciledMessagesTableDBName( + TypedKey localConversationRecordKey) => + 'msg_$localConversationRecordKey'; + + ///////////////////////////////////////////////////////////////////////// + final WaitSet _initWait = WaitSet(); final ActiveAccountInfo _activeAccountInfo; final TypedKey _remoteIdentityPublicKey; @@ -435,7 +455,6 @@ class SingleContactMessagesCubit extends Cubit { final TypedKey _localMessagesRecordKey; final TypedKey _remoteConversationRecordKey; final TypedKey _remoteMessagesRecordKey; - final OwnedDHTRecordPointer _reconciledChatRecord; late final VeilidCrypto _messagesCrypto; diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart index 8618054..e14c9e8 100644 --- a/lib/chat/models/message_state.dart +++ b/lib/chat/models/message_state.dart @@ -30,10 +30,28 @@ class MessageState with _$MessageState { required proto.Message content, // Received or delivered timestamp required Timestamp timestamp, - // The state of the mssage + // The state of the message required MessageSendState? sendState, }) = _MessageState; factory MessageState.fromJson(dynamic json) => _$MessageStateFromJson(json as Map); } + +extension MessageStateExt on MessageState { + String get uniqueId { + final author = content.author.toVeilid().toString(); + final id = base64UrlNoPadEncode(content.id); + return '$author|$id'; + } + + static (proto.TypedKey, Uint8List) splitUniqueId(String uniqueId) { + final parts = uniqueId.split('|'); + if (parts.length != 2) { + throw Exception('invalid unique id'); + } + final author = TypedKey.fromString(parts[0]).toProto(); + final id = base64UrlNoPadDecode(parts[1]); + return (author, id); + } +} diff --git a/lib/chat/models/message_state.freezed.dart b/lib/chat/models/message_state.freezed.dart index ec8195b..b411f4c 100644 --- a/lib/chat/models/message_state.freezed.dart +++ b/lib/chat/models/message_state.freezed.dart @@ -25,7 +25,7 @@ mixin _$MessageState { proto.Message get content => throw _privateConstructorUsedError; // Received or delivered timestamp Timestamp get timestamp => - throw _privateConstructorUsedError; // The state of the mssage + throw _privateConstructorUsedError; // The state of the message MessageSendState? get sendState => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @@ -147,7 +147,7 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { // Received or delivered timestamp @override final Timestamp timestamp; -// The state of the mssage +// The state of the message @override final MessageSendState? sendState; @@ -211,7 +211,7 @@ abstract class _MessageState implements MessageState { proto.Message get content; @override // Received or delivered timestamp Timestamp get timestamp; - @override // The state of the mssage + @override // The state of the message MessageSendState? get sendState; @override @JsonKey(ignore: true) diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 8ca471e..2ca49a1 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -1,5 +1,8 @@ +import 'dart:typed_data'; + import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; @@ -13,6 +16,10 @@ import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../chat.dart'; +const String metadataKeyExpirationDuration = 'expiration'; +const String metadataKeyViewLimit = 'view_limit'; +const String metadataKeyAttachments = 'attachments'; + class ChatComponent extends StatelessWidget { const ChatComponent._( {required TypedKey localUserIdentityKey, @@ -35,7 +42,7 @@ class ChatComponent extends StatelessWidget { // Builder wrapper function that takes care of state management requirements static Widget builder( - {required TypedKey remoteConversationRecordKey, Key? key}) => + {required TypedKey localConversationRecordKey, Key? key}) => Builder(builder: (context) { // Get all watched dependendies final activeAccountInfo = context.watch(); @@ -51,7 +58,7 @@ class ChatComponent extends StatelessWidget { } final avconversation = context.select?>( - (x) => x.state[remoteConversationRecordKey]); + (x) => x.state[localConversationRecordKey]); if (avconversation == null) { return waitingPage(); } @@ -77,7 +84,7 @@ class ChatComponent extends StatelessWidget { // Get the messages cubit final messages = context.select( - (x) => x.tryOperate(remoteConversationRecordKey, + (x) => x.tryOperate(localConversationRecordKey, closure: (cubit) => (cubit, cubit.state))); // Get the messages to display @@ -97,8 +104,8 @@ class ChatComponent extends StatelessWidget { ///////////////////////////////////////////////////////////////////// - types.Message messageToChatMessage(MessageState message) { - final isLocal = message.author == _localUserIdentityKey; + types.Message? messageToChatMessage(MessageState message) { + final isLocal = message.content.author.toVeilid() == _localUserIdentityKey; types.Status? status; if (message.sendState != null) { @@ -113,31 +120,83 @@ class ChatComponent extends StatelessWidget { } } - final textMessage = types.TextMessage( - author: isLocal ? _localUser : _remoteUser, - createdAt: (message.timestamp.value ~/ BigInt.from(1000)).toInt(), - id: message.timestamp.toString(), - text: message.text, - showStatus: status != null, - status: status); - return textMessage; + switch (message.content.whichKind()) { + case proto.Message_Kind.text: + final contextText = message.content.text; + final textMessage = types.TextMessage( + author: isLocal ? _localUser : _remoteUser, + createdAt: (message.timestamp.value ~/ BigInt.from(1000)).toInt(), + id: message.uniqueId, + text: contextText.text, + showStatus: status != null, + status: status); + return textMessage; + case proto.Message_Kind.secret: + case proto.Message_Kind.delete: + case proto.Message_Kind.erase: + case proto.Message_Kind.settings: + case proto.Message_Kind.permissions: + case proto.Message_Kind.membership: + case proto.Message_Kind.moderation: + case proto.Message_Kind.notSet: + return null; + } } - void _addMessage(proto.Message message) { - if (message.text.isEmpty) { - return; + void _addTextMessage( + {required String text, + String? topic, + Uint8List? replyId, + Timestamp? expiration, + int? viewLimit, + List attachments = const []}) { + final protoMessageText = proto.Message_Text()..text = text; + if (topic != null) { + protoMessageText.topic = topic; } - _messagesCubit.addMessage(message: message); + if (replyId != null) { + protoMessageText.replyId = replyId; + } + protoMessageText + ..expiration = expiration?.toInt64() ?? Int64.ZERO + ..viewLimit = viewLimit ?? 0; + protoMessageText.attachments.addAll(attachments); + + _messagesCubit.addTextMessage(messageText: protoMessageText); } void _handleSendPressed(types.PartialText message) { - final protoMessage = proto.Message() - ..author = _localUserIdentityKey.toProto() - ..timestamp = Veilid.instance.now().toInt64() - ..text = message.text; - //..signature = signature; + final text = message.text; + final replyId = (message.repliedMessage != null) + ? MessageStateExt.splitUniqueId(message.repliedMessage!.id).$2 + : null; + Timestamp? expiration; + int? viewLimit; + List? attachments; + final metadata = message.metadata; + if (metadata != null) { + final expirationValue = + metadata[metadataKeyExpirationDuration] as TimestampDuration?; + if (expirationValue != null) { + expiration = Veilid.instance.now().offset(expirationValue); + } + final viewLimitValue = metadata[metadataKeyViewLimit] as int?; + if (viewLimitValue != null) { + viewLimit = viewLimitValue; + } + final attachmentsValue = + metadata[metadataKeyAttachments] as List?; + if (attachmentsValue != null) { + attachments = attachmentsValue; + } + } - _addMessage(protoMessage); + _addTextMessage( + text: text, + replyId: replyId, + expiration: expiration, + viewLimit: viewLimit, + attachments: attachments ?? []); } // void _handleAttachmentPressed() async { @@ -161,6 +220,9 @@ class ChatComponent extends StatelessWidget { final tsSet = {}; for (final message in messages) { final chatMessage = messageToChatMessage(message); + if (chatMessage == null) { + continue; + } chatMessages.insert(0, chatMessage); if (!tsSet.add(chatMessage.id)) { // ignore: avoid_print diff --git a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart index b221208..c497941 100644 --- a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart @@ -31,7 +31,7 @@ typedef ActiveConversationCubit = TransformerCubit< typedef ActiveConversationsBlocMapState = BlocMapState>; -// Map of remoteConversationRecordKey to ActiveConversationCubit +// Map of localConversationRecordKey to ActiveConversationCubit // Wraps a conversation cubit to only expose completely built conversations // Automatically follows the state of a ChatListCubit. // Even though 'conversations' are per-contact and not per-chat @@ -49,7 +49,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit _addConversation({required proto.Contact contact}) async => add(() => MapEntry( - contact.remoteConversationRecordKey.toVeilid(), + contact.localConversationRecordKey.toVeilid(), TransformerCubit( ConversationCubit( activeAccountInfo: _activeAccountInfo, @@ -86,7 +86,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit c.value.remoteConversationRecordKey.toVeilid() == key); + (c) => c.value.localConversationRecordKey.toVeilid() == key); if (contactIndex == -1) { await addState(key, AsyncValue.error('Contact not found')); return; diff --git a/lib/chat_list/cubits/active_single_contact_chat_bloc_map_cubit.dart b/lib/chat_list/cubits/active_single_contact_chat_bloc_map_cubit.dart index d9ecb67..914d357 100644 --- a/lib/chat_list/cubits/active_single_contact_chat_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_single_contact_chat_bloc_map_cubit.dart @@ -11,7 +11,7 @@ import '../../proto/proto.dart' as proto; import 'active_conversations_bloc_map_cubit.dart'; import 'chat_list_cubit.dart'; -// Map of remoteConversationRecordKey to MessagesCubit +// Map of localConversationRecordKey to MessagesCubit // Wraps a MessagesCubit to stream the latest messages to the state // Automatically follows the state of a ActiveConversationsBlocMapCubit. class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit add(() => MapEntry( - contact.remoteConversationRecordKey.toVeilid(), + contact.localConversationRecordKey.toVeilid(), SingleContactMessagesCubit( activeAccountInfo: _activeAccountInfo, remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), @@ -43,7 +43,6 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit c.value.remoteConversationRecordKey.toVeilid() == key); + (c) => c.value.localConversationRecordKey.toVeilid() == key); if (contactIndex == -1) { await addState( key, AsyncValue.error('Contact not found for conversation')); @@ -76,7 +75,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit c.value.remoteConversationRecordKey.toVeilid() == key); + (c) => c.value.localConversationRecordKey.toVeilid() == key); if (contactIndex == -1) { await addState(key, AsyncValue.error('Chat not found for conversation')); return; diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index d04b008..ff1d5d0 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -21,8 +22,7 @@ class ChatListCubit extends DHTShortArrayCubit required ActiveAccountInfo activeAccountInfo, required proto.Account account, required this.activeChatCubit, - }) : _activeAccountInfo = activeAccountInfo, - super( + }) : super( open: () => _open(activeAccountInfo, account), decodeElement: proto.Chat.fromBuffer); @@ -39,16 +39,30 @@ class ChatListCubit extends DHTShortArrayCubit return dhtRecord; } + Future getDefaultChatSettings( + proto.Contact contact) async { + final pronouns = contact.editedProfile.pronouns.isEmpty + ? '' + : ' (${contact.editedProfile.pronouns})'; + return proto.ChatSettings() + ..title = '${contact.editedProfile.name}$pronouns' + ..description = '' + ..defaultExpiration = Int64.ZERO; + } + /// Create a new chat (singleton for single contact chats) Future getOrCreateChatSingleContact({ - required TypedKey remoteConversationRecordKey, + required proto.Contact contact, }) async { + // Make local copy so we don't share the buffer + final localConversationRecordKey = + contact.localConversationRecordKey.toVeilid(); + final remoteConversationRecordKey = + contact.remoteConversationRecordKey.toVeilid(); + // Add Chat to account's list // if this fails, don't keep retrying, user can try again later await operateWrite((writer) async { - final remoteConversationRecordKeyProto = - remoteConversationRecordKey.toProto(); - // See if we have added this chat already for (var i = 0; i < writer.length; i++) { final cbuf = await writer.getItem(i); @@ -56,26 +70,18 @@ class ChatListCubit extends DHTShortArrayCubit throw Exception('Failed to get chat'); } final c = proto.Chat.fromBuffer(cbuf); - if (c.remoteConversationRecordKey == remoteConversationRecordKeyProto) { + if (c.localConversationRecordKey == + contact.localConversationRecordKey) { // Nothing to do here return; } } - final accountRecordKey = _activeAccountInfo - .userLogin.accountRecordInfo.accountRecord.recordKey; - // Make a record that can store the reconciled version of the chat - final reconciledChatRecord = await (await DHTLog.create( - debugName: - 'ChatListCubit::getOrCreateChatSingleContact::ReconciledChat', - parent: accountRecordKey)) - .scope((r) async => r.recordPointer); - - // Create conversation type Chat + // Create 1:1 conversation type Chat final chat = proto.Chat() - ..type = proto.ChatType.SINGLE_CONTACT - ..remoteConversationRecordKey = remoteConversationRecordKeyProto - ..reconciledChatRecord = reconciledChatRecord.toProto(); + ..settings = await getDefaultChatSettings(contact) + ..localConversationRecordKey = localConversationRecordKey.toProto() + ..remoteConversationRecordKey = remoteConversationRecordKey.toProto(); // Add chat final added = await writer.tryAddItem(chat.writeToBuffer()); @@ -87,15 +93,16 @@ class ChatListCubit extends DHTShortArrayCubit /// Delete a chat Future deleteChat( - {required TypedKey remoteConversationRecordKey}) async { - final remoteConversationKey = remoteConversationRecordKey.toProto(); + {required TypedKey localConversationRecordKey}) async { + final localConversationRecordKeyProto = + localConversationRecordKey.toProto(); // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later final deletedItem = // Ensure followers get their changes before we return await syncFollowers(() => operateWrite((writer) async { - if (activeChatCubit.state == remoteConversationRecordKey) { + if (activeChatCubit.state == localConversationRecordKey) { activeChatCubit.setActiveChat(null); } for (var i = 0; i < writer.length; i++) { @@ -104,7 +111,8 @@ class ChatListCubit extends DHTShortArrayCubit if (c == null) { throw Exception('Failed to get chat'); } - if (c.remoteConversationRecordKey == remoteConversationKey) { + if (c.localConversationRecordKey == + localConversationRecordKeyProto) { // Found the right chat await writer.removeItem(i); return c; @@ -116,10 +124,10 @@ class ChatListCubit extends DHTShortArrayCubit // chat record now if (deletedItem != null) { try { - await DHTRecordPool.instance.deleteRecord( - deletedItem.reconciledChatRecord.toVeilid().recordKey); + await SingleContactMessagesCubit.cleanupAndDeleteMessages( + localConversationRecordKey: localConversationRecordKey); } on Exception catch (e) { - log.debug('error removing reconciled chat record: $e', e); + log.debug('error removing reconciled chat table: $e', e); } } } @@ -132,10 +140,9 @@ class ChatListCubit extends DHTShortArrayCubit return IMap(); } return IMap.fromIterable(stateValue, - keyMapper: (e) => e.value.remoteConversationRecordKey.toVeilid(), + keyMapper: (e) => e.value.localConversationRecordKey.toVeilid(), valueMapper: (e) => e.value); } final ActiveChatCubit activeChatCubit; - final ActiveAccountInfo _activeAccountInfo; } diff --git a/lib/chat_list/views/chat_single_contact_item_widget.dart b/lib/chat_list/views/chat_single_contact_item_widget.dart index 75501b4..ce9cf0e 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -24,9 +24,9 @@ class ChatSingleContactItemWidget extends StatelessWidget { BuildContext context, ) { final activeChatCubit = context.watch(); - final remoteConversationRecordKey = - _contact.remoteConversationRecordKey.toVeilid(); - final selected = activeChatCubit.state == remoteConversationRecordKey; + final localConversationRecordKey = + _contact.localConversationRecordKey.toVeilid(); + final selected = activeChatCubit.state == localConversationRecordKey; return SliderTile( key: ObjectKey(_contact), @@ -38,7 +38,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { icon: Icons.chat, onTap: () { singleFuture(activeChatCubit, () async { - activeChatCubit.setActiveChat(remoteConversationRecordKey); + activeChatCubit.setActiveChat(localConversationRecordKey); }); }, endActions: [ @@ -49,7 +49,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { onPressed: (context) async { final chatListCubit = context.read(); await chatListCubit.deleteChat( - remoteConversationRecordKey: remoteConversationRecordKey); + localConversationRecordKey: localConversationRecordKey); }) ], ); diff --git a/lib/chat_list/views/chat_single_contact_list_widget.dart b/lib/chat_list/views/chat_single_contact_list_widget.dart index 785dbcb..9053bc6 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -20,7 +20,7 @@ class ChatSingleContactListWidget extends StatelessWidget { return contactListV.builder((context, contactList) { final contactMap = IMap.fromIterable(contactList, - keyMapper: (c) => c.value.remoteConversationRecordKey, + keyMapper: (c) => c.value.localConversationRecordKey, valueMapper: (c) => c.value); final chatListV = context.watch().state; @@ -36,7 +36,7 @@ class ChatSingleContactListWidget extends StatelessWidget { initialList: chatList.map((x) => x.value).toList(), itemBuilder: (c) { final contact = - contactMap[c.remoteConversationRecordKey]; + contactMap[c.localConversationRecordKey]; if (contact == null) { return const Text('...'); } @@ -49,7 +49,7 @@ class ChatSingleContactListWidget extends StatelessWidget { final lowerValue = value.toLowerCase(); return chatList.map((x) => x.value).where((c) { final contact = - contactMap[c.remoteConversationRecordKey]; + contactMap[c.localConversationRecordKey]; if (contact == null) { return false; } diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index fcf021f..c2a93c7 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -27,12 +27,12 @@ class ContactInvitationItemWidget extends StatelessWidget { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - // final remoteConversationKey = - // contact.remoteConversationRecordKey.toVeilid(); + // final localConversationKey = + // contact.localConversationRecordKey.toVeilid(); const selected = false; // xxx: eventually when we have selectable invitations: - // activeContactCubit.state == remoteConversationRecordKey; + // activeContactCubit.state == localConversationRecordKey; final tileDisabled = disabled || context.watch().isBusy; diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index a139b89..2eb8a08 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -76,8 +76,8 @@ class ContactListCubit extends DHTShortArrayCubit { if (item == null) { throw Exception('Failed to get contact'); } - if (item.remoteConversationRecordKey == - contact.remoteConversationRecordKey) { + if (item.localConversationRecordKey == + contact.localConversationRecordKey) { await writer.removeItem(i); return item; } diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index dfe9e6e..3deae23 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -29,11 +29,11 @@ class ContactItemWidget extends StatelessWidget { Widget build( BuildContext context, ) { - final remoteConversationKey = - contact.remoteConversationRecordKey.toVeilid(); + final localConversationRecordKey = + contact.localConversationRecordKey.toVeilid(); const selected = false; // xxx: eventually when we have selectable contacts: - // activeContactCubit.state == remoteConversationRecordKey; + // activeContactCubit.state == localConversationRecordKey; final tileDisabled = disabled || context.watch().isBusy; @@ -49,8 +49,7 @@ class ContactItemWidget extends StatelessWidget { // Start a chat final chatListCubit = context.read(); - await chatListCubit.getOrCreateChatSingleContact( - remoteConversationRecordKey: remoteConversationKey); + await chatListCubit.getOrCreateChatSingleContact(contact: contact); // Click over to chats if (context.mounted) { await MainPager.of(context) @@ -69,7 +68,7 @@ class ContactItemWidget extends StatelessWidget { // Remove any chats for this contact await chatListCubit.deleteChat( - remoteConversationRecordKey: remoteConversationKey); + localConversationRecordKey: localConversationRecordKey); // Delete the contact itself await contactListCubit.deleteContact(contact: contact); diff --git a/lib/layout/home/home_account_ready/home_account_ready_chat.dart b/lib/layout/home/home_account_ready/home_account_ready_chat.dart index 621f9e8..fb0e7b4 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_chat.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_chat.dart @@ -34,7 +34,7 @@ class HomeAccountReadyChatState extends State { return const EmptyChatWidget(); } return ChatComponent.builder( - remoteConversationRecordKey: activeChatRemoteConversationKey); + localConversationRecordKey: activeChatRemoteConversationKey); } @override diff --git a/lib/layout/home/home_account_ready/home_account_ready_main.dart b/lib/layout/home/home_account_ready/home_account_ready_main.dart index e6ed99e..59eb00b 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_main.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_main.dart @@ -66,13 +66,13 @@ class _HomeAccountReadyMainState extends State { Material(color: Colors.transparent, child: buildUserPanel())); Widget buildTabletRightPane(BuildContext context) { - final activeChatRemoteConversationKey = + final activeChatLocalConversationKey = context.watch().state; - if (activeChatRemoteConversationKey == null) { + if (activeChatLocalConversationKey == null) { return const EmptyChatWidget(); } return ChatComponent.builder( - remoteConversationRecordKey: activeChatRemoteConversationKey); + localConversationRecordKey: activeChatLocalConversationKey); } // ignore: prefer_expression_function_bodies diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 164c117..3544f00 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -350,9 +350,9 @@ class Message_Text extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.Text', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'text') ..aOS(2, _omitFieldNames ? '' : 'topic') - ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'replyId', subBuilder: $0.TypedKey.create) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'replyId', $pb.PbFieldType.OY) ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) - ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'viewLimit', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.int>(5, _omitFieldNames ? '' : 'viewLimit', $pb.PbFieldType.OU3) ..pc(6, _omitFieldNames ? '' : 'attachments', $pb.PbFieldType.PM, subBuilder: Attachment.create) ..hasRequiredFields = false ; @@ -397,15 +397,13 @@ class Message_Text extends $pb.GeneratedMessage { void clearTopic() => clearField(2); @$pb.TagNumber(3) - $0.TypedKey get replyId => $_getN(2); + $core.List<$core.int> get replyId => $_getN(2); @$pb.TagNumber(3) - set replyId($0.TypedKey v) { setField(3, v); } + set replyId($core.List<$core.int> v) { $_setBytes(2, v); } @$pb.TagNumber(3) $core.bool hasReplyId() => $_has(2); @$pb.TagNumber(3) void clearReplyId() => clearField(3); - @$pb.TagNumber(3) - $0.TypedKey ensureReplyId() => $_ensure(2); @$pb.TagNumber(4) $fixnum.Int64 get expiration => $_getI64(3); @@ -417,9 +415,9 @@ class Message_Text extends $pb.GeneratedMessage { void clearExpiration() => clearField(4); @$pb.TagNumber(5) - $fixnum.Int64 get viewLimit => $_getI64(4); + $core.int get viewLimit => $_getIZ(4); @$pb.TagNumber(5) - set viewLimit($fixnum.Int64 v) { $_setInt64(4, v); } + set viewLimit($core.int v) { $_setUnsignedInt32(4, v); } @$pb.TagNumber(5) $core.bool hasViewLimit() => $_has(4); @$pb.TagNumber(5) @@ -517,13 +515,13 @@ class Message_ControlDelete extends $pb.GeneratedMessage { $core.List<$0.TypedKey> get ids => $_getList(0); } -class Message_ControlClear extends $pb.GeneratedMessage { - factory Message_ControlClear() => create(); - Message_ControlClear._() : super(); - factory Message_ControlClear.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Message_ControlClear.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); +class Message_ControlErase extends $pb.GeneratedMessage { + factory Message_ControlErase() => create(); + Message_ControlErase._() : super(); + factory Message_ControlErase.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlErase.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlClear', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlErase', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..hasRequiredFields = false ; @@ -532,22 +530,22 @@ class Message_ControlClear extends $pb.GeneratedMessage { 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Will be removed in next major version') - Message_ControlClear clone() => Message_ControlClear()..mergeFromMessage(this); + Message_ControlErase clone() => Message_ControlErase()..mergeFromMessage(this); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Will be removed in next major version') - Message_ControlClear copyWith(void Function(Message_ControlClear) updates) => super.copyWith((message) => updates(message as Message_ControlClear)) as Message_ControlClear; + Message_ControlErase copyWith(void Function(Message_ControlErase) updates) => super.copyWith((message) => updates(message as Message_ControlErase)) as Message_ControlErase; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') - static Message_ControlClear create() => Message_ControlClear._(); - Message_ControlClear createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static Message_ControlErase create() => Message_ControlErase._(); + Message_ControlErase createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Message_ControlClear getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Message_ControlClear? _defaultInstance; + static Message_ControlErase getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlErase? _defaultInstance; @$pb.TagNumber(1) $fixnum.Int64 get timestamp => $_getI64(0); @@ -735,7 +733,7 @@ enum Message_Kind { text, secret, delete, - clear_7, + erase, settings, permissions, membership, @@ -753,7 +751,7 @@ class Message extends $pb.GeneratedMessage { 4 : Message_Kind.text, 5 : Message_Kind.secret, 6 : Message_Kind.delete, - 7 : Message_Kind.clear_7, + 7 : Message_Kind.erase, 8 : Message_Kind.settings, 9 : Message_Kind.permissions, 10 : Message_Kind.membership, @@ -762,13 +760,13 @@ class Message extends $pb.GeneratedMessage { }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..oo(0, [4, 5, 6, 7, 8, 9, 10, 11]) - ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'id', subBuilder: $0.TypedKey.create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'id', $pb.PbFieldType.OY) ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'author', subBuilder: $0.TypedKey.create) ..a<$fixnum.Int64>(3, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..aOM(4, _omitFieldNames ? '' : 'text', subBuilder: Message_Text.create) ..aOM(5, _omitFieldNames ? '' : 'secret', subBuilder: Message_Secret.create) ..aOM(6, _omitFieldNames ? '' : 'delete', subBuilder: Message_ControlDelete.create) - ..aOM(7, _omitFieldNames ? '' : 'clear', subBuilder: Message_ControlClear.create) + ..aOM(7, _omitFieldNames ? '' : 'erase', subBuilder: Message_ControlErase.create) ..aOM(8, _omitFieldNames ? '' : 'settings', subBuilder: Message_ControlSettings.create) ..aOM(9, _omitFieldNames ? '' : 'permissions', subBuilder: Message_ControlPermissions.create) ..aOM(10, _omitFieldNames ? '' : 'membership', subBuilder: Message_ControlMembership.create) @@ -802,15 +800,13 @@ class Message extends $pb.GeneratedMessage { void clearKind() => clearField($_whichOneof(0)); @$pb.TagNumber(1) - $0.TypedKey get id => $_getN(0); + $core.List<$core.int> get id => $_getN(0); @$pb.TagNumber(1) - set id($0.TypedKey v) { setField(1, v); } + set id($core.List<$core.int> v) { $_setBytes(0, v); } @$pb.TagNumber(1) $core.bool hasId() => $_has(0); @$pb.TagNumber(1) void clearId() => clearField(1); - @$pb.TagNumber(1) - $0.TypedKey ensureId() => $_ensure(0); @$pb.TagNumber(2) $0.TypedKey get author => $_getN(1); @@ -866,15 +862,15 @@ class Message extends $pb.GeneratedMessage { Message_ControlDelete ensureDelete() => $_ensure(5); @$pb.TagNumber(7) - Message_ControlClear get clear_7 => $_getN(6); + Message_ControlErase get erase => $_getN(6); @$pb.TagNumber(7) - set clear_7(Message_ControlClear v) { setField(7, v); } + set erase(Message_ControlErase v) { setField(7, v); } @$pb.TagNumber(7) - $core.bool hasClear_7() => $_has(6); + $core.bool hasErase() => $_has(6); @$pb.TagNumber(7) - void clearClear_7() => clearField(7); + void clearErase() => clearField(7); @$pb.TagNumber(7) - Message_ControlClear ensureClear_7() => $_ensure(6); + Message_ControlErase ensureErase() => $_ensure(6); @$pb.TagNumber(8) Message_ControlSettings get settings => $_getN(7); diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index 2aaecb2..1470054 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -159,20 +159,20 @@ final $typed_data.Uint8List chatSettingsDescriptor = $convert.base64Decode( const Message$json = { '1': 'Message', '2': [ - {'1': 'id', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'id'}, + {'1': 'id', '3': 1, '4': 1, '5': 12, '10': 'id'}, {'1': 'author', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'author'}, {'1': 'timestamp', '3': 3, '4': 1, '5': 4, '10': 'timestamp'}, {'1': 'text', '3': 4, '4': 1, '5': 11, '6': '.veilidchat.Message.Text', '9': 0, '10': 'text'}, {'1': 'secret', '3': 5, '4': 1, '5': 11, '6': '.veilidchat.Message.Secret', '9': 0, '10': 'secret'}, {'1': 'delete', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlDelete', '9': 0, '10': 'delete'}, - {'1': 'clear', '3': 7, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlClear', '9': 0, '10': 'clear'}, + {'1': 'erase', '3': 7, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlErase', '9': 0, '10': 'erase'}, {'1': 'settings', '3': 8, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlSettings', '9': 0, '10': 'settings'}, {'1': 'permissions', '3': 9, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlPermissions', '9': 0, '10': 'permissions'}, {'1': 'membership', '3': 10, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlMembership', '9': 0, '10': 'membership'}, {'1': 'moderation', '3': 11, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlModeration', '9': 0, '10': 'moderation'}, {'1': 'signature', '3': 12, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, ], - '3': [Message_Text$json, Message_Secret$json, Message_ControlDelete$json, Message_ControlClear$json, Message_ControlSettings$json, Message_ControlPermissions$json, Message_ControlMembership$json, Message_ControlModeration$json], + '3': [Message_Text$json, Message_Secret$json, Message_ControlDelete$json, Message_ControlErase$json, Message_ControlSettings$json, Message_ControlPermissions$json, Message_ControlMembership$json, Message_ControlModeration$json], '8': [ {'1': 'kind'}, ], @@ -183,12 +183,16 @@ const Message_Text$json = { '1': 'Text', '2': [ {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'}, - {'1': 'topic', '3': 2, '4': 1, '5': 9, '10': 'topic'}, - {'1': 'reply_id', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'replyId'}, + {'1': 'topic', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'topic', '17': true}, + {'1': 'reply_id', '3': 3, '4': 1, '5': 12, '9': 1, '10': 'replyId', '17': true}, {'1': 'expiration', '3': 4, '4': 1, '5': 4, '10': 'expiration'}, - {'1': 'view_limit', '3': 5, '4': 1, '5': 4, '10': 'viewLimit'}, + {'1': 'view_limit', '3': 5, '4': 1, '5': 13, '10': 'viewLimit'}, {'1': 'attachments', '3': 6, '4': 3, '5': 11, '6': '.veilidchat.Attachment', '10': 'attachments'}, ], + '8': [ + {'1': '_topic'}, + {'1': '_reply_id'}, + ], }; @$core.Deprecated('Use messageDescriptor instead') @@ -209,8 +213,8 @@ const Message_ControlDelete$json = { }; @$core.Deprecated('Use messageDescriptor instead') -const Message_ControlClear$json = { - '1': 'ControlClear', +const Message_ControlErase$json = { + '1': 'ControlErase', '2': [ {'1': 'timestamp', '3': 1, '4': 1, '5': 4, '10': 'timestamp'}, ], @@ -251,32 +255,32 @@ const Message_ControlModeration$json = { /// Descriptor for `Message`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List messageDescriptor = $convert.base64Decode( - 'CgdNZXNzYWdlEiAKAmlkGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5UgJpZBIoCgZhdXRob3IYAi' - 'ABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBmF1dGhvchIcCgl0aW1lc3RhbXAYAyABKARSCXRpbWVz' - 'dGFtcBIuCgR0ZXh0GAQgASgLMhgudmVpbGlkY2hhdC5NZXNzYWdlLlRleHRIAFIEdGV4dBI0Cg' - 'ZzZWNyZXQYBSABKAsyGi52ZWlsaWRjaGF0Lk1lc3NhZ2UuU2VjcmV0SABSBnNlY3JldBI7CgZk' - 'ZWxldGUYBiABKAsyIS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbERlbGV0ZUgAUgZkZWxldG' - 'USOAoFY2xlYXIYByABKAsyIC52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbENsZWFySABSBWNs' - 'ZWFyEkEKCHNldHRpbmdzGAggASgLMiMudmVpbGlkY2hhdC5NZXNzYWdlLkNvbnRyb2xTZXR0aW' - '5nc0gAUghzZXR0aW5ncxJKCgtwZXJtaXNzaW9ucxgJIAEoCzImLnZlaWxpZGNoYXQuTWVzc2Fn' - 'ZS5Db250cm9sUGVybWlzc2lvbnNIAFILcGVybWlzc2lvbnMSRwoKbWVtYmVyc2hpcBgKIAEoCz' - 'IlLnZlaWxpZGNoYXQuTWVzc2FnZS5Db250cm9sTWVtYmVyc2hpcEgAUgptZW1iZXJzaGlwEkcK' - 'Cm1vZGVyYXRpb24YCyABKAsyJS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbE1vZGVyYXRpb2' - '5IAFIKbW9kZXJhdGlvbhIvCglzaWduYXR1cmUYDCABKAsyES52ZWlsaWQuU2lnbmF0dXJlUglz' - 'aWduYXR1cmUa1gEKBFRleHQSEgoEdGV4dBgBIAEoCVIEdGV4dBIUCgV0b3BpYxgCIAEoCVIFdG' - '9waWMSKwoIcmVwbHlfaWQYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSB3JlcGx5SWQSHgoKZXhw' - 'aXJhdGlvbhgEIAEoBFIKZXhwaXJhdGlvbhIdCgp2aWV3X2xpbWl0GAUgASgEUgl2aWV3TGltaX' - 'QSOAoLYXR0YWNobWVudHMYBiADKAsyFi52ZWlsaWRjaGF0LkF0dGFjaG1lbnRSC2F0dGFjaG1l' - 'bnRzGkgKBlNlY3JldBIeCgpjaXBoZXJ0ZXh0GAEgASgMUgpjaXBoZXJ0ZXh0Eh4KCmV4cGlyYX' - 'Rpb24YAiABKARSCmV4cGlyYXRpb24aMwoNQ29udHJvbERlbGV0ZRIiCgNpZHMYASADKAsyEC52' - 'ZWlsaWQuVHlwZWRLZXlSA2lkcxosCgxDb250cm9sQ2xlYXISHAoJdGltZXN0YW1wGAEgASgEUg' - 'l0aW1lc3RhbXAaRwoPQ29udHJvbFNldHRpbmdzEjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlk' - 'Y2hhdC5DaGF0U2V0dGluZ3NSCHNldHRpbmdzGk8KEkNvbnRyb2xQZXJtaXNzaW9ucxI5CgtwZX' - 'JtaXNzaW9ucxgBIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbnNSC3Blcm1pc3Npb25zGksK' - 'EUNvbnRyb2xNZW1iZXJzaGlwEjYKCm1lbWJlcnNoaXAYASABKAsyFi52ZWlsaWRjaGF0Lk1lbW' - 'JlcnNoaXBSCm1lbWJlcnNoaXAafQoRQ29udHJvbE1vZGVyYXRpb24SMwoMYWNjZXB0ZWRfaWRz' - 'GAEgAygLMhAudmVpbGlkLlR5cGVkS2V5UgthY2NlcHRlZElkcxIzCgxyZWplY3RlZF9pZHMYAi' - 'ADKAsyEC52ZWlsaWQuVHlwZWRLZXlSC3JlamVjdGVkSWRzQgYKBGtpbmQ='); + 'CgdNZXNzYWdlEg4KAmlkGAEgASgMUgJpZBIoCgZhdXRob3IYAiABKAsyEC52ZWlsaWQuVHlwZW' + 'RLZXlSBmF1dGhvchIcCgl0aW1lc3RhbXAYAyABKARSCXRpbWVzdGFtcBIuCgR0ZXh0GAQgASgL' + 'MhgudmVpbGlkY2hhdC5NZXNzYWdlLlRleHRIAFIEdGV4dBI0CgZzZWNyZXQYBSABKAsyGi52ZW' + 'lsaWRjaGF0Lk1lc3NhZ2UuU2VjcmV0SABSBnNlY3JldBI7CgZkZWxldGUYBiABKAsyIS52ZWls' + 'aWRjaGF0Lk1lc3NhZ2UuQ29udHJvbERlbGV0ZUgAUgZkZWxldGUSOAoFZXJhc2UYByABKAsyIC' + '52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbEVyYXNlSABSBWVyYXNlEkEKCHNldHRpbmdzGAgg' + 'ASgLMiMudmVpbGlkY2hhdC5NZXNzYWdlLkNvbnRyb2xTZXR0aW5nc0gAUghzZXR0aW5ncxJKCg' + 'twZXJtaXNzaW9ucxgJIAEoCzImLnZlaWxpZGNoYXQuTWVzc2FnZS5Db250cm9sUGVybWlzc2lv' + 'bnNIAFILcGVybWlzc2lvbnMSRwoKbWVtYmVyc2hpcBgKIAEoCzIlLnZlaWxpZGNoYXQuTWVzc2' + 'FnZS5Db250cm9sTWVtYmVyc2hpcEgAUgptZW1iZXJzaGlwEkcKCm1vZGVyYXRpb24YCyABKAsy' + 'JS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbE1vZGVyYXRpb25IAFIKbW9kZXJhdGlvbhIvCg' + 'lzaWduYXR1cmUYDCABKAsyES52ZWlsaWQuU2lnbmF0dXJlUglzaWduYXR1cmUa5QEKBFRleHQS' + 'EgoEdGV4dBgBIAEoCVIEdGV4dBIZCgV0b3BpYxgCIAEoCUgAUgV0b3BpY4gBARIeCghyZXBseV' + '9pZBgDIAEoDEgBUgdyZXBseUlkiAEBEh4KCmV4cGlyYXRpb24YBCABKARSCmV4cGlyYXRpb24S' + 'HQoKdmlld19saW1pdBgFIAEoDVIJdmlld0xpbWl0EjgKC2F0dGFjaG1lbnRzGAYgAygLMhYudm' + 'VpbGlkY2hhdC5BdHRhY2htZW50UgthdHRhY2htZW50c0IICgZfdG9waWNCCwoJX3JlcGx5X2lk' + 'GkgKBlNlY3JldBIeCgpjaXBoZXJ0ZXh0GAEgASgMUgpjaXBoZXJ0ZXh0Eh4KCmV4cGlyYXRpb2' + '4YAiABKARSCmV4cGlyYXRpb24aMwoNQ29udHJvbERlbGV0ZRIiCgNpZHMYASADKAsyEC52ZWls' + 'aWQuVHlwZWRLZXlSA2lkcxosCgxDb250cm9sRXJhc2USHAoJdGltZXN0YW1wGAEgASgEUgl0aW' + '1lc3RhbXAaRwoPQ29udHJvbFNldHRpbmdzEjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hh' + 'dC5DaGF0U2V0dGluZ3NSCHNldHRpbmdzGk8KEkNvbnRyb2xQZXJtaXNzaW9ucxI5CgtwZXJtaX' + 'NzaW9ucxgBIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbnNSC3Blcm1pc3Npb25zGksKEUNv' + 'bnRyb2xNZW1iZXJzaGlwEjYKCm1lbWJlcnNoaXAYASABKAsyFi52ZWlsaWRjaGF0Lk1lbWJlcn' + 'NoaXBSCm1lbWJlcnNoaXAafQoRQ29udHJvbE1vZGVyYXRpb24SMwoMYWNjZXB0ZWRfaWRzGAEg' + 'AygLMhAudmVpbGlkLlR5cGVkS2V5UgthY2NlcHRlZElkcxIzCgxyZWplY3RlZF9pZHMYAiADKA' + 'syEC52ZWlsaWQuVHlwZWRLZXlSC3JlamVjdGVkSWRzQgYKBGtpbmQ='); @$core.Deprecated('Use reconciledMessageDescriptor instead') const ReconciledMessage$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index eb6d08a..943f79b 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -123,13 +123,13 @@ message Message { // Text of the message string text = 1; // Topic of the message / Content warning - string topic = 2; + optional string topic = 2; // Message id replied to - veilid.TypedKey reply_id = 3; + optional bytes reply_id = 3; // Message expiration timestamp uint64 expiration = 4; // Message view limit before deletion - uint64 view_limit = 5; + uint32 view_limit = 5; // Attachments on the message repeated Attachment attachments = 6; } @@ -148,9 +148,9 @@ message Message { message ControlDelete { repeated veilid.TypedKey ids = 1; } - // A 'clear' control message + // An 'erase' control message // Deletes a set of messages from before some timestamp - message ControlClear { + message ControlErase { // The latest timestamp to delete messages before // If this is zero then all messages are cleared uint64 timestamp = 1; @@ -181,10 +181,9 @@ message Message { ////////////////////////////////////////////////////////////////////////// - // Hash of previous message from the same author, - // including its previous hash. - // Also serves as a unique key for the message. - veilid.TypedKey id = 1; + // Unique id for this author stream + // Calculated from the hash of the previous message from this author + bytes id = 1; // Author of the message (identity public key) veilid.TypedKey author = 2; // Time the message was sent according to sender @@ -195,7 +194,7 @@ message Message { Text text = 4; Secret secret = 5; ControlDelete delete = 6; - ControlClear clear = 7; + ControlErase erase = 7; ControlSettings settings = 8; ControlPermissions permissions = 9; ControlMembership membership = 10; From e04fd7ee778d627f1b147668d1794d4e317da0c3 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 27 May 2024 20:34:54 -0400 Subject: [PATCH 05/19] table db change tracking --- .../cubits/single_contact_messages_cubit.dart | 1 + lib/chat/models/message_state.dart | 18 +- lib/chat/views/chat_component.dart | 4 +- lib/proto/veilidchat.proto | 2 +- .../lib/src/table_db_array.dart | 46 ++++- .../lib/src/table_db_array_cubit.dart | 185 ++++++++++++++++++ .../veilid_support/lib/veilid_support.dart | 1 + 7 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 packages/veilid_support/lib/src/table_db_array_cubit.dart diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 20c8d22..cf644f9 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -420,6 +420,7 @@ class SingleContactMessagesCubit extends Cubit { void addTextMessage({required proto.Message_Text messageText}) { final message = proto.Message() + ..id = generateNextId() ..author = _activeAccountInfo.localAccount.identityMaster .identityPublicTypedKey() .toProto() diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart index e14c9e8..993ebcb 100644 --- a/lib/chat/models/message_state.dart +++ b/lib/chat/models/message_state.dart @@ -39,19 +39,9 @@ class MessageState with _$MessageState { } extension MessageStateExt on MessageState { - String get uniqueId { - final author = content.author.toVeilid().toString(); - final id = base64UrlNoPadEncode(content.id); - return '$author|$id'; - } - - static (proto.TypedKey, Uint8List) splitUniqueId(String uniqueId) { - final parts = uniqueId.split('|'); - if (parts.length != 2) { - throw Exception('invalid unique id'); - } - final author = TypedKey.fromString(parts[0]).toProto(); - final id = base64UrlNoPadDecode(parts[1]); - return (author, id); + Uint8List get uniqueId { + final author = content.author.toVeilid().decode(); + final id = content.id; + return author..addAll(id); } } diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 2ca49a1..5aa70b7 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -126,7 +126,7 @@ class ChatComponent extends StatelessWidget { final textMessage = types.TextMessage( author: isLocal ? _localUser : _remoteUser, createdAt: (message.timestamp.value ~/ BigInt.from(1000)).toInt(), - id: message.uniqueId, + id: base64UrlNoPadEncode(message.uniqueId), text: contextText.text, showStatus: status != null, status: status); @@ -168,7 +168,7 @@ class ChatComponent extends StatelessWidget { void _handleSendPressed(types.PartialText message) { final text = message.text; final replyId = (message.repliedMessage != null) - ? MessageStateExt.splitUniqueId(message.repliedMessage!.id).$2 + ? base64UrlNoPadDecode(message.repliedMessage!.id) : null; Timestamp? expiration; int? viewLimit; diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 943f79b..81c963c 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -124,7 +124,7 @@ message Message { string text = 1; // Topic of the message / Content warning optional string topic = 2; - // Message id replied to + // Message id replied to (author id + message id) optional bytes reply_id = 3; // Message expiration timestamp uint64 expiration = 4; diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index 51b15b8..9714770 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -4,9 +4,24 @@ import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:charcode/charcode.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; import '../veilid_support.dart'; +@immutable +class TableDBArrayUpdate extends Equatable { + const TableDBArrayUpdate( + {required this.headDelta, required this.tailDelta, required this.length}) + : assert(length >= 0, 'should never have negative length'); + final int headDelta; + final int tailDelta; + final int length; + + @override + List get props => [headDelta, tailDelta, length]; +} + class TableDBArray { TableDBArray({ required String table, @@ -63,8 +78,9 @@ class TableDBArray { await Veilid.instance.deleteTableDB(_table); } - Future> listen(void Function() onChanged) async => - _changeStream.stream.listen((_) => onChanged()); + Future> listen( + void Function(TableDBArrayUpdate) onChanged) async => + _changeStream.stream.listen(onChanged); //////////////////////////////////////////////////////////// // Public interface @@ -160,6 +176,7 @@ class TableDBArray { // Put the entry in the index final pos = _length; _length++; + _tailDelta++; await _setIndexEntry(pos, entry); } @@ -167,6 +184,7 @@ class TableDBArray { VeilidTableDBTransaction t, List values) async { var pos = _length; _length += values.length; + _tailDelta += values.length; for (final value in values) { // Allocate an entry to store the value final entry = await _allocateEntry(); @@ -318,11 +336,18 @@ class TableDBArray { final _oldLength = _length; final _oldNextFree = _nextFree; final _oldMaxEntry = _maxEntry; + final _oldHeadDelta = _headDelta; + final _oldTailDelta = _tailDelta; try { final out = await transactionScope(_tableDB, (t) async { final out = await closure(t); await _saveHead(t); await _flushDirtyChunks(t); + // Send change + _changeStream.add(TableDBArrayUpdate( + headDelta: _headDelta, tailDelta: _tailDelta, length: _length)); + _headDelta = 0; + _tailDelta = 0; return out; }); @@ -332,6 +357,8 @@ class TableDBArray { _length = _oldLength; _nextFree = _oldNextFree; _maxEntry = _oldMaxEntry; + _headDelta = _oldHeadDelta; + _tailDelta = _oldTailDelta; // invalidate caches because they could have been written to _chunkCache.clear(); _dirtyChunks.clear(); @@ -428,6 +455,10 @@ class TableDBArray { // Then add to length _length += length; + if (start == 0) { + _headDelta += length; + } + _tailDelta += length; } Future _removeIndexEntry(int pos) async => _removeIndexEntries(pos, 1); @@ -485,6 +516,10 @@ class TableDBArray { // Then truncate _length -= length; + if (start == 0) { + _headDelta -= length; + } + _tailDelta -= length; } Future _loadIndexChunk(int chunkNumber) async { @@ -578,6 +613,10 @@ class TableDBArray { final WaitSet _initWait = WaitSet(); final Mutex _mutex = Mutex(); + // Change tracking + int _headDelta = 0; + int _tailDelta = 0; + // Head state int _length = 0; int _nextFree = 0; @@ -587,5 +626,6 @@ class TableDBArray { final Map _dirtyChunks = {}; static const int _chunkCacheLength = 3; - final StreamController _changeStream = StreamController.broadcast(); + final StreamController _changeStream = + StreamController.broadcast(); } diff --git a/packages/veilid_support/lib/src/table_db_array_cubit.dart b/packages/veilid_support/lib/src/table_db_array_cubit.dart new file mode 100644 index 0000000..1cebab5 --- /dev/null +++ b/packages/veilid_support/lib/src/table_db_array_cubit.dart @@ -0,0 +1,185 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:bloc/bloc.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; +import 'package:equatable/equatable.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:meta/meta.dart'; + +import '../../../veilid_support.dart'; + +@immutable +class TableDBArrayStateData extends Equatable { + const TableDBArrayStateData( + {required this.elements, + required this.tail, + required this.count, + required this.follow}); + // The view of the elements in the dhtlog + // Span is from [tail-length, tail) + final IList elements; + // One past the end of the last element + final int tail; + // The total number of elements to try to keep in 'elements' + final int count; + // If we should have the tail following the array + final bool follow; + + @override + List get props => [elements, tail, count, follow]; +} + +typedef TableDBArrayState = AsyncValue>; +typedef TableDBArrayBusyState = BlocBusyState>; + +class TableDBArrayCubit extends Cubit> + with BlocBusyWrapper> { + TableDBArrayCubit({ + required Future Function() open, + required T Function(List data) decodeElement, + }) : _decodeElement = decodeElement, + super(const BlocBusyState(AsyncValue.loading())) { + _initWait.add(() async { + // Open table db array + _array = await open(); + _wantsCloseArray = true; + + // Make initial state update + await _refreshNoWait(); + _subscription = await _array.listen(_update); + }); + } + + // Set the tail position of the array for pagination. + // If tail is 0, the end of the array is used. + // If tail is negative, the position is subtracted from the current array + // length. + // If tail is positive, the position is absolute from the head of the array + // If follow is enabled, the tail offset will update when the array changes + Future setWindow( + {int? tail, int? count, bool? follow, bool forceRefresh = false}) async { + await _initWait(); + if (tail != null) { + _tail = tail; + } + if (count != null) { + _count = count; + } + if (follow != null) { + _follow = follow; + } + await _refreshNoWait(forceRefresh: forceRefresh); + } + + Future refresh({bool forceRefresh = false}) async { + await _initWait(); + await _refreshNoWait(forceRefresh: forceRefresh); + } + + Future _refreshNoWait({bool forceRefresh = false}) async => + busy((emit) async => _refreshInner(emit, forceRefresh: forceRefresh)); + + Future _refreshInner( + void Function(AsyncValue>) emit, + {bool forceRefresh = false}) async { + final avElements = await _loadElements(_tail, _count); + final err = avElements.asError; + if (err != null) { + emit(AsyncValue.error(err.error, err.stackTrace)); + return; + } + final loading = avElements.asLoading; + if (loading != null) { + emit(const AsyncValue.loading()); + return; + } + final elements = avElements.asData!.value; + emit(AsyncValue.data(TableDBArrayStateData( + elements: elements, tail: _tail, count: _count, follow: _follow))); + } + + Future>> _loadElements( + int tail, + int count, + ) async { + try { + final length = _array.length; + final end = ((tail - 1) % length) + 1; + final start = (count < end) ? end - count : 0; + final allItems = + (await _array.getRange(start, end)).map(_decodeElement).toIList(); + return AsyncValue.data(allItems); + } on Exception catch (e, st) { + return AsyncValue.error(e, st); + } + } + + void _update(TableDBArrayUpdate upd) { + // Run at most one background update process + // Because this is async, we could get an update while we're + // still processing the last one. Only called after init future has run + // so we dont have to wait for that here. + + // Accumulate head and tail deltas + _headDelta += upd.headDelta; + _tailDelta += upd.tailDelta; + + _sspUpdate.busyUpdate>(busy, (emit) async { + // apply follow + if (_follow) { + if (_tail <= 0) { + // Negative tail is already following tail changes + } else { + // Positive tail is measured from the head, so apply deltas + _tail = (_tail + _tailDelta - _headDelta) % upd.length; + } + } else { + if (_tail <= 0) { + // Negative tail is following tail changes so apply deltas + var posTail = _tail + upd.length; + posTail = (posTail + _tailDelta - _headDelta) % upd.length; + _tail = posTail - upd.length; + } else { + // Positive tail is measured from head so not following tail + } + } + _headDelta = 0; + _tailDelta = 0; + + await _refreshInner(emit); + }); + } + + @override + Future close() async { + await _initWait(); + await _subscription?.cancel(); + _subscription = null; + if (_wantsCloseArray) { + await _array.close(); + } + await super.close(); + } + + Future operate(Future Function(TableDBArray) closure) async { + await _initWait(); + return closure(_array); + } + + final WaitSet _initWait = WaitSet(); + late final TableDBArray _array; + final T Function(List data) _decodeElement; + StreamSubscription? _subscription; + bool _wantsCloseArray = false; + final _sspUpdate = SingleStatelessProcessor(); + + // Accumulated deltas since last update + var _headDelta = 0; + var _tailDelta = 0; + + // Cubit window into the TableDBArray + var _tail = 0; + var _count = DHTShortArray.maxElements; + var _follow = true; +} diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index 1f17da2..42aa839 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -15,5 +15,6 @@ export 'src/persistent_queue.dart'; export 'src/protobuf_tools.dart'; export 'src/table_db.dart'; export 'src/table_db_array.dart'; +export 'src/table_db_array_cubit.dart'; export 'src/veilid_crypto.dart'; export 'src/veilid_log.dart' hide veilidLoggy; From 8a5af51ec7714d27a5480313a6591c292888ed28 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 27 May 2024 22:58:37 -0400 Subject: [PATCH 06/19] crypto work --- .../models/active_account_info.dart | 4 ++-- .../cubits/single_contact_messages_cubit.dart | 16 ++++++-------- .../cubits/contact_invitation_list_cubit.dart | 6 ++--- .../cubits/contact_request_inbox_cubit.dart | 8 +++---- .../home_account_ready_chat.dart | 6 ++--- .../src/dht_record/dht_record_pool.dart | 16 ++++++++++---- packages/veilid_support/lib/src/identity.dart | 11 +++++----- .../veilid_support/lib/src/veilid_crypto.dart | 22 ++++++++++++++----- 8 files changed, 53 insertions(+), 36 deletions(-) diff --git a/lib/account_manager/models/active_account_info.dart b/lib/account_manager/models/active_account_info.dart index 2997434..e4a5beb 100644 --- a/lib/account_manager/models/active_account_info.dart +++ b/lib/account_manager/models/active_account_info.dart @@ -33,8 +33,8 @@ class ActiveAccountInfo { identitySecret.value, utf8.encode('VeilidChat Conversation')); - final messagesCrypto = - await VeilidCryptoPrivate.fromSecret(identitySecret.kind, sharedSecret); + final messagesCrypto = await VeilidCryptoPrivate.fromSharedSecret( + identitySecret.kind, sharedSecret); return messagesCrypto; } diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index cf644f9..dc0e2d4 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -146,15 +146,13 @@ class SingleContactMessagesCubit extends Cubit { // Open reconciled chat record key Future _initReconciledMessagesCubit() async { - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final tableName = _localConversationRecordKey.toString(); - _reconciledMessagesCubit = DHTLogCubit( - open: () async => DHTLog.openOwned(_reconciledChatRecord, - debugName: - 'SingleContactMessagesCubit::_initReconciledMessagesCubit::' - 'ReconciledMessages', - parent: accountRecordKey), + xxx whats the right encryption for reconciled messages cubit? + + final crypto = VeilidCryptoPrivate.fromTypedKey(kind, secretKey); + _reconciledMessagesCubit = TableDBArrayCubit( + open: () async => TableDBArray.make(table: tableName, crypto: crypto), decodeElement: proto.Message.fromBuffer); _reconciledSubscription = _reconciledMessagesCubit!.stream.listen(_updateReconciledMessagesState); @@ -461,7 +459,7 @@ class SingleContactMessagesCubit extends Cubit { DHTLogCubit? _sentMessagesCubit; DHTLogCubit? _rcvdMessagesCubit; - DHTLogCubit? _reconciledMessagesCubit; + TableDBArrayCubit? _reconciledMessagesCubit; late final PersistentQueue _unreconciledMessagesQueue; late final PersistentQueue _sendingMessagesQueue; diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index b76eaee..da1f6e3 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -129,9 +129,9 @@ class ContactInvitationListCubit await contactRequestInbox.eventualWriteBytes(Uint8List(0), subkey: 1, writer: contactRequestWriter, - crypto: await VeilidCryptoPrivate.fromTypedKeyPair( - TypedKeyPair.fromKeyPair( - contactRequestInbox.key.kind, contactRequestWriter))); + crypto: await DHTRecordPool.privateCryptoFromTypedSecret(TypedKey( + kind: contactRequestInbox.key.kind, + value: contactRequestWriter.secret))); // Create ContactInvitation and SignedContactInvitation final cinv = proto.ContactInvitation() diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart index a4d0b8a..214d08b 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -28,16 +28,16 @@ class ContactRequestInboxCubit final pool = DHTRecordPool.instance; final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final writerKey = contactInvitationRecord.writerKey.toVeilid(); final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); final recordKey = contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); - final writer = TypedKeyPair( - kind: recordKey.kind, key: writerKey, secret: writerSecret); + final writerTypedSecret = + TypedKey(kind: recordKey.kind, value: writerSecret); return pool.openRecordRead(recordKey, debugName: 'ContactRequestInboxCubit::_open::' 'ContactRequestInbox', - crypto: await VeilidCryptoPrivate.fromTypedKeyPair(writer), + crypto: + await DHTRecordPool.privateCryptoFromTypedSecret(writerTypedSecret), parent: accountRecordKey, defaultSubkey: 1); } diff --git a/lib/layout/home/home_account_ready/home_account_ready_chat.dart b/lib/layout/home/home_account_ready/home_account_ready_chat.dart index fb0e7b4..087bb34 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_chat.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_chat.dart @@ -28,13 +28,13 @@ class HomeAccountReadyChatState extends State { } Widget buildChatComponent(BuildContext context) { - final activeChatRemoteConversationKey = + final activeChatLocalConversationKey = context.watch().state; - if (activeChatRemoteConversationKey == null) { + if (activeChatLocalConversationKey == null) { return const EmptyChatWidget(); } return ChatComponent.builder( - localConversationRecordKey: activeChatRemoteConversationKey); + localConversationRecordKey: activeChatLocalConversationKey); } @override diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart index 440698a..8b65d41 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart @@ -27,6 +27,9 @@ const int watchRenewalDenominator = 5; // Maximum number of concurrent DHT operations to perform on the network const int maxDHTConcurrency = 8; +// DHT crypto domain +const String cryptoDomainDHT = 'dht'; + typedef DHTRecordPoolLogger = void Function(String message); /// Record pool that managed DHTRecords and allows for tagged deletion @@ -547,9 +550,9 @@ class DHTRecordPool with TableDBBackedJson { writer: writer ?? openedRecordInfo.shared.recordDescriptor.ownerKeyPair(), crypto: crypto ?? - await VeilidCryptoPrivate.fromTypedKeyPair(openedRecordInfo + await privateCryptoFromTypedSecret(openedRecordInfo .shared.recordDescriptor - .ownerTypedKeyPair()!)); + .ownerTypedSecret()!)); openedRecordInfo.records.add(rec); @@ -612,8 +615,8 @@ class DHTRecordPool with TableDBBackedJson { writer: writer, sharedDHTRecordData: openedRecordInfo.shared, crypto: crypto ?? - await VeilidCryptoPrivate.fromTypedKeyPair( - TypedKeyPair.fromKeyPair(recordKey.kind, writer))); + await privateCryptoFromTypedSecret( + TypedKey(kind: recordKey.kind, value: writer.secret))); openedRecordInfo.records.add(rec); @@ -663,6 +666,11 @@ class DHTRecordPool with TableDBBackedJson { } } + /// Generate default VeilidCrypto for a writer + static Future privateCryptoFromTypedSecret( + TypedKey typedSecret) async => + VeilidCryptoPrivate.fromTypedKey(typedSecret, cryptoDomainDHT); + /// Handle the DHT record updates coming from Veilid void processRemoteValueChange(VeilidUpdateValueChange updateValueChange) { if (updateValueChange.subkeys.isNotEmpty) { diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index 400d68b..4666487 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -125,13 +125,14 @@ extension IdentityMasterExtension on IdentityMaster { } Future> readAccountsFromIdentity( - {required SharedSecret identitySecret, - required String accountKey}) async { + {required SecretKey identitySecret, required String accountKey}) async { // Read the identity key to get the account keys final pool = DHTRecordPool.instance; - final identityRecordCrypto = await VeilidCryptoPrivate.fromSecret( - identityRecordKey.kind, identitySecret); + final identityRecordCrypto = + await DHTRecordPool.privateCryptoFromTypedSecret( + TypedKey(kind: identityRecordKey.kind, value: identitySecret), + ); late final List accountRecordInfo; await (await pool.openRecordRead(identityRecordKey, @@ -157,7 +158,7 @@ extension IdentityMasterExtension on IdentityMaster { /// Creates a new Account associated with master identity and store it in the /// identity key. Future addAccountToIdentity({ - required SharedSecret identitySecret, + required SecretKey identitySecret, required String accountKey, required Future Function(TypedKey parent) createAccountCallback, int maxAccounts = 1, diff --git a/packages/veilid_support/lib/src/veilid_crypto.dart b/packages/veilid_support/lib/src/veilid_crypto.dart index 6965089..75087fb 100644 --- a/packages/veilid_support/lib/src/veilid_crypto.dart +++ b/packages/veilid_support/lib/src/veilid_crypto.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:typed_data'; import '../../../veilid_support.dart'; @@ -16,15 +17,24 @@ class VeilidCryptoPrivate implements VeilidCrypto { final VeilidCryptoSystem _cryptoSystem; final SharedSecret _secretKey; - static Future fromTypedKeyPair( - TypedKeyPair typedKeyPair) async { - final cryptoSystem = - await Veilid.instance.getCryptoSystem(typedKeyPair.kind); - final secretKey = typedKeyPair.secret; + static Future fromTypedKey( + TypedKey typedKey, String domain) async { + final cryptoSystem = await Veilid.instance.getCryptoSystem(typedKey.kind); + final keyMaterial = Uint8List(0) + ..addAll(typedKey.value.decode()) + ..addAll(utf8.encode(domain)); + final secretKey = await cryptoSystem.generateHash(keyMaterial); return VeilidCryptoPrivate._(cryptoSystem, secretKey); } - static Future fromSecret( + static Future fromTypedKeyPair( + TypedKeyPair typedKeyPair, String domain) async { + final typedSecret = + TypedKey(kind: typedKeyPair.kind, value: typedKeyPair.secret); + return fromTypedKey(typedSecret, domain); + } + + static Future fromSharedSecret( CryptoKind kind, SharedSecret secretKey) async { final cryptoSystem = await Veilid.instance.getCryptoSystem(kind); return VeilidCryptoPrivate._(cryptoSystem, secretKey); From 37f6ca19f7da911c9e19dc9c34ac284450f0938e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 28 May 2024 22:01:50 -0400 Subject: [PATCH 07/19] checkpoint --- .../cubits/single_contact_messages_cubit.dart | 108 ++++++++++++++---- lib/chat/views/chat_component.dart | 2 +- lib/chat_list/cubits/chat_list_cubit.dart | 9 +- .../cubits/contact_invitation_list_cubit.dart | 6 +- lib/contacts/cubits/contact_list_cubit.dart | 6 +- lib/proto/veilidchat.pb.dart | 48 +++++++- lib/proto/veilidchat.pbjson.dart | 34 +++--- lib/proto/veilidchat.proto | 11 +- .../integration_test/test_dht_log.dart | 20 ++-- .../test_dht_short_array.dart | 18 +-- .../lib/dht_support/proto/dht.proto | 15 ++- .../lib/dht_support/src/dht_log/dht_log.dart | 2 +- .../src/dht_log/dht_log_cubit.dart | 7 +- .../dht_support/src/dht_log/dht_log_read.dart | 12 +- .../src/dht_log/dht_log_spine.dart | 9 +- .../src/dht_log/dht_log_write.dart | 50 +++++++- .../dht_short_array_cubit.dart | 13 +-- .../dht_short_array/dht_short_array_read.dart | 8 +- .../dht_short_array_write.dart | 15 ++- .../{dht_append.dart => dht_add.dart} | 12 +- .../src/interfaces/dht_insert_remove.dart | 23 ++-- .../src/interfaces/dht_random_read.dart | 28 ++--- .../src/interfaces/dht_random_write.dart | 5 + .../src/interfaces/interfaces.dart | 2 +- .../lib/src/table_db_array.dart | 38 +++++- .../veilid_support/lib/src/veilid_crypto.dart | 22 ++-- 26 files changed, 357 insertions(+), 166 deletions(-) rename packages/veilid_support/lib/dht_support/src/interfaces/{dht_append.dart => dht_add.dart} (80%) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index dc0e2d4..7ab3401 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -15,7 +17,6 @@ class RenderStateElement { {required this.message, required this.isLocal, this.reconciled = false, - this.reconciledOffline = false, this.sent = false, this.sentOffline = false}); @@ -27,7 +28,7 @@ class RenderStateElement { if (sent && !sentOffline) { return MessageSendState.delivered; } - if (reconciled && !reconciledOffline) { + if (reconciled) { return MessageSendState.sent; } return MessageSendState.sending; @@ -36,7 +37,6 @@ class RenderStateElement { proto.Message message; bool isLocal; bool reconciled; - bool reconciledOffline; bool sent; bool sentOffline; } @@ -96,7 +96,7 @@ class SingleContactMessagesCubit extends Cubit { ); // Make crypto - await _initMessagesCrypto(); + await _initCrypto(); // Reconciled messages key await _initReconciledMessagesCubit(); @@ -109,9 +109,13 @@ class SingleContactMessagesCubit extends Cubit { } // Make crypto - Future _initMessagesCrypto() async { + Future _initCrypto() async { _messagesCrypto = await _activeAccountInfo .makeConversationCrypto(_remoteIdentityPublicKey); + _localMessagesCryptoSystem = + await Veilid.instance.getCryptoSystem(_localMessagesRecordKey.kind); + _identityCryptoSystem = + await _activeAccountInfo.localAccount.identityMaster.identityCrypto; } // Open local messages key @@ -144,13 +148,16 @@ class SingleContactMessagesCubit extends Cubit { _updateRcvdMessagesState(_rcvdMessagesCubit!.state); } + Future _makeLocalMessagesCrypto() async => + VeilidCryptoPrivate.fromTypedKey( + _activeAccountInfo.userLogin.identitySecret, 'tabledb'); + // Open reconciled chat record key Future _initReconciledMessagesCubit() async { final tableName = _localConversationRecordKey.toString(); - xxx whats the right encryption for reconciled messages cubit? + final crypto = await _makeLocalMessagesCrypto(); - final crypto = VeilidCryptoPrivate.fromTypedKey(kind, secretKey); _reconciledMessagesCubit = TableDBArrayCubit( open: () async => TableDBArray.make(table: tableName, crypto: crypto), decodeElement: proto.Message.fromBuffer); @@ -183,8 +190,8 @@ class SingleContactMessagesCubit extends Cubit { if (sentMessages == null) { return; } - // Don't reconcile, the sending machine will have already added - // to the reconciliation queue on that machine + + await _reconcileMessages(sentMessages, _sentMessagesCubit); // Update the view _renderState(); @@ -197,16 +204,18 @@ class SingleContactMessagesCubit extends Cubit { return; } + await _reconcileMessages(rcvdMessages, _rcvdMessagesCubit); + singleFuture(_rcvdMessagesCubit!, () async { // Get the timestamp of our most recent reconciled message final lastReconciledMessageTs = - await _reconciledMessagesCubit!.operate((r) async { - final len = r.length; + await _reconciledMessagesCubit!.operate((arr) async { + final len = arr.length; if (len == 0) { return null; } else { final lastMessage = - await r.getItemProtobuf(proto.Message.fromBuffer, len - 1); + await arr.getProtobuf(proto.Message.fromBuffer, len - 1); if (lastMessage == null) { throw StateError('should have gotten last message'); } @@ -232,11 +241,9 @@ class SingleContactMessagesCubit extends Cubit { }); } - // Called when the reconciled messages list gets a change - // This can happen when multiple clients for the same identity are - // reading and reconciling the same remote chat + // Called when the reconciled messages window gets a change void _updateReconciledMessagesState( - DHTLogBusyState avmessages) { + TableDBArrayBusyState avmessages) { // Update the view _renderState(); } @@ -252,10 +259,62 @@ class SingleContactMessagesCubit extends Cubit { // }); } + Future _hashSignature(proto.Signature signature) async => + (await _localMessagesCryptoSystem + .generateHash(signature.toVeilid().decode())) + .decode(); + + Future _signMessage(proto.Message message) async { + // Generate data to sign + final data = Uint8List.fromList(utf8.encode(message.writeToJson())); + + // Sign with our identity + final signature = await _identityCryptoSystem.sign( + _activeAccountInfo.localAccount.identityMaster.identityPublicKey, + _activeAccountInfo.userLogin.identitySecret.value, + data); + + // Add to the message + message.signature = signature.toProto(); + } + + Future _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)); + + if (previousMessage == null) { + // If there's no last sent message, + // we start at a hash of the identity public key + message.id = (await _localMessagesCryptoSystem.generateHash( + _activeAccountInfo.localAccount.identityMaster.identityPublicKey + .decode())) + .decode(); + } else { + // If there is a last message, we generate the hash + // of the last message's signature and use it as our next id + message.id = await _hashSignature(previousMessage.signature); + } + + // Now sign it + await _signMessage(message); + } + // Async process to send messages in the background Future _processSendingMessages(IList messages) async { + // Go through and assign ids to all the messages in order + proto.Message? previousMessage; + final processedMessages = messages.toList(); + for (final message in processedMessages) { + await _processMessageToSend(message, previousMessage); + previousMessage = message; + } + await _sentMessagesCubit!.operateAppendEventual((writer) => - writer.tryAddItems(messages.map((m) => m.writeToBuffer()).toList())); + writer.tryAddAll(messages.map((m) => m.writeToBuffer()).toList())); } Future _reconcileMessagesInner( @@ -345,9 +404,8 @@ class SingleContactMessagesCubit extends Cubit { keyMapper: (x) => x.value.timestamp, values: sentMessages.elements, ); - final reconciledMessagesMap = - IMap>.fromValues( - keyMapper: (x) => x.value.timestamp, + final reconciledMessagesMap = IMap.fromValues( + keyMapper: (x) => x.timestamp, values: reconciledMessages.elements, ); final sendingMessagesMap = IMap.fromValues( @@ -416,7 +474,7 @@ class SingleContactMessagesCubit extends Cubit { emit(AsyncValue.data(renderedState)); } - void addTextMessage({required proto.Message_Text messageText}) { + void sendTextMessage({required proto.Message_Text messageText}) { final message = proto.Message() ..id = generateNextId() ..author = _activeAccountInfo.localAccount.identityMaster @@ -425,7 +483,6 @@ class SingleContactMessagesCubit extends Cubit { ..timestamp = Veilid.instance.now().toInt64() ..text = messageText; - _unreconciledMessagesQueue.addSync(message); _sendingMessagesQueue.addSync(message); // Update the view @@ -456,15 +513,18 @@ class SingleContactMessagesCubit extends Cubit { final TypedKey _remoteMessagesRecordKey; late final VeilidCrypto _messagesCrypto; + late final VeilidCryptoSystem _localMessagesCryptoSystem; + late final VeilidCryptoSystem _identityCryptoSystem; DHTLogCubit? _sentMessagesCubit; DHTLogCubit? _rcvdMessagesCubit; TableDBArrayCubit? _reconciledMessagesCubit; - late final PersistentQueue _unreconciledMessagesQueue; + late final PersistentQueue _unreconciledMessagesQueue; xxx can we eliminate this? and make rcvd messages cubit listener work like sent? late final PersistentQueue _sendingMessagesQueue; StreamSubscription>? _sentSubscription; StreamSubscription>? _rcvdSubscription; - StreamSubscription>? _reconciledSubscription; + StreamSubscription>? + _reconciledSubscription; } diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 5aa70b7..06d4312 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -162,7 +162,7 @@ class ChatComponent extends StatelessWidget { ..viewLimit = viewLimit ?? 0; protoMessageText.attachments.addAll(attachments); - _messagesCubit.addTextMessage(messageText: protoMessageText); + _messagesCubit.sendTextMessage(messageText: protoMessageText); } void _handleSendPressed(types.PartialText message) { diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index ff1d5d0..2ab2993 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -65,7 +65,7 @@ class ChatListCubit extends DHTShortArrayCubit await operateWrite((writer) async { // See if we have added this chat already for (var i = 0; i < writer.length; i++) { - final cbuf = await writer.getItem(i); + final cbuf = await writer.get(i); if (cbuf == null) { throw Exception('Failed to get chat'); } @@ -84,7 +84,7 @@ class ChatListCubit extends DHTShortArrayCubit ..remoteConversationRecordKey = remoteConversationRecordKey.toProto(); // Add chat - final added = await writer.tryAddItem(chat.writeToBuffer()); + final added = await writer.tryAdd(chat.writeToBuffer()); if (!added) { throw Exception('Failed to add chat'); } @@ -106,15 +106,14 @@ class ChatListCubit extends DHTShortArrayCubit activeChatCubit.setActiveChat(null); } for (var i = 0; i < writer.length; i++) { - final c = - await writer.getItemProtobuf(proto.Chat.fromBuffer, i); + final c = await writer.getProtobuf(proto.Chat.fromBuffer, i); if (c == null) { throw Exception('Failed to get chat'); } if (c.localConversationRecordKey == localConversationRecordKeyProto) { // Found the right chat - await writer.removeItem(i); + await writer.remove(i); return c; } } diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index da1f6e3..640d416 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -159,7 +159,7 @@ class ContactInvitationListCubit // Add ContactInvitationRecord to account's list // if this fails, don't keep retrying, user can try again later await operateWrite((writer) async { - if (await writer.tryAddItem(cinvrec.writeToBuffer()) == false) { + if (await writer.tryAdd(cinvrec.writeToBuffer()) == false) { throw Exception('Failed to add contact invitation record'); } }); @@ -179,14 +179,14 @@ class ContactInvitationListCubit // Remove ContactInvitationRecord from account's list final deletedItem = await operateWrite((writer) async { for (var i = 0; i < writer.length; i++) { - final item = await writer.getItemProtobuf( + final item = await writer.getProtobuf( proto.ContactInvitationRecord.fromBuffer, i); if (item == null) { throw Exception('Failed to get contact invitation record'); } if (item.contactRequestInbox.recordKey.toVeilid() == contactRequestInboxRecordKey) { - await writer.removeItem(i); + await writer.remove(i); return item; } } diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index 2eb8a08..71669fc 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -56,7 +56,7 @@ class ContactListCubit extends DHTShortArrayCubit { // Add Contact to account's list // if this fails, don't keep retrying, user can try again later await operateWrite((writer) async { - if (!await writer.tryAddItem(contact.writeToBuffer())) { + if (!await writer.tryAdd(contact.writeToBuffer())) { throw Exception('Failed to add contact record'); } }); @@ -72,13 +72,13 @@ class ContactListCubit extends DHTShortArrayCubit { // Remove Contact from account's list final deletedItem = await operateWrite((writer) async { for (var i = 0; i < writer.length; i++) { - final item = await writer.getItemProtobuf(proto.Contact.fromBuffer, i); + final item = await writer.getProtobuf(proto.Contact.fromBuffer, i); if (item == null) { throw Exception('Failed to get contact'); } if (item.localConversationRecordKey == contact.localConversationRecordKey) { - await writer.removeItem(i); + await writer.remove(i); return item; } } diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 3544f00..7770d73 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -486,7 +486,7 @@ class Message_ControlDelete extends $pb.GeneratedMessage { factory Message_ControlDelete.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlDelete', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..pc<$0.TypedKey>(1, _omitFieldNames ? '' : 'ids', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..p<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'ids', $pb.PbFieldType.PY) ..hasRequiredFields = false ; @@ -512,7 +512,7 @@ class Message_ControlDelete extends $pb.GeneratedMessage { static Message_ControlDelete? _defaultInstance; @$pb.TagNumber(1) - $core.List<$0.TypedKey> get ids => $_getList(0); + $core.List<$core.List<$core.int>> get ids => $_getList(0); } class Message_ControlErase extends $pb.GeneratedMessage { @@ -696,8 +696,8 @@ class Message_ControlModeration extends $pb.GeneratedMessage { factory Message_ControlModeration.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlModeration', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..pc<$0.TypedKey>(1, _omitFieldNames ? '' : 'acceptedIds', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) - ..pc<$0.TypedKey>(2, _omitFieldNames ? '' : 'rejectedIds', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..p<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'acceptedIds', $pb.PbFieldType.PY) + ..p<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'rejectedIds', $pb.PbFieldType.PY) ..hasRequiredFields = false ; @@ -723,10 +723,46 @@ class Message_ControlModeration extends $pb.GeneratedMessage { static Message_ControlModeration? _defaultInstance; @$pb.TagNumber(1) - $core.List<$0.TypedKey> get acceptedIds => $_getList(0); + $core.List<$core.List<$core.int>> get acceptedIds => $_getList(0); @$pb.TagNumber(2) - $core.List<$0.TypedKey> get rejectedIds => $_getList(1); + $core.List<$core.List<$core.int>> get rejectedIds => $_getList(1); +} + +class Message_ControlReadReceipt extends $pb.GeneratedMessage { + factory Message_ControlReadReceipt() => create(); + Message_ControlReadReceipt._() : super(); + factory Message_ControlReadReceipt.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlReadReceipt.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlReadReceipt', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..p<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'readIds', $pb.PbFieldType.PY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Message_ControlReadReceipt clone() => Message_ControlReadReceipt()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message_ControlReadReceipt copyWith(void Function(Message_ControlReadReceipt) updates) => super.copyWith((message) => updates(message as Message_ControlReadReceipt)) as Message_ControlReadReceipt; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlReadReceipt create() => Message_ControlReadReceipt._(); + Message_ControlReadReceipt createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlReadReceipt getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlReadReceipt? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.List<$core.int>> get readIds => $_getList(0); } enum Message_Kind { diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index 1470054..56ebbe6 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -172,7 +172,7 @@ const Message$json = { {'1': 'moderation', '3': 11, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlModeration', '9': 0, '10': 'moderation'}, {'1': 'signature', '3': 12, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, ], - '3': [Message_Text$json, Message_Secret$json, Message_ControlDelete$json, Message_ControlErase$json, Message_ControlSettings$json, Message_ControlPermissions$json, Message_ControlMembership$json, Message_ControlModeration$json], + '3': [Message_Text$json, Message_Secret$json, Message_ControlDelete$json, Message_ControlErase$json, Message_ControlSettings$json, Message_ControlPermissions$json, Message_ControlMembership$json, Message_ControlModeration$json, Message_ControlReadReceipt$json], '8': [ {'1': 'kind'}, ], @@ -208,7 +208,7 @@ const Message_Secret$json = { const Message_ControlDelete$json = { '1': 'ControlDelete', '2': [ - {'1': 'ids', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'ids'}, + {'1': 'ids', '3': 1, '4': 3, '5': 12, '10': 'ids'}, ], }; @@ -248,8 +248,16 @@ const Message_ControlMembership$json = { const Message_ControlModeration$json = { '1': 'ControlModeration', '2': [ - {'1': 'accepted_ids', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'acceptedIds'}, - {'1': 'rejected_ids', '3': 2, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'rejectedIds'}, + {'1': 'accepted_ids', '3': 1, '4': 3, '5': 12, '10': 'acceptedIds'}, + {'1': 'rejected_ids', '3': 2, '4': 3, '5': 12, '10': 'rejectedIds'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlReadReceipt$json = { + '1': 'ControlReadReceipt', + '2': [ + {'1': 'read_ids', '3': 1, '4': 3, '5': 12, '10': 'readIds'}, ], }; @@ -272,15 +280,15 @@ final $typed_data.Uint8List messageDescriptor = $convert.base64Decode( 'HQoKdmlld19saW1pdBgFIAEoDVIJdmlld0xpbWl0EjgKC2F0dGFjaG1lbnRzGAYgAygLMhYudm' 'VpbGlkY2hhdC5BdHRhY2htZW50UgthdHRhY2htZW50c0IICgZfdG9waWNCCwoJX3JlcGx5X2lk' 'GkgKBlNlY3JldBIeCgpjaXBoZXJ0ZXh0GAEgASgMUgpjaXBoZXJ0ZXh0Eh4KCmV4cGlyYXRpb2' - '4YAiABKARSCmV4cGlyYXRpb24aMwoNQ29udHJvbERlbGV0ZRIiCgNpZHMYASADKAsyEC52ZWls' - 'aWQuVHlwZWRLZXlSA2lkcxosCgxDb250cm9sRXJhc2USHAoJdGltZXN0YW1wGAEgASgEUgl0aW' - '1lc3RhbXAaRwoPQ29udHJvbFNldHRpbmdzEjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hh' - 'dC5DaGF0U2V0dGluZ3NSCHNldHRpbmdzGk8KEkNvbnRyb2xQZXJtaXNzaW9ucxI5CgtwZXJtaX' - 'NzaW9ucxgBIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbnNSC3Blcm1pc3Npb25zGksKEUNv' - 'bnRyb2xNZW1iZXJzaGlwEjYKCm1lbWJlcnNoaXAYASABKAsyFi52ZWlsaWRjaGF0Lk1lbWJlcn' - 'NoaXBSCm1lbWJlcnNoaXAafQoRQ29udHJvbE1vZGVyYXRpb24SMwoMYWNjZXB0ZWRfaWRzGAEg' - 'AygLMhAudmVpbGlkLlR5cGVkS2V5UgthY2NlcHRlZElkcxIzCgxyZWplY3RlZF9pZHMYAiADKA' - 'syEC52ZWlsaWQuVHlwZWRLZXlSC3JlamVjdGVkSWRzQgYKBGtpbmQ='); + '4YAiABKARSCmV4cGlyYXRpb24aIQoNQ29udHJvbERlbGV0ZRIQCgNpZHMYASADKAxSA2lkcxos' + 'CgxDb250cm9sRXJhc2USHAoJdGltZXN0YW1wGAEgASgEUgl0aW1lc3RhbXAaRwoPQ29udHJvbF' + 'NldHRpbmdzEjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3NSCHNl' + 'dHRpbmdzGk8KEkNvbnRyb2xQZXJtaXNzaW9ucxI5CgtwZXJtaXNzaW9ucxgBIAEoCzIXLnZlaW' + 'xpZGNoYXQuUGVybWlzc2lvbnNSC3Blcm1pc3Npb25zGksKEUNvbnRyb2xNZW1iZXJzaGlwEjYK' + 'Cm1lbWJlcnNoaXAYASABKAsyFi52ZWlsaWRjaGF0Lk1lbWJlcnNoaXBSCm1lbWJlcnNoaXAaWQ' + 'oRQ29udHJvbE1vZGVyYXRpb24SIQoMYWNjZXB0ZWRfaWRzGAEgAygMUgthY2NlcHRlZElkcxIh' + 'CgxyZWplY3RlZF9pZHMYAiADKAxSC3JlamVjdGVkSWRzGi8KEkNvbnRyb2xSZWFkUmVjZWlwdB' + 'IZCghyZWFkX2lkcxgBIAMoDFIHcmVhZElkc0IGCgRraW5k'); @$core.Deprecated('Use reconciledMessageDescriptor instead') const ReconciledMessage$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 81c963c..fa701fb 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -146,7 +146,7 @@ message Message { // A 'delete' control message // Deletes a set of messages by their ids message ControlDelete { - repeated veilid.TypedKey ids = 1; + repeated bytes ids = 1; } // An 'erase' control message // Deletes a set of messages from before some timestamp @@ -175,8 +175,13 @@ message Message { // A 'moderation' control message // Accepts or rejects a set of messages message ControlModeration { - repeated veilid.TypedKey accepted_ids = 1; - repeated veilid.TypedKey rejected_ids = 2; + repeated bytes accepted_ids = 1; + repeated bytes rejected_ids = 2; + } + + // A 'read receipt' control message + message ControlReadReceipt { + repeated bytes read_ids = 1; } ////////////////////////////////////////////////////////////////////////// diff --git a/packages/veilid_support/example/integration_test/test_dht_log.dart b/packages/veilid_support/example/integration_test/test_dht_log.dart index 0c06c87..0ebdd55 100644 --- a/packages/veilid_support/example/integration_test/test_dht_log.dart +++ b/packages/veilid_support/example/integration_test/test_dht_log.dart @@ -64,7 +64,7 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => const chunk = 25; for (var n = 0; n < dataset.length; n += chunk) { print('$n-${n + chunk - 1} '); - final success = await w.tryAddItems(dataset.sublist(n, n + chunk)); + final success = await w.tryAddAll(dataset.sublist(n, n + chunk)); expect(success, isTrue); } }); @@ -73,22 +73,22 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => print('get all\n'); { - final dataset2 = await dlog.operate((r) async => r.getItemRange(0)); + final dataset2 = await dlog.operate((r) async => r.getRange(0)); expect(dataset2, equals(dataset)); } { final dataset3 = - await dlog.operate((r) async => r.getItemRange(64, length: 128)); + await dlog.operate((r) async => r.getRange(64, length: 128)); expect(dataset3, equals(dataset.sublist(64, 64 + 128))); } { final dataset4 = - await dlog.operate((r) async => r.getItemRange(0, length: 1000)); + await dlog.operate((r) async => r.getRange(0, length: 1000)); expect(dataset4, equals(dataset.sublist(0, 1000))); } { final dataset5 = - await dlog.operate((r) async => r.getItemRange(500, length: 499)); + await dlog.operate((r) async => r.getRange(500, length: 499)); expect(dataset5, equals(dataset.sublist(500, 999))); } print('truncate\n'); @@ -96,8 +96,8 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => await dlog.operateAppend((w) async => w.truncate(w.length - 5)); } { - final dataset6 = await dlog - .operate((r) async => r.getItemRange(500 - 5, length: 499)); + final dataset6 = + await dlog.operate((r) async => r.getRange(500 - 5, length: 499)); expect(dataset6, equals(dataset.sublist(500, 999))); } print('truncate 2\n'); @@ -105,8 +105,8 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => await dlog.operateAppend((w) async => w.truncate(w.length - 251)); } { - final dataset7 = await dlog - .operate((r) async => r.getItemRange(500 - 256, length: 499)); + final dataset7 = + await dlog.operate((r) async => r.getRange(500 - 256, length: 499)); expect(dataset7, equals(dataset.sublist(500, 999))); } print('clear\n'); @@ -115,7 +115,7 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => } print('get all\n'); { - final dataset8 = await dlog.operate((r) async => r.getItemRange(0)); + final dataset8 = await dlog.operate((r) async => r.getRange(0)); expect(dataset8, isEmpty); } print('delete and close\n'); diff --git a/packages/veilid_support/example/integration_test/test_dht_short_array.dart b/packages/veilid_support/example/integration_test/test_dht_short_array.dart index 637afe0..244b3d5 100644 --- a/packages/veilid_support/example/integration_test/test_dht_short_array.dart +++ b/packages/veilid_support/example/integration_test/test_dht_short_array.dart @@ -64,7 +64,7 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => final res = await arr.operateWrite((w) async { for (var n = 4; n < 8; n++) { print('$n '); - final success = await w.tryAddItem(dataset[n]); + final success = await w.tryAdd(dataset[n]); expect(success, isTrue); } }); @@ -75,8 +75,8 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => { final res = await arr.operateWrite((w) async { print('${dataset.length ~/ 2}-${dataset.length}'); - final success = await w.tryAddItems( - dataset.sublist(dataset.length ~/ 2, dataset.length)); + final success = await w + .tryAddAll(dataset.sublist(dataset.length ~/ 2, dataset.length)); expect(success, isTrue); }); expect(res, isNull); @@ -87,7 +87,7 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => final res = await arr.operateWrite((w) async { for (var n = 0; n < 4; n++) { print('$n '); - final success = await w.tryInsertItem(n, dataset[n]); + final success = await w.tryInsert(n, dataset[n]); expect(success, isTrue); } }); @@ -98,8 +98,8 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => { final res = await arr.operateWrite((w) async { print('8-${dataset.length ~/ 2}'); - final success = await w.tryInsertItems( - 8, dataset.sublist(8, dataset.length ~/ 2)); + final success = + await w.tryInsertAll(8, dataset.sublist(8, dataset.length ~/ 2)); expect(success, isTrue); }); expect(res, isNull); @@ -107,12 +107,12 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => //print('get all\n'); { - final dataset2 = await arr.operate((r) async => r.getItemRange(0)); + final dataset2 = await arr.operate((r) async => r.getRange(0)); expect(dataset2, equals(dataset)); } { final dataset3 = - await arr.operate((r) async => r.getItemRange(64, length: 128)); + await arr.operate((r) async => r.getRange(64, length: 128)); expect(dataset3, equals(dataset.sublist(64, 64 + 128))); } @@ -126,7 +126,7 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => //print('get all\n'); { - final dataset4 = await arr.operate((r) async => r.getItemRange(0)); + final dataset4 = await arr.operate((r) async => r.getRange(0)); expect(dataset4, isEmpty); } diff --git a/packages/veilid_support/lib/dht_support/proto/dht.proto b/packages/veilid_support/lib/dht_support/proto/dht.proto index 6796753..c27915c 100644 --- a/packages/veilid_support/lib/dht_support/proto/dht.proto +++ b/packages/veilid_support/lib/dht_support/proto/dht.proto @@ -62,13 +62,24 @@ message DHTShortArray { // calculated through iteration } +// Reference to data on the DHT +message DHTDataReference { + veilid.TypedKey dht_data = 1; + veilid.TypedKey hash = 2; +} + +// Reference to data on the BlockStore +message BlockStoreDataReference { + veilid.TypedKey block = 1; +} + // DataReference // Pointer to data somewhere in Veilid // Abstraction over DHTData and BlockStore message DataReference { oneof kind { - veilid.TypedKey dht_data = 1; - // TypedKey block = 2; + DHTDataReference dht_data = 1; + BlockStoreDataReference block_store_data = 2; } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index cba15f4..acdc6fe 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -9,7 +9,7 @@ import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; import '../../proto/proto.dart' as proto; -import '../interfaces/dht_append.dart'; +import '../interfaces/dht_add.dart'; part 'dht_log_spine.dart'; part 'dht_log_read.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index 3c054fc..010c76e 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -92,7 +92,7 @@ class DHTLogCubit extends Cubit> Future _refreshInner(void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { - final avElements = await _loadElements(_tail, _count); + final avElements = await loadElements(_tail, _count); final err = avElements.asError; if (err != null) { emit(AsyncValue.error(err.error, err.stackTrace)); @@ -108,9 +108,10 @@ class DHTLogCubit extends Cubit> elements: elements, tail: _tail, count: _count, follow: _follow))); } - Future>>> _loadElements( + Future>>> loadElements( int tail, int count, {bool forceRefresh = false}) async { + await _initWait(); try { final allItems = await _log.operate((reader) async { final length = reader.length; @@ -118,7 +119,7 @@ class DHTLogCubit extends Cubit> final start = (count < end) ? end - count : 0; final offlinePositions = await reader.getOfflinePositions(); - final allItems = (await reader.getItemRange(start, + final allItems = (await reader.getRange(start, length: end - start, forceRefresh: forceRefresh)) ?.indexed .map((x) => DHTLogElementState( diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart index 0a66a01..7f397ac 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart @@ -12,7 +12,7 @@ class _DHTLogRead implements DHTLogReadOperations { int get length => _spine.length; @override - Future getItem(int pos, {bool forceRefresh = false}) async { + Future get(int pos, {bool forceRefresh = false}) async { if (pos < 0 || pos >= length) { throw IndexError.withLength(pos, length); } @@ -21,8 +21,8 @@ class _DHTLogRead implements DHTLogReadOperations { return null; } - return lookup.scope((sa) => sa.operate( - (read) => read.getItem(lookup.pos, forceRefresh: forceRefresh))); + return lookup.scope((sa) => + sa.operate((read) => read.get(lookup.pos, forceRefresh: forceRefresh))); } (int, int) _clampStartLen(int start, int? len) { @@ -40,14 +40,14 @@ class _DHTLogRead implements DHTLogReadOperations { } @override - Future?> getItemRange(int start, + Future?> getRange(int start, {int? length, bool forceRefresh = false}) async { final out = []; (start, length) = _clampStartLen(start, length); final chunks = Iterable.generate(length).slices(maxDHTConcurrency).map( - (chunk) => chunk - .map((pos) => getItem(pos + start, forceRefresh: forceRefresh))); + (chunk) => + chunk.map((pos) => get(pos + start, forceRefresh: forceRefresh))); for (final chunk in chunks) { final elems = await chunk.wait; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 9a8c64e..a47602a 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -3,16 +3,15 @@ part of 'dht_log.dart'; class _DHTLogPosition extends DHTCloseable<_DHTLogPosition, DHTShortArray> { _DHTLogPosition._({ required _DHTLogSpine dhtLogSpine, - required DHTShortArray shortArray, + required this.shortArray, required this.pos, required int segmentNumber, - }) : _segmentShortArray = shortArray, - _dhtLogSpine = dhtLogSpine, + }) : _dhtLogSpine = dhtLogSpine, _segmentNumber = segmentNumber; final int pos; final _DHTLogSpine _dhtLogSpine; - final DHTShortArray _segmentShortArray; + final DHTShortArray shortArray; var _openCount = 1; final int _segmentNumber; final Mutex _mutex = Mutex(); @@ -23,7 +22,7 @@ class _DHTLogPosition extends DHTCloseable<_DHTLogPosition, DHTShortArray> { /// The type of the openable scope @override - FutureOr scoped() => _segmentShortArray; + FutureOr scoped() => shortArray; /// Add a reference to this log @override diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index 5503051..49acce0 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -17,7 +17,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } final lookup = await _spine.lookupPosition(pos); if (lookup == null) { - throw StateError("can't write to dht log"); + throw StateError("can't lookup position in write to dht log"); } // Write item to the segment @@ -26,7 +26,47 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } @override - Future tryAddItem(Uint8List value) async { + Future swap(int aPos, int bPos) async { + if (aPos < 0 || aPos >= _spine.length) { + throw IndexError.withLength(aPos, _spine.length); + } + if (bPos < 0 || bPos >= _spine.length) { + throw IndexError.withLength(bPos, _spine.length); + } + final aLookup = await _spine.lookupPosition(aPos); + if (aLookup == null) { + throw StateError("can't lookup position a in swap of dht log"); + } + final bLookup = await _spine.lookupPosition(bPos); + if (bLookup == null) { + throw StateError("can't lookup position b in swap of dht log"); + } + + // Swap items in the segments + if (aLookup.shortArray == bLookup.shortArray) { + await aLookup.scope((sa) => sa.operateWriteEventual((aWrite) async { + await aWrite.swap(aLookup.pos, bLookup.pos); + return true; + })); + } else { + final bItem = Output(); + await aLookup.scope( + (sa) => bLookup.scope((sb) => sa.operateWriteEventual((aWrite) async { + if (bItem.value == null) { + final aItem = await aWrite.get(aLookup.pos); + if (aItem == null) { + throw StateError("can't get item for position a in swap"); + } + await sb.operateWriteEventual((bWrite) async => + bWrite.tryWriteItem(bLookup.pos, aItem, output: bItem)); + } + return aWrite.tryWriteItem(aLookup.pos, bItem.value!); + }))); + } + } + + @override + Future tryAdd(Uint8List value) async { // Allocate empty index at the end of the list final insertPos = _spine.length; _spine.allocateTail(1); @@ -44,12 +84,12 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { // We should always be appending at the length throw StateError('appending should be at the end'); } - return write.tryAddItem(value); + return write.tryAdd(value); })); } @override - Future tryAddItems(List values) async { + Future tryAddAll(List values) async { // Allocate empty index at the end of the list final insertPos = _spine.length; _spine.allocateTail(values.length); @@ -79,7 +119,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { // We should always be appending at the length throw StateError('appending should be at the end'); } - return write.tryAddItems(sublistValues); + return write.tryAddAll(sublistValues); })); if (!ok) { success = false; diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart index f4b806e..90fcbad 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart @@ -54,13 +54,12 @@ class DHTShortArrayCubit extends Cubit> try { final newState = await _shortArray.operate((reader) async { final offlinePositions = await reader.getOfflinePositions(); - final allItems = - (await reader.getItemRange(0, forceRefresh: forceRefresh)) - ?.indexed - .map((x) => DHTShortArrayElementState( - value: _decodeElement(x.$2), - isOffline: offlinePositions.contains(x.$1))) - .toIList(); + final allItems = (await reader.getRange(0, forceRefresh: forceRefresh)) + ?.indexed + .map((x) => DHTShortArrayElementState( + value: _decodeElement(x.$2), + isOffline: offlinePositions.contains(x.$1))) + .toIList(); return allItems; }); if (newState != null) { diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_read.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_read.dart index 5da8cf8..abe7198 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_read.dart @@ -12,7 +12,7 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations { int get length => _head.length; @override - Future getItem(int pos, {bool forceRefresh = false}) async { + Future get(int pos, {bool forceRefresh = false}) async { if (pos < 0 || pos >= length) { throw IndexError.withLength(pos, length); } @@ -49,14 +49,14 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations { } @override - Future?> getItemRange(int start, + Future?> getRange(int start, {int? length, bool forceRefresh = false}) async { final out = []; (start, length) = _clampStartLen(start, length); final chunks = Iterable.generate(length).slices(maxDHTConcurrency).map( - (chunk) => chunk - .map((pos) => getItem(pos + start, forceRefresh: forceRefresh))); + (chunk) => + chunk.map((pos) => get(pos + start, forceRefresh: forceRefresh))); for (final chunk in chunks) { final elems = await chunk.wait; diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_write.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_write.dart index c336e47..d002e35 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_write.dart @@ -16,15 +16,14 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead _DHTShortArrayWrite._(super.head) : super._(); @override - Future tryAddItem(Uint8List value) => - tryInsertItem(_head.length, value); + Future tryAdd(Uint8List value) => tryInsert(_head.length, value); @override - Future tryAddItems(List values) => - tryInsertItems(_head.length, values); + Future tryAddAll(List values) => + tryInsertAll(_head.length, values); @override - Future tryInsertItem(int pos, Uint8List value) async { + Future tryInsert(int pos, Uint8List value) async { if (pos < 0 || pos > _head.length) { throw IndexError.withLength(pos, _head.length); } @@ -44,7 +43,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } @override - Future tryInsertItems(int pos, List values) async { + Future tryInsertAll(int pos, List values) async { if (pos < 0 || pos > _head.length) { throw IndexError.withLength(pos, _head.length); } @@ -100,7 +99,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } @override - Future swapItem(int aPos, int bPos) async { + Future swap(int aPos, int bPos) async { if (aPos < 0 || aPos >= _head.length) { throw IndexError.withLength(aPos, _head.length); } @@ -112,7 +111,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } @override - Future removeItem(int pos, {Output? output}) async { + Future remove(int pos, {Output? output}) async { if (pos < 0 || pos >= _head.length) { throw IndexError.withLength(pos, _head.length); } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart similarity index 80% rename from packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart rename to packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart index a1f47ee..e2b5ad7 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart @@ -12,30 +12,30 @@ abstract class DHTAdd { /// changed before the element could be added or a newer value was found on /// the network. /// Throws a StateError if the container exceeds its maximum size. - Future tryAddItem(Uint8List value); + Future tryAdd(Uint8List value); /// Try to add a list of items to the DHT container. /// Return true if the elements were successfully added, and false if the /// state changed before the element could be added or a newer value was found /// on the network. /// Throws a StateError if the container exceeds its maximum size. - Future tryAddItems(List values); + Future tryAddAll(List values); } extension DHTAddExt on DHTAdd { /// Convenience function: /// Like tryAddItem but also encodes the input value as JSON and parses the /// returned element as JSON - Future tryAppendItemJson( + Future tryAddJson( T newValue, ) => - tryAddItem(jsonEncodeBytes(newValue)); + tryAdd(jsonEncodeBytes(newValue)); /// Convenience function: /// Like tryAddItem but also encodes the input value as a protobuf object /// and parses the returned element as a protobuf object - Future tryAddItemProtobuf( + Future tryAddProtobuf( T newValue, ) => - tryAddItem(newValue.writeToBuffer()); + tryAdd(newValue.writeToBuffer()); } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart index 1f98a22..fe44368 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart @@ -14,7 +14,7 @@ abstract class DHTInsertRemove { /// Throws an IndexError if the position removed exceeds the length of /// the container. /// Throws a StateError if the container exceeds its maximum size. - Future tryInsertItem(int pos, Uint8List value); + Future tryInsert(int pos, Uint8List value); /// Try to insert items at position 'pos' of the DHT container. /// Return true if the elements were successfully inserted, and false if the @@ -23,38 +23,33 @@ abstract class DHTInsertRemove { /// Throws an IndexError if the position removed exceeds the length of /// the container. /// Throws a StateError if the container exceeds its maximum size. - Future tryInsertItems(int pos, List values); - - /// Swap items at position 'aPos' and 'bPos' in the DHTArray. - /// Throws an IndexError if either of the positions swapped exceeds the length - /// of the container - Future swapItem(int aPos, int bPos); + Future tryInsertAll(int pos, List values); /// Remove an item at position 'pos' in the DHT container. /// If the remove was successful this returns: /// * outValue will return the prior contents of the element /// Throws an IndexError if the position removed exceeds the length of /// the container. - Future removeItem(int pos, {Output? output}); + Future remove(int pos, {Output? output}); } extension DHTInsertRemoveExt on DHTInsertRemove { /// Convenience function: - /// Like removeItem but also parses the returned element as JSON - Future removeItemJson(T Function(dynamic) fromJson, int pos, + /// Like remove but also parses the returned element as JSON + Future removeJson(T Function(dynamic) fromJson, int pos, {Output? output}) async { final outValueBytes = output == null ? null : Output(); - await removeItem(pos, output: outValueBytes); + await remove(pos, output: outValueBytes); output.mapSave(outValueBytes, (b) => jsonDecodeBytes(fromJson, b)); } /// Convenience function: - /// Like removeItem but also parses the returned element as JSON - Future removeItemProtobuf( + /// Like remove but also parses the returned element as JSON + Future removeProtobuf( T Function(List) fromBuffer, int pos, {Output? output}) async { final outValueBytes = output == null ? null : Output(); - await removeItem(pos, output: outValueBytes); + await remove(pos, output: outValueBytes); output.mapSave(outValueBytes, fromBuffer); } } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart index 39d49e6..362d688 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart @@ -15,14 +15,14 @@ abstract class DHTRandomRead { /// rather than returning the existing locally stored copy of the elements. /// Throws an IndexError if the 'pos' is not within the length /// of the container. - Future getItem(int pos, {bool forceRefresh = false}); + Future get(int pos, {bool forceRefresh = false}); /// Return a list of a range of items in the DHTArray. If 'forceRefresh' /// is specified, the network will always be checked for newer values /// rather than returning the existing locally stored copy of the elements. /// Throws an IndexError if either 'start' or '(start+length)' is not within /// the length of the container. - Future?> getItemRange(int start, + Future?> getRange(int start, {int? length, bool forceRefresh = false}); /// Get a list of the positions that were written offline and not flushed yet @@ -31,32 +31,32 @@ abstract class DHTRandomRead { extension DHTRandomReadExt on DHTRandomRead { /// Convenience function: - /// Like getItem but also parses the returned element as JSON - Future getItemJson(T Function(dynamic) fromJson, int pos, + /// Like get but also parses the returned element as JSON + Future getJson(T Function(dynamic) fromJson, int pos, {bool forceRefresh = false}) => - getItem(pos, forceRefresh: forceRefresh) + get(pos, forceRefresh: forceRefresh) .then((out) => jsonDecodeOptBytes(fromJson, out)); /// Convenience function: - /// Like getAllItems but also parses the returned elements as JSON - Future?> getItemRangeJson(T Function(dynamic) fromJson, int start, + /// Like getRange but also parses the returned elements as JSON + Future?> getRangeJson(T Function(dynamic) fromJson, int start, {int? length, bool forceRefresh = false}) => - getItemRange(start, length: length, forceRefresh: forceRefresh) + getRange(start, length: length, forceRefresh: forceRefresh) .then((out) => out?.map(fromJson).toList()); /// Convenience function: - /// Like getItem but also parses the returned element as a protobuf object - Future getItemProtobuf( + /// Like get but also parses the returned element as a protobuf object + Future getProtobuf( T Function(List) fromBuffer, int pos, {bool forceRefresh = false}) => - getItem(pos, forceRefresh: forceRefresh) + get(pos, forceRefresh: forceRefresh) .then((out) => (out == null) ? null : fromBuffer(out)); /// Convenience function: - /// Like getAllItems but also parses the returned elements as protobuf objects - Future?> getItemRangeProtobuf( + /// Like getRange but also parses the returned elements as protobuf objects + Future?> getRangeProtobuf( T Function(List) fromBuffer, int start, {int? length, bool forceRefresh = false}) => - getItemRange(start, length: length, forceRefresh: forceRefresh) + getRange(start, length: length, forceRefresh: forceRefresh) .then((out) => out?.map(fromBuffer).toList()); } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart index 0d8f3ac..5b3f032 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart @@ -23,6 +23,11 @@ abstract class DHTRandomWrite { /// of the container. Future tryWriteItem(int pos, Uint8List newValue, {Output? output}); + + /// Swap items at position 'aPos' and 'bPos' in the DHTArray. + /// Throws an IndexError if either of the positions swapped exceeds the length + /// of the container + Future swap(int aPos, int bPos); } extension DHTRandomWriteExt on DHTRandomWrite { diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart index dd95cac..57d0979 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart @@ -1,4 +1,4 @@ -export 'dht_append.dart'; +export 'dht_add.dart'; export 'dht_clear.dart'; export 'dht_closeable.dart'; export 'dht_insert_remove.dart'; diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index 9714770..4d5b9dd 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -6,6 +6,7 @@ import 'package:async_tools/async_tools.dart'; import 'package:charcode/charcode.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; +import 'package:protobuf/protobuf.dart'; import '../veilid_support.dart'; @@ -128,13 +129,13 @@ class TableDBArray { }); } - Future> getRange(int start, int end) async { + Future> getRange(int start, [int? end]) async { await _initWait(); return _mutex.protect(() async { if (!_open) { throw StateError('not open'); } - return _getRangeInner(start, end); + return _getRangeInner(start, end ?? _length); }); } @@ -629,3 +630,36 @@ class TableDBArray { final StreamController _changeStream = StreamController.broadcast(); } + +extension TableDBArrayExt on TableDBArray { + /// Convenience function: + /// Like get but also parses the returned element as JSON + Future getJson( + T Function(dynamic) fromJson, + int pos, + ) => + get( + pos, + ).then((out) => jsonDecodeOptBytes(fromJson, out)); + + /// Convenience function: + /// Like getRange but also parses the returned elements as JSON + Future?> getRangeJson(T Function(dynamic) fromJson, int start, + [int? end]) => + getRange(start, end ?? _length).then((out) => out.map(fromJson).toList()); + + /// Convenience function: + /// Like get but also parses the returned element as a protobuf object + Future getProtobuf( + T Function(List) fromBuffer, + int pos, + ) => + get(pos).then(fromBuffer); + + /// Convenience function: + /// Like getRange but also parses the returned elements as protobuf objects + Future?> getRangeProtobuf( + T Function(List) fromBuffer, int start, [int? end]) => + getRange(start, end ?? _length) + .then((out) => out.map(fromBuffer).toList()); +} diff --git a/packages/veilid_support/lib/src/veilid_crypto.dart b/packages/veilid_support/lib/src/veilid_crypto.dart index 75087fb..565459c 100644 --- a/packages/veilid_support/lib/src/veilid_crypto.dart +++ b/packages/veilid_support/lib/src/veilid_crypto.dart @@ -13,16 +13,16 @@ abstract class VeilidCrypto { class VeilidCryptoPrivate implements VeilidCrypto { VeilidCryptoPrivate._(VeilidCryptoSystem cryptoSystem, SharedSecret secretKey) : _cryptoSystem = cryptoSystem, - _secretKey = secretKey; + _secret = secretKey; final VeilidCryptoSystem _cryptoSystem; - final SharedSecret _secretKey; + final SharedSecret _secret; static Future fromTypedKey( - TypedKey typedKey, String domain) async { - final cryptoSystem = await Veilid.instance.getCryptoSystem(typedKey.kind); - final keyMaterial = Uint8List(0) - ..addAll(typedKey.value.decode()) - ..addAll(utf8.encode(domain)); + TypedKey typedSecret, String domain) async { + final cryptoSystem = + await Veilid.instance.getCryptoSystem(typedSecret.kind); + final keyMaterial = Uint8List.fromList( + [...typedSecret.value.decode(), ...utf8.encode(domain)]); final secretKey = await cryptoSystem.generateHash(keyMaterial); return VeilidCryptoPrivate._(cryptoSystem, secretKey); } @@ -35,18 +35,18 @@ class VeilidCryptoPrivate implements VeilidCrypto { } static Future fromSharedSecret( - CryptoKind kind, SharedSecret secretKey) async { + CryptoKind kind, SharedSecret sharedSecret) async { final cryptoSystem = await Veilid.instance.getCryptoSystem(kind); - return VeilidCryptoPrivate._(cryptoSystem, secretKey); + return VeilidCryptoPrivate._(cryptoSystem, sharedSecret); } @override Future encrypt(Uint8List data) => - _cryptoSystem.encryptNoAuthWithNonce(data, _secretKey); + _cryptoSystem.encryptNoAuthWithNonce(data, _secret); @override Future decrypt(Uint8List data) => - _cryptoSystem.decryptNoAuthWithNonce(data, _secretKey); + _cryptoSystem.decryptNoAuthWithNonce(data, _secret); } //////////////////////////////////// From 6d05c9f1258cce185450a2f90ab140fd5ddbf32c Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 29 May 2024 10:47:43 -0400 Subject: [PATCH 08/19] everything but reconcile --- .../cubits/single_contact_messages_cubit.dart | 180 ++++++++---------- lib/chat/models/message_state.dart | 14 +- lib/chat/models/message_state.freezed.dart | 71 ++++--- lib/chat/models/message_state.g.dart | 8 +- lib/chat/views/chat_component.dart | 9 +- lib/proto/extensions.dart | 14 ++ packages/veilid_support/lib/proto/dht.pb.dart | 124 +++++++++++- .../veilid_support/lib/proto/dht.pbjson.dart | 35 +++- 8 files changed, 305 insertions(+), 150 deletions(-) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 7ab3401..6310af6 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -16,7 +16,7 @@ class RenderStateElement { RenderStateElement( {required this.message, required this.isLocal, - this.reconciled = false, + this.reconciledTimestamp, this.sent = false, this.sentOffline = false}); @@ -28,7 +28,7 @@ class RenderStateElement { if (sent && !sentOffline) { return MessageSendState.delivered; } - if (reconciled) { + if (reconciledTimestamp != null) { return MessageSendState.sent; } return MessageSendState.sending; @@ -36,7 +36,7 @@ class RenderStateElement { proto.Message message; bool isLocal; - bool reconciled; + Timestamp? reconciledTimestamp; bool sent; bool sentOffline; } @@ -68,7 +68,6 @@ class SingleContactMessagesCubit extends Cubit { Future close() async { await _initWait(); - await _unreconciledMessagesQueue.close(); await _sendingMessagesQueue.close(); await _sentSubscription?.cancel(); await _rcvdSubscription?.cancel(); @@ -81,13 +80,6 @@ class SingleContactMessagesCubit extends Cubit { // Initialize everything Future _init() async { - // Late initialization of queues with closures - _unreconciledMessagesQueue = PersistentQueue( - table: 'SingleContactUnreconciledMessages', - key: _remoteConversationRecordKey.toString(), - fromBuffer: proto.Message.fromBuffer, - closure: _processUnreconciledMessages, - ); _sendingMessagesQueue = PersistentQueue( table: 'SingleContactSendingMessages', key: _remoteConversationRecordKey.toString(), @@ -160,13 +152,14 @@ class SingleContactMessagesCubit extends Cubit { _reconciledMessagesCubit = TableDBArrayCubit( open: () async => TableDBArray.make(table: tableName, crypto: crypto), - decodeElement: proto.Message.fromBuffer); + decodeElement: proto.ReconciledMessage.fromBuffer); _reconciledSubscription = _reconciledMessagesCubit!.stream.listen(_updateReconciledMessagesState); _updateReconciledMessagesState(_reconciledMessagesCubit!.state); } //////////////////////////////////////////////////////////////////////////// + // Public interface // Set the tail position of the log for pagination. // If tail is 0, the end of the log is used. @@ -181,7 +174,14 @@ class SingleContactMessagesCubit extends Cubit { tail: tail, count: count, follow: follow, forceRefresh: forceRefresh); } + // 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); + } + //////////////////////////////////////////////////////////////////////////// + // Internal implementation // Called when the sent messages cubit gets a change // This will re-render when messages are sent from another machine @@ -191,10 +191,7 @@ class SingleContactMessagesCubit extends Cubit { return; } - await _reconcileMessages(sentMessages, _sentMessagesCubit); - - // Update the view - _renderState(); + _reconcileMessages(sentMessages, _sentMessagesCubit!); } // Called when the received messages cubit gets a change @@ -204,61 +201,16 @@ class SingleContactMessagesCubit extends Cubit { return; } - await _reconcileMessages(rcvdMessages, _rcvdMessagesCubit); - - singleFuture(_rcvdMessagesCubit!, () async { - // Get the timestamp of our most recent reconciled message - final lastReconciledMessageTs = - await _reconciledMessagesCubit!.operate((arr) async { - final len = arr.length; - if (len == 0) { - return null; - } else { - final lastMessage = - await arr.getProtobuf(proto.Message.fromBuffer, len - 1); - if (lastMessage == null) { - throw StateError('should have gotten last message'); - } - return lastMessage.timestamp; - } - }); - - // Find oldest message we have not yet reconciled - - // // Go through all the ones from the cubit state first since we've already - // // gotten them from the DHT - // for (var rn = rcvdMessages.elements.length; rn >= 0; rn--) { - // // - // } - - // // Add remote messages updates to queue to process asynchronously - // // Ignore offline state because remote messages are always fully delivered - // // This may happen once per client but should be idempotent - // _unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value)); - - // Update the view - _renderState(); - }); + _reconcileMessages(rcvdMessages, _rcvdMessagesCubit!); } // Called when the reconciled messages window gets a change void _updateReconciledMessagesState( - TableDBArrayBusyState avmessages) { + TableDBArrayBusyState avmessages) { // Update the view _renderState(); } - // Async process to reconcile messages sent or received in the background - Future _processUnreconciledMessages( - IList messages) async { - // await _reconciledMessagesCubit! - // .operateAppendEventual((reconciledMessagesWriter) async { - // await _reconcileMessagesInner( - // reconciledMessagesWriter: reconciledMessagesWriter, - // messages: messages); - // }); - } - Future _hashSignature(proto.Signature signature) async => (await _localMessagesCryptoSystem .generateHash(signature.toVeilid().decode())) @@ -317,6 +269,43 @@ class SingleContactMessagesCubit extends Cubit { writer.tryAddAll(messages.map((m) => m.writeToBuffer()).toList())); } + void _reconcileMessages(DHTLogStateData inputMessages, + DHTLogCubit inputMessagesCubit) { + singleFuture(_reconciledMessagesCubit!, () async { + // Get the timestamp of our most recent reconciled message + final lastReconciledMessageTs = + await _reconciledMessagesCubit!.operate((arr) async { + final len = arr.length; + if (len == 0) { + return null; + } else { + final lastMessage = + await arr.getProtobuf(proto.Message.fromBuffer, len - 1); + if (lastMessage == null) { + throw StateError('should have gotten last message'); + } + return lastMessage.timestamp; + } + }); + + // Find oldest message we have not yet reconciled + + // // Go through all the ones from the cubit state first since we've already + // // gotten them from the DHT + // for (var rn = rcvdMessages.elements.length; rn >= 0; rn--) { + // // + // } + + // // Add remote messages updates to queue to process asynchronously + // // Ignore offline state because remote messages are always fully delivered + // // This may happen once per client but should be idempotent + // _unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value)); + + // Update the view + _renderState(); + }); + } + Future _reconcileMessagesInner( {required DHTLogWriteOperations reconciledMessagesWriter, required IList messages}) async { @@ -380,15 +369,11 @@ class SingleContactMessagesCubit extends Cubit { // Produce a state for this cubit from the input cubits and queues void _renderState() { - // xxx move into a singlefuture - // Get all reconciled messages final reconciledMessages = _reconciledMessagesCubit?.state.state.asData?.value; // Get all sent messages final sentMessages = _sentMessagesCubit?.state.state.asData?.value; - // Get all items in the unreconciled queue - final unreconciledMessages = _unreconciledMessagesQueue.queue; // Get all items in the unsent queue final sendingMessages = _sendingMessagesQueue.queue; @@ -400,31 +385,30 @@ class SingleContactMessagesCubit extends Cubit { // Generate state for each message final sentMessagesMap = - IMap>.fromValues( - keyMapper: (x) => x.value.timestamp, + IMap>.fromValues( + keyMapper: (x) => x.value.uniqueIdString, values: sentMessages.elements, ); - final reconciledMessagesMap = IMap.fromValues( - keyMapper: (x) => x.timestamp, + final reconciledMessagesMap = + IMap.fromValues( + keyMapper: (x) => x.content.uniqueIdString, values: reconciledMessages.elements, ); - final sendingMessagesMap = IMap.fromValues( - keyMapper: (x) => x.timestamp, + final sendingMessagesMap = IMap.fromValues( + keyMapper: (x) => x.uniqueIdString, values: sendingMessages, ); - final unreconciledMessagesMap = IMap.fromValues( - keyMapper: (x) => x.timestamp, - values: unreconciledMessages, - ); - final renderedElements = {}; + final renderedElements = {}; for (final m in reconciledMessagesMap.entries) { renderedElements[m.key] = RenderStateElement( - message: m.value.value, - isLocal: m.value.value.author.toVeilid() != _remoteIdentityPublicKey, - reconciled: true, - reconciledOffline: m.value.isOffline); + message: m.value.content, + isLocal: m.value.content.author.toVeilid() == + _activeAccountInfo.localAccount.identityMaster + .identityPublicTypedKey(), + reconciledTimestamp: Timestamp.fromInt64(m.value.reconciledTime), + ); } for (final m in sentMessagesMap.entries) { renderedElements.putIfAbsent( @@ -436,17 +420,6 @@ class SingleContactMessagesCubit extends Cubit { ..sent = true ..sentOffline = m.value.isOffline; } - for (final m in unreconciledMessagesMap.entries) { - renderedElements - .putIfAbsent( - m.key, - () => RenderStateElement( - message: m.value, - isLocal: - m.value.author.toVeilid() != _remoteIdentityPublicKey, - )) - .reconciled = false; - } for (final m in sendingMessagesMap.entries) { renderedElements .putIfAbsent( @@ -465,24 +438,25 @@ class SingleContactMessagesCubit extends Cubit { final renderedState = messageKeys .map((x) => MessageState( content: x.value.message, - timestamp: Timestamp.fromInt64(x.key), + sentTimestamp: Timestamp.fromInt64(x.value.message.timestamp), + reconciledTimestamp: x.value.reconciledTimestamp, sendState: x.value.sendState)) .toIList(); // Emit the rendered state - emit(AsyncValue.data(renderedState)); } - void sendTextMessage({required proto.Message_Text messageText}) { - final message = proto.Message() - ..id = generateNextId() + void _sendMessage({required proto.Message message}) { + // Add common fields + // id and signature will get set by _processMessageToSend + message ..author = _activeAccountInfo.localAccount.identityMaster .identityPublicTypedKey() .toProto() - ..timestamp = Veilid.instance.now().toInt64() - ..text = messageText; + ..timestamp = Veilid.instance.now().toInt64(); + // Put in the queue _sendingMessagesQueue.addSync(message); // Update the view @@ -490,6 +464,7 @@ class SingleContactMessagesCubit extends Cubit { } ///////////////////////////////////////////////////////////////////////// + // Static utility functions static Future cleanupAndDeleteMessages( {required TypedKey localConversationRecordKey}) async { @@ -518,13 +493,12 @@ class SingleContactMessagesCubit extends Cubit { DHTLogCubit? _sentMessagesCubit; DHTLogCubit? _rcvdMessagesCubit; - TableDBArrayCubit? _reconciledMessagesCubit; + TableDBArrayCubit? _reconciledMessagesCubit; - late final PersistentQueue _unreconciledMessagesQueue; xxx can we eliminate this? and make rcvd messages cubit listener work like sent? late final PersistentQueue _sendingMessagesQueue; StreamSubscription>? _sentSubscription; StreamSubscription>? _rcvdSubscription; - StreamSubscription>? + StreamSubscription>? _reconciledSubscription; } diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart index 993ebcb..f9952fa 100644 --- a/lib/chat/models/message_state.dart +++ b/lib/chat/models/message_state.dart @@ -28,8 +28,10 @@ class MessageState with _$MessageState { // Content of the message @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) required proto.Message content, - // Received or delivered timestamp - required Timestamp timestamp, + // Sent timestamp + required Timestamp sentTimestamp, + // Reconciled timestamp + required Timestamp? reconciledTimestamp, // The state of the message required MessageSendState? sendState, }) = _MessageState; @@ -37,11 +39,3 @@ class MessageState with _$MessageState { factory MessageState.fromJson(dynamic json) => _$MessageStateFromJson(json as Map); } - -extension MessageStateExt on MessageState { - Uint8List get uniqueId { - final author = content.author.toVeilid().decode(); - final id = content.id; - return author..addAll(id); - } -} diff --git a/lib/chat/models/message_state.freezed.dart b/lib/chat/models/message_state.freezed.dart index b411f4c..a99f937 100644 --- a/lib/chat/models/message_state.freezed.dart +++ b/lib/chat/models/message_state.freezed.dart @@ -23,8 +23,10 @@ mixin _$MessageState { // Content of the message @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) proto.Message get content => - throw _privateConstructorUsedError; // Received or delivered timestamp - Timestamp get timestamp => + throw _privateConstructorUsedError; // Sent timestamp + Timestamp get sentTimestamp => + throw _privateConstructorUsedError; // Reconciled timestamp + Timestamp? get reconciledTimestamp => throw _privateConstructorUsedError; // The state of the message MessageSendState? get sendState => throw _privateConstructorUsedError; @@ -43,7 +45,8 @@ abstract class $MessageStateCopyWith<$Res> { $Res call( {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) proto.Message content, - Timestamp timestamp, + Timestamp sentTimestamp, + Timestamp? reconciledTimestamp, MessageSendState? sendState}); } @@ -61,7 +64,8 @@ class _$MessageStateCopyWithImpl<$Res, $Val extends MessageState> @override $Res call({ Object? content = null, - Object? timestamp = null, + Object? sentTimestamp = null, + Object? reconciledTimestamp = freezed, Object? sendState = freezed, }) { return _then(_value.copyWith( @@ -69,10 +73,14 @@ class _$MessageStateCopyWithImpl<$Res, $Val extends MessageState> ? _value.content : content // ignore: cast_nullable_to_non_nullable as proto.Message, - timestamp: null == timestamp - ? _value.timestamp - : timestamp // ignore: cast_nullable_to_non_nullable + sentTimestamp: null == sentTimestamp + ? _value.sentTimestamp + : sentTimestamp // ignore: cast_nullable_to_non_nullable as Timestamp, + reconciledTimestamp: freezed == reconciledTimestamp + ? _value.reconciledTimestamp + : reconciledTimestamp // ignore: cast_nullable_to_non_nullable + as Timestamp?, sendState: freezed == sendState ? _value.sendState : sendState // ignore: cast_nullable_to_non_nullable @@ -92,7 +100,8 @@ abstract class _$$MessageStateImplCopyWith<$Res> $Res call( {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) proto.Message content, - Timestamp timestamp, + Timestamp sentTimestamp, + Timestamp? reconciledTimestamp, MessageSendState? sendState}); } @@ -108,7 +117,8 @@ class __$$MessageStateImplCopyWithImpl<$Res> @override $Res call({ Object? content = null, - Object? timestamp = null, + Object? sentTimestamp = null, + Object? reconciledTimestamp = freezed, Object? sendState = freezed, }) { return _then(_$MessageStateImpl( @@ -116,10 +126,14 @@ class __$$MessageStateImplCopyWithImpl<$Res> ? _value.content : content // ignore: cast_nullable_to_non_nullable as proto.Message, - timestamp: null == timestamp - ? _value.timestamp - : timestamp // ignore: cast_nullable_to_non_nullable + sentTimestamp: null == sentTimestamp + ? _value.sentTimestamp + : sentTimestamp // ignore: cast_nullable_to_non_nullable as Timestamp, + reconciledTimestamp: freezed == reconciledTimestamp + ? _value.reconciledTimestamp + : reconciledTimestamp // ignore: cast_nullable_to_non_nullable + as Timestamp?, sendState: freezed == sendState ? _value.sendState : sendState // ignore: cast_nullable_to_non_nullable @@ -134,7 +148,8 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { const _$MessageStateImpl( {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) required this.content, - required this.timestamp, + required this.sentTimestamp, + required this.reconciledTimestamp, required this.sendState}); factory _$MessageStateImpl.fromJson(Map json) => @@ -144,16 +159,19 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { @override @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) final proto.Message content; -// Received or delivered timestamp +// Sent timestamp @override - final Timestamp timestamp; + final Timestamp sentTimestamp; +// Reconciled timestamp + @override + final Timestamp? reconciledTimestamp; // The state of the message @override final MessageSendState? sendState; @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'MessageState(content: $content, timestamp: $timestamp, sendState: $sendState)'; + return 'MessageState(content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)'; } @override @@ -162,7 +180,8 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { properties ..add(DiagnosticsProperty('type', 'MessageState')) ..add(DiagnosticsProperty('content', content)) - ..add(DiagnosticsProperty('timestamp', timestamp)) + ..add(DiagnosticsProperty('sentTimestamp', sentTimestamp)) + ..add(DiagnosticsProperty('reconciledTimestamp', reconciledTimestamp)) ..add(DiagnosticsProperty('sendState', sendState)); } @@ -172,15 +191,18 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { (other.runtimeType == runtimeType && other is _$MessageStateImpl && (identical(other.content, content) || other.content == content) && - (identical(other.timestamp, timestamp) || - other.timestamp == timestamp) && + (identical(other.sentTimestamp, sentTimestamp) || + other.sentTimestamp == sentTimestamp) && + (identical(other.reconciledTimestamp, reconciledTimestamp) || + other.reconciledTimestamp == reconciledTimestamp) && (identical(other.sendState, sendState) || other.sendState == sendState)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, content, timestamp, sendState); + int get hashCode => Object.hash( + runtimeType, content, sentTimestamp, reconciledTimestamp, sendState); @JsonKey(ignore: true) @override @@ -200,7 +222,8 @@ abstract class _MessageState implements MessageState { const factory _MessageState( {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) required final proto.Message content, - required final Timestamp timestamp, + required final Timestamp sentTimestamp, + required final Timestamp? reconciledTimestamp, required final MessageSendState? sendState}) = _$MessageStateImpl; factory _MessageState.fromJson(Map json) = @@ -209,8 +232,10 @@ abstract class _MessageState implements MessageState { @override // Content of the message @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) proto.Message get content; - @override // Received or delivered timestamp - Timestamp get timestamp; + @override // Sent timestamp + Timestamp get sentTimestamp; + @override // Reconciled timestamp + Timestamp? get reconciledTimestamp; @override // The state of the message MessageSendState? get sendState; @override diff --git a/lib/chat/models/message_state.g.dart b/lib/chat/models/message_state.g.dart index 3471720..99899a7 100644 --- a/lib/chat/models/message_state.g.dart +++ b/lib/chat/models/message_state.g.dart @@ -9,7 +9,10 @@ part of 'message_state.dart'; _$MessageStateImpl _$$MessageStateImplFromJson(Map json) => _$MessageStateImpl( content: messageFromJson(json['content'] as Map), - timestamp: Timestamp.fromJson(json['timestamp']), + sentTimestamp: Timestamp.fromJson(json['sent_timestamp']), + reconciledTimestamp: json['reconciled_timestamp'] == null + ? null + : Timestamp.fromJson(json['reconciled_timestamp']), sendState: json['send_state'] == null ? null : MessageSendState.fromJson(json['send_state']), @@ -18,6 +21,7 @@ _$MessageStateImpl _$$MessageStateImplFromJson(Map json) => Map _$$MessageStateImplToJson(_$MessageStateImpl instance) => { 'content': messageToJson(instance.content), - 'timestamp': instance.timestamp.toJson(), + 'sent_timestamp': instance.sentTimestamp.toJson(), + 'reconciled_timestamp': instance.reconciledTimestamp?.toJson(), 'send_state': instance.sendState?.toJson(), }; diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 06d4312..7f549cb 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -104,7 +104,7 @@ class ChatComponent extends StatelessWidget { ///////////////////////////////////////////////////////////////////// - types.Message? messageToChatMessage(MessageState message) { + types.Message? messageStateToChatMessage(MessageState message) { final isLocal = message.content.author.toVeilid() == _localUserIdentityKey; types.Status? status; @@ -125,8 +125,9 @@ class ChatComponent extends StatelessWidget { final contextText = message.content.text; final textMessage = types.TextMessage( author: isLocal ? _localUser : _remoteUser, - createdAt: (message.timestamp.value ~/ BigInt.from(1000)).toInt(), - id: base64UrlNoPadEncode(message.uniqueId), + createdAt: + (message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(), + id: message.content.uniqueIdString, text: contextText.text, showStatus: status != null, status: status); @@ -219,7 +220,7 @@ class ChatComponent extends StatelessWidget { final chatMessages = []; final tsSet = {}; for (final message in messages) { - final chatMessage = messageToChatMessage(message); + final chatMessage = messageStateToChatMessage(message); if (chatMessage == null) { continue; } diff --git a/lib/proto/extensions.dart b/lib/proto/extensions.dart index 64fabf6..e9fd9a2 100644 --- a/lib/proto/extensions.dart +++ b/lib/proto/extensions.dart @@ -1,3 +1,7 @@ +import 'dart:typed_data'; + +import 'package:veilid_support/veilid_support.dart'; + import 'proto.dart' as proto; proto.Message messageFromJson(Map j) => @@ -10,3 +14,13 @@ proto.ReconciledMessage reconciledMessageFromJson(Map j) => Map reconciledMessageToJson(proto.ReconciledMessage m) => m.writeToJsonMap(); + +extension MessageExt on proto.Message { + Uint8List get uniqueIdBytes { + final author = this.author.toVeilid().decode(); + final id = this.id; + return Uint8List.fromList([...author, ...id]); + } + + String get uniqueIdString => base64UrlNoPadEncode(uniqueIdBytes); +} diff --git a/packages/veilid_support/lib/proto/dht.pb.dart b/packages/veilid_support/lib/proto/dht.pb.dart index 4007d3d..814bd22 100644 --- a/packages/veilid_support/lib/proto/dht.pb.dart +++ b/packages/veilid_support/lib/proto/dht.pb.dart @@ -195,8 +195,109 @@ class DHTShortArray extends $pb.GeneratedMessage { $core.List<$core.int> get seqs => $_getList(2); } +class DHTDataReference extends $pb.GeneratedMessage { + factory DHTDataReference() => create(); + DHTDataReference._() : super(); + factory DHTDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DHTDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DHTDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create) + ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'hash', subBuilder: $0.TypedKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DHTDataReference clone() => DHTDataReference()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DHTDataReference copyWith(void Function(DHTDataReference) updates) => super.copyWith((message) => updates(message as DHTDataReference)) as DHTDataReference; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DHTDataReference create() => DHTDataReference._(); + DHTDataReference createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DHTDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DHTDataReference? _defaultInstance; + + @$pb.TagNumber(1) + $0.TypedKey get dhtData => $_getN(0); + @$pb.TagNumber(1) + set dhtData($0.TypedKey v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasDhtData() => $_has(0); + @$pb.TagNumber(1) + void clearDhtData() => clearField(1); + @$pb.TagNumber(1) + $0.TypedKey ensureDhtData() => $_ensure(0); + + @$pb.TagNumber(2) + $0.TypedKey get hash => $_getN(1); + @$pb.TagNumber(2) + set hash($0.TypedKey v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasHash() => $_has(1); + @$pb.TagNumber(2) + void clearHash() => clearField(2); + @$pb.TagNumber(2) + $0.TypedKey ensureHash() => $_ensure(1); +} + +class BlockStoreDataReference extends $pb.GeneratedMessage { + factory BlockStoreDataReference() => create(); + BlockStoreDataReference._() : super(); + factory BlockStoreDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BlockStoreDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BlockStoreDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create) + ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'block', subBuilder: $0.TypedKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BlockStoreDataReference clone() => BlockStoreDataReference()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BlockStoreDataReference copyWith(void Function(BlockStoreDataReference) updates) => super.copyWith((message) => updates(message as BlockStoreDataReference)) as BlockStoreDataReference; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BlockStoreDataReference create() => BlockStoreDataReference._(); + BlockStoreDataReference createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BlockStoreDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BlockStoreDataReference? _defaultInstance; + + @$pb.TagNumber(1) + $0.TypedKey get block => $_getN(0); + @$pb.TagNumber(1) + set block($0.TypedKey v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasBlock() => $_has(0); + @$pb.TagNumber(1) + void clearBlock() => clearField(1); + @$pb.TagNumber(1) + $0.TypedKey ensureBlock() => $_ensure(0); +} + enum DataReference_Kind { dhtData, + blockStoreData, notSet } @@ -208,11 +309,13 @@ class DataReference extends $pb.GeneratedMessage { static const $core.Map<$core.int, DataReference_Kind> _DataReference_KindByTag = { 1 : DataReference_Kind.dhtData, + 2 : DataReference_Kind.blockStoreData, 0 : DataReference_Kind.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create) - ..oo(0, [1]) - ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: $0.TypedKey.create) + ..oo(0, [1, 2]) + ..aOM(1, _omitFieldNames ? '' : 'dhtData', subBuilder: DHTDataReference.create) + ..aOM(2, _omitFieldNames ? '' : 'blockStoreData', subBuilder: BlockStoreDataReference.create) ..hasRequiredFields = false ; @@ -241,15 +344,26 @@ class DataReference extends $pb.GeneratedMessage { void clearKind() => clearField($_whichOneof(0)); @$pb.TagNumber(1) - $0.TypedKey get dhtData => $_getN(0); + DHTDataReference get dhtData => $_getN(0); @$pb.TagNumber(1) - set dhtData($0.TypedKey v) { setField(1, v); } + set dhtData(DHTDataReference v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasDhtData() => $_has(0); @$pb.TagNumber(1) void clearDhtData() => clearField(1); @$pb.TagNumber(1) - $0.TypedKey ensureDhtData() => $_ensure(0); + DHTDataReference ensureDhtData() => $_ensure(0); + + @$pb.TagNumber(2) + BlockStoreDataReference get blockStoreData => $_getN(1); + @$pb.TagNumber(2) + set blockStoreData(BlockStoreDataReference v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasBlockStoreData() => $_has(1); + @$pb.TagNumber(2) + void clearBlockStoreData() => clearField(2); + @$pb.TagNumber(2) + BlockStoreDataReference ensureBlockStoreData() => $_ensure(1); } class OwnedDHTRecordPointer extends $pb.GeneratedMessage { diff --git a/packages/veilid_support/lib/proto/dht.pbjson.dart b/packages/veilid_support/lib/proto/dht.pbjson.dart index 6c99cb7..b8575b9 100644 --- a/packages/veilid_support/lib/proto/dht.pbjson.dart +++ b/packages/veilid_support/lib/proto/dht.pbjson.dart @@ -60,11 +60,39 @@ final $typed_data.Uint8List dHTShortArrayDescriptor = $convert.base64Decode( 'Cg1ESFRTaG9ydEFycmF5EiQKBGtleXMYASADKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGtleXMSFA' 'oFaW5kZXgYAiABKAxSBWluZGV4EhIKBHNlcXMYAyADKA1SBHNlcXM='); +@$core.Deprecated('Use dHTDataReferenceDescriptor instead') +const DHTDataReference$json = { + '1': 'DHTDataReference', + '2': [ + {'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'dhtData'}, + {'1': 'hash', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'hash'}, + ], +}; + +/// Descriptor for `DHTDataReference`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List dHTDataReferenceDescriptor = $convert.base64Decode( + 'ChBESFREYXRhUmVmZXJlbmNlEisKCGRodF9kYXRhGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5Ug' + 'dkaHREYXRhEiQKBGhhc2gYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGhhc2g='); + +@$core.Deprecated('Use blockStoreDataReferenceDescriptor instead') +const BlockStoreDataReference$json = { + '1': 'BlockStoreDataReference', + '2': [ + {'1': 'block', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'block'}, + ], +}; + +/// Descriptor for `BlockStoreDataReference`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List blockStoreDataReferenceDescriptor = $convert.base64Decode( + 'ChdCbG9ja1N0b3JlRGF0YVJlZmVyZW5jZRImCgVibG9jaxgBIAEoCzIQLnZlaWxpZC5UeXBlZE' + 'tleVIFYmxvY2s='); + @$core.Deprecated('Use dataReferenceDescriptor instead') const DataReference$json = { '1': 'DataReference', '2': [ - {'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '9': 0, '10': 'dhtData'}, + {'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.dht.DHTDataReference', '9': 0, '10': 'dhtData'}, + {'1': 'block_store_data', '3': 2, '4': 1, '5': 11, '6': '.dht.BlockStoreDataReference', '9': 0, '10': 'blockStoreData'}, ], '8': [ {'1': 'kind'}, @@ -73,8 +101,9 @@ const DataReference$json = { /// Descriptor for `DataReference`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List dataReferenceDescriptor = $convert.base64Decode( - 'Cg1EYXRhUmVmZXJlbmNlEi0KCGRodF9kYXRhGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5SABSB2' - 'RodERhdGFCBgoEa2luZA=='); + 'Cg1EYXRhUmVmZXJlbmNlEjIKCGRodF9kYXRhGAEgASgLMhUuZGh0LkRIVERhdGFSZWZlcmVuY2' + 'VIAFIHZGh0RGF0YRJIChBibG9ja19zdG9yZV9kYXRhGAIgASgLMhwuZGh0LkJsb2NrU3RvcmVE' + 'YXRhUmVmZXJlbmNlSABSDmJsb2NrU3RvcmVEYXRhQgYKBGtpbmQ='); @$core.Deprecated('Use ownedDHTRecordPointerDescriptor instead') const OwnedDHTRecordPointer$json = { From c9525bde77b115c5a6646d0c22515e4adabf2af6 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 29 May 2024 16:09:09 -0400 Subject: [PATCH 09/19] more reconciliation --- .../cubits/single_contact_messages_cubit.dart | 139 +++++++++++++++--- 1 file changed, 115 insertions(+), 24 deletions(-) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 6310af6..ff21d43 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -1,17 +1,29 @@ import 'dart:async'; +import 'dart:collection'; import 'dart:convert'; import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; +import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; import '../models/models.dart'; +@immutable +class MessagePosition extends Equatable { + const MessagePosition(this.message, this.pos); + final proto.Message message; + final int pos; + @override + List get props => [message, pos]; +} + class RenderStateElement { RenderStateElement( {required this.message, @@ -191,7 +203,10 @@ class SingleContactMessagesCubit extends Cubit { return; } - _reconcileMessages(sentMessages, _sentMessagesCubit!); + _reconcileMessages( + _activeAccountInfo.localAccount.identityMaster.identityPublicTypedKey(), + sentMessages, + _sentMessagesCubit!); } // Called when the received messages cubit gets a change @@ -201,7 +216,8 @@ class SingleContactMessagesCubit extends Cubit { return; } - _reconcileMessages(rcvdMessages, _rcvdMessagesCubit!); + _reconcileMessages( + _remoteIdentityPublicKey, rcvdMessages, _rcvdMessagesCubit!); } // Called when the reconciled messages window gets a change @@ -269,37 +285,108 @@ class SingleContactMessagesCubit extends Cubit { writer.tryAddAll(messages.map((m) => m.writeToBuffer()).toList())); } - void _reconcileMessages(DHTLogStateData inputMessages, + void _reconcileMessages( + TypedKey author, + DHTLogStateData inputMessages, DHTLogCubit inputMessagesCubit) { singleFuture(_reconciledMessagesCubit!, () async { - // Get the timestamp of our most recent reconciled message - final lastReconciledMessageTs = + // Get the position of our most recent + // reconciled message from this author + // XXX: For a group chat, this should find when the author + // was added to the membership so we don't just go back in time forever + final lastReconciledMessage = await _reconciledMessagesCubit!.operate((arr) async { - final len = arr.length; - if (len == 0) { - return null; - } else { - final lastMessage = - await arr.getProtobuf(proto.Message.fromBuffer, len - 1); - if (lastMessage == null) { + var pos = arr.length - 1; + while (pos >= 0) { + final message = await arr.getProtobuf(proto.Message.fromBuffer, pos); + if (message == null) { throw StateError('should have gotten last message'); } - return lastMessage.timestamp; + if (message.author.toVeilid() == author) { + return MessagePosition(message, pos); + } + pos--; } + return null; }); // Find oldest message we have not yet reconciled + final toReconcile = ListQueue(); - // // Go through all the ones from the cubit state first since we've already - // // gotten them from the DHT - // for (var rn = rcvdMessages.elements.length; rn >= 0; rn--) { - // // - // } + // Go through batches of the input dhtlog starting with + // the current cubit state which is at the tail of the log + // Find the last reconciled message for this author + var currentInputPos = inputMessages.tail; + var currentInputElements = inputMessages.elements; + final inputBatchCount = inputMessages.count; + outer: + while (true) { + for (var rn = currentInputElements.length; + rn >= 0 && currentInputPos >= 0; + rn--, currentInputPos--) { + final elem = currentInputElements[rn]; - // // Add remote messages updates to queue to process asynchronously - // // Ignore offline state because remote messages are always fully delivered - // // This may happen once per client but should be idempotent - // _unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value)); + // If we've found an input element that is older than our last + // reconciled message for this author, then we stop + if (lastReconciledMessage != null) { + if (elem.value.timestamp < + lastReconciledMessage.message.timestamp) { + break outer; + } + } + + // Drop the 'offline' elements because we don't reconcile + // anything until it has been confirmed to be committed to the DHT + if (elem.isOffline) { + continue; + } + + // Add to head of reconciliation queue + toReconcile.addFirst(elem.value); + if (toReconcile.length > _maxReconcileChunk) { + toReconcile.removeLast(); + } + } + if (currentInputPos < 0) { + break; + } + + // Get another input batch futher back + final nextInputBatch = await inputMessagesCubit.loadElements( + currentInputPos, inputBatchCount); + final asErr = nextInputBatch.asError; + if (asErr != null) { + emit(AsyncValue.error(asErr.error, asErr.stackTrace)); + return; + } + final asLoading = nextInputBatch.asLoading; + if (asLoading != null) { + // xxx: no need to block the cubit here for this + // xxx: might want to switch to a 'busy' state though + // xxx: to let the messages view show a spinner at the bottom + // xxx: while we reconcile... + // emit(const AsyncValue.loading()); + return; + } + currentInputElements = nextInputBatch.asData!.value; + } + + // Now iterate from our current input position in batches + // and reconcile the messages in the forward direction + var insertPosition = + (lastReconciledMessage != null) ? lastReconciledMessage.pos : 0; + var lastInsertTime = (lastReconciledMessage != null) + ? lastReconciledMessage.message.timestamp + : Int64.ZERO; + + // Insert this batch + xxx expand upon 'res' and iterate batches and update insert position/time + final res = await _reconciledMessagesCubit!.operate((arr) async => + _reconcileMessagesInner( + reconciledArray: arr, + toReconcile: toReconcile, + insertPosition: insertPosition, + lastInsertTime: lastInsertTime)); // Update the view _renderState(); @@ -307,8 +394,10 @@ class SingleContactMessagesCubit extends Cubit { } Future _reconcileMessagesInner( - {required DHTLogWriteOperations reconciledMessagesWriter, - required IList messages}) async { + {required TableDBArray reconciledArray, + required Iterable toReconcile, + required int insertPosition, + required Int64 lastInsertTime}) async { // // Ensure remoteMessages is sorted by timestamp // final newMessages = messages // .sort((a, b) => a.timestamp.compareTo(b.timestamp)) @@ -501,4 +590,6 @@ class SingleContactMessagesCubit extends Cubit { StreamSubscription>? _rcvdSubscription; StreamSubscription>? _reconciledSubscription; + + static const int _maxReconcileChunk = 65536; } From 490051a650ddf08e8f2a3d783572660e013b3a50 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 30 May 2024 23:25:47 -0400 Subject: [PATCH 10/19] reconciliation work --- .../reconciliation/author_input_queue.dart | 145 ++++++++++++ .../reconciliation/author_input_source.dart | 10 + .../message_reconciliation.dart | 206 +++++++++++++++++ .../reconciliation/output_position.dart | 13 ++ .../cubits/reconciliation/reconciliation.dart | 1 + .../cubits/single_contact_messages_cubit.dart | 213 ++---------------- lib/proto/extensions.dart | 3 + .../lib/dht_support/src/dht_log/dht_log.dart | 3 +- .../src/dht_log/dht_log_cubit.dart | 4 +- .../lib/src/table_db_array.dart | 42 ++++ pubspec.lock | 9 + pubspec.yaml | 4 + 12 files changed, 457 insertions(+), 196 deletions(-) create mode 100644 lib/chat/cubits/reconciliation/author_input_queue.dart create mode 100644 lib/chat/cubits/reconciliation/author_input_source.dart create mode 100644 lib/chat/cubits/reconciliation/message_reconciliation.dart create mode 100644 lib/chat/cubits/reconciliation/output_position.dart create mode 100644 lib/chat/cubits/reconciliation/reconciliation.dart diff --git a/lib/chat/cubits/reconciliation/author_input_queue.dart b/lib/chat/cubits/reconciliation/author_input_queue.dart new file mode 100644 index 0000000..b441e75 --- /dev/null +++ b/lib/chat/cubits/reconciliation/author_input_queue.dart @@ -0,0 +1,145 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:math'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../../proto/proto.dart' as proto; + +import 'author_input_source.dart'; +import 'output_position.dart'; + +class AuthorInputQueue { + AuthorInputQueue({ + required this.author, + required this.inputSource, + required this.lastOutputPosition, + required this.onError, + }): + assert(inputSource.messages.count>0, 'no input source window length'), + assert(inputSource.messages.elements.isNotEmpty, 'no input source elements'), + assert(inputSource.messages.tail >= inputSource.messages.elements.length, 'tail is before initial messages end'), + assert(inputSource.messages.tail > 0, 'tail is not greater than zero'), + currentPosition = inputSource.messages.tail, + currentWindow = inputSource.messages.elements, + windowLength = inputSource.messages.count, + windowFirst = inputSource.messages.tail - inputSource.messages.elements.length, + windowLast = inputSource.messages.tail - 1; + + //////////////////////////////////////////////////////////////////////////// + + bool get isEmpty => toReconcile.isEmpty; + + proto.Message? get current => toReconcile.firstOrNull; + + bool consume() { + toReconcile.removeFirst(); + return toReconcile.isNotEmpty; + } + + Future prepareInputQueue() async { + // Go through batches of the input dhtlog starting with + // the current cubit state which is at the tail of the log + // Find the last reconciled message for this author + + outer: + while (true) { + for (var rn = currentWindow.length; + rn >= 0 && currentPosition >= 0; + rn--, currentPosition--) { + final elem = currentWindow[rn]; + + // If we've found an input element that is older than our last + // reconciled message for this author, then we stop + if (lastOutputPosition != null) { + if (elem.value.timestamp < lastOutputPosition!.message.timestamp) { + break outer; + } + } + + // Drop the 'offline' elements because we don't reconcile + // anything until it has been confirmed to be committed to the DHT + if (elem.isOffline) { + continue; + } + + // Add to head of reconciliation queue + toReconcile.addFirst(elem.value); + if (toReconcile.length > _maxQueueChunk) { + toReconcile.removeLast(); + } + } + if (currentPosition < 0) { + break; + } + + xxx update window here and make this and other methods work + } + return true; + } + + // Slide the window toward the current position and load the batch around it + Future updateWindow() async { + + // Check if we are still in the window + if (currentPosition>=windowFirst && currentPosition <= windowLast) { + return true; + } + + // Get the length of the cubit + final inputLength = await inputSource.cubit.operate((r) async => r.length); + + // If not, slide the window + if (currentPosition toReconcile = ListQueue(); + final AuthorInputSource inputSource; + final OutputPosition? lastOutputPosition; + final void Function(Object, StackTrace?) onError; + + // The current position in the input log that we are looking at + int currentPosition; + // The current input window elements + IList> currentWindow; + // The first position of the sliding input window + int windowFirst; + // The last position of the sliding input window + int windowLast; + // Desired maximum window length + int windowLength; + + static const int _maxQueueChunk = 256; +} diff --git a/lib/chat/cubits/reconciliation/author_input_source.dart b/lib/chat/cubits/reconciliation/author_input_source.dart new file mode 100644 index 0000000..75f020e --- /dev/null +++ b/lib/chat/cubits/reconciliation/author_input_source.dart @@ -0,0 +1,10 @@ +import 'package:veilid_support/veilid_support.dart'; + +import '../../../proto/proto.dart' as proto; + +class AuthorInputSource { + AuthorInputSource({required this.messages, required this.cubit}); + + final DHTLogStateData messages; + final DHTLogCubit cubit; +} diff --git a/lib/chat/cubits/reconciliation/message_reconciliation.dart b/lib/chat/cubits/reconciliation/message_reconciliation.dart new file mode 100644 index 0000000..51cfe2b --- /dev/null +++ b/lib/chat/cubits/reconciliation/message_reconciliation.dart @@ -0,0 +1,206 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:sorted_list/sorted_list.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../../proto/proto.dart' as proto; +import 'author_input_queue.dart'; +import 'author_input_source.dart'; +import 'output_position.dart'; + +class MessageReconciliation { + MessageReconciliation( + {required TableDBArrayCubit output, + required void Function(Object, StackTrace?) onError}) + : _outputCubit = output, + _onError = onError; + + //////////////////////////////////////////////////////////////////////////// + + void reconcileMessages( + TypedKey author, + DHTLogStateData inputMessages, + DHTLogCubit inputMessagesCubit) { + if (inputMessages.elements.isEmpty) { + return; + } + + _inputSources[author] = + AuthorInputSource(messages: inputMessages, cubit: inputMessagesCubit); + + singleFuture(this, onError: _onError, () async { + // Take entire list of input sources we have currently and process them + final inputSources = _inputSources; + _inputSources = {}; + + final inputFuts = >[]; + for (final kv in inputSources.entries) { + final author = kv.key; + final inputSource = kv.value; + inputFuts + .add(_enqueueAuthorInput(author: author, inputSource: inputSource)); + } + final inputQueues = await inputFuts.wait; + + // Make this safe to cast by removing inputs that were rejected or empty + inputQueues.removeNulls(); + + // Process all input queues together + await _outputCubit + .operate((reconciledArray) async => _reconcileInputQueues( + reconciledArray: reconciledArray, + inputQueues: inputQueues.cast(), + )); + }); + } + + //////////////////////////////////////////////////////////////////////////// + + // Set up a single author's message reconciliation + Future _enqueueAuthorInput( + {required TypedKey author, + required AuthorInputSource inputSource}) async { + // Get the position of our most recent reconciled message from this author + final lastReconciledMessage = + await _findNewestReconciledMessage(author: author); + + // Find oldest message we have not yet reconciled + final inputQueue = await _buildAuthorInputQueue( + author: author, + inputSource: inputSource, + lastOutputPosition: lastReconciledMessage); + return inputQueue; + } + + // Get the position of our most recent + // reconciled message from this author + // XXX: For a group chat, this should find when the author + // was added to the membership so we don't just go back in time forever + Future _findNewestReconciledMessage( + {required TypedKey author}) async => + _outputCubit.operate((arr) async { + var pos = arr.length - 1; + while (pos >= 0) { + final message = await arr.getProtobuf(proto.Message.fromBuffer, pos); + if (message == null) { + throw StateError('should have gotten last message'); + } + if (message.author.toVeilid() == author) { + return OutputPosition(message, pos); + } + pos--; + } + return null; + }); + + // Find oldest message we have not yet reconciled and build a queue forward + // from that position + Future _buildAuthorInputQueue( + {required TypedKey author, + required AuthorInputSource inputSource, + required OutputPosition? lastOutputPosition}) async { + // Make an author input queue + final authorInputQueue = AuthorInputQueue( + author: author, + inputSource: inputSource, + lastOutputPosition: lastOutputPosition, + onError: _onError); + + if (!await authorInputQueue.prepareInputQueue()) { + return null; + } + + return authorInputQueue; + } + + // Process a list of author input queues and insert their messages + // into the output array, performing validation steps along the way + Future _reconcileInputQueues({ + required TableDBArray reconciledArray, + required List inputQueues, + }) async { + // Ensure queues all have something to do + inputQueues.removeWhere((q) => q.isEmpty); + if (inputQueues.isEmpty) { + return; + } + + // Sort queues from earliest to latest and then by author + // to ensure a deterministic insert order + inputQueues.sort((a, b) { + final acmp = a.lastOutputPosition?.pos ?? -1; + final bcmp = b.lastOutputPosition?.pos ?? -1; + if (acmp == bcmp) { + return a.author.toString().compareTo(b.author.toString()); + } + return acmp.compareTo(bcmp); + }); + + // Start at the earliest position we know about in all the queues + final firstOutputPos = inputQueues.first.lastOutputPosition?.pos; + // Get the timestamp for this output position + var currentOutputMessage = firstOutputPos == null + ? null + : await reconciledArray.getProtobuf( + proto.Message.fromBuffer, firstOutputPos); + + var currentOutputPos = firstOutputPos ?? 0; + + final toInsert = + SortedList(proto.MessageExt.compareTimestamp); + + while (inputQueues.isNotEmpty) { + // Get up to '_maxReconcileChunk' of the items from the queues + // that we can insert at this location + + bool added; + do { + added = false; + var someQueueEmpty = false; + for (final inputQueue in inputQueues) { + final inputCurrent = inputQueue.current!; + if (currentOutputMessage == null || + inputCurrent.timestamp <= currentOutputMessage.timestamp) { + toInsert.add(inputCurrent); + added = true; + + // Advance this queue + if (!inputQueue.consume()) { + // Queue is empty now, run a queue purge + someQueueEmpty = true; + } + } + } + // Remove empty queues now that we're done iterating + if (someQueueEmpty) { + inputQueues.removeWhere((q) => q.isEmpty); + } + + if (toInsert.length >= _maxReconcileChunk) { + break; + } + } while (added); + + // Perform insertions in bulk + if (toInsert.isNotEmpty) { + await reconciledArray.insertAllProtobuf(currentOutputPos, toInsert); + toInsert.clear(); + } else { + // If there's nothing to insert at this position move to the next one + currentOutputPos++; + currentOutputMessage = await reconciledArray.getProtobuf( + proto.Message.fromBuffer, currentOutputPos); + } + } + } + + //////////////////////////////////////////////////////////////////////////// + + Map _inputSources = {}; + final TableDBArrayCubit _outputCubit; + final void Function(Object, StackTrace?) _onError; + + static const int _maxReconcileChunk = 65536; +} diff --git a/lib/chat/cubits/reconciliation/output_position.dart b/lib/chat/cubits/reconciliation/output_position.dart new file mode 100644 index 0000000..258259e --- /dev/null +++ b/lib/chat/cubits/reconciliation/output_position.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +import '../../../proto/proto.dart' as proto; + +@immutable +class OutputPosition extends Equatable { + const OutputPosition(this.message, this.pos); + final proto.Message message; + final int pos; + @override + List get props => [message, pos]; +} diff --git a/lib/chat/cubits/reconciliation/reconciliation.dart b/lib/chat/cubits/reconciliation/reconciliation.dart new file mode 100644 index 0000000..2dc0b93 --- /dev/null +++ b/lib/chat/cubits/reconciliation/reconciliation.dart @@ -0,0 +1 @@ +export 'message_reconciliation.dart'; diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index ff21d43..6c4fd8f 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -1,28 +1,16 @@ import 'dart:async'; -import 'dart:collection'; import 'dart:convert'; import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; -import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:fixnum/fixnum.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; import '../models/models.dart'; - -@immutable -class MessagePosition extends Equatable { - const MessagePosition(this.message, this.pos); - final proto.Message message; - final int pos; - @override - List get props => [message, pos]; -} +import 'message_reconciliation.dart'; class RenderStateElement { RenderStateElement( @@ -165,6 +153,13 @@ class SingleContactMessagesCubit extends Cubit { _reconciledMessagesCubit = TableDBArrayCubit( open: () async => TableDBArray.make(table: tableName, crypto: crypto), decodeElement: proto.ReconciledMessage.fromBuffer); + + _reconciliation = MessageReconciliation( + output: _reconciledMessagesCubit!, + onError: (e, st) { + emit(AsyncValue.error(e, st)); + }); + _reconciledSubscription = _reconciledMessagesCubit!.stream.listen(_updateReconciledMessagesState); _updateReconciledMessagesState(_reconciledMessagesCubit!.state); @@ -203,7 +198,7 @@ class SingleContactMessagesCubit extends Cubit { return; } - _reconcileMessages( + _reconciliation.reconcileMessages( _activeAccountInfo.localAccount.identityMaster.identityPublicTypedKey(), sentMessages, _sentMessagesCubit!); @@ -216,7 +211,7 @@ class SingleContactMessagesCubit extends Cubit { return; } - _reconcileMessages( + _reconciliation.reconcileMessages( _remoteIdentityPublicKey, rcvdMessages, _rcvdMessagesCubit!); } @@ -246,6 +241,12 @@ class SingleContactMessagesCubit extends Cubit { message.signature = signature.toProto(); } + Future _generateInitialId( + {required PublicKey identityPublicKey}) async => + (await _localMessagesCryptoSystem + .generateHash(identityPublicKey.decode())) + .decode(); + Future _processMessageToSend( proto.Message message, proto.Message? previousMessage) async { // Get the previous message if we don't have one @@ -257,10 +258,9 @@ class SingleContactMessagesCubit extends Cubit { if (previousMessage == null) { // If there's no last sent message, // we start at a hash of the identity public key - message.id = (await _localMessagesCryptoSystem.generateHash( - _activeAccountInfo.localAccount.identityMaster.identityPublicKey - .decode())) - .decode(); + message.id = await _generateInitialId( + identityPublicKey: + _activeAccountInfo.localAccount.identityMaster.identityPublicKey); } else { // If there is a last message, we generate the hash // of the last message's signature and use it as our next id @@ -285,177 +285,6 @@ class SingleContactMessagesCubit extends Cubit { writer.tryAddAll(messages.map((m) => m.writeToBuffer()).toList())); } - void _reconcileMessages( - TypedKey author, - DHTLogStateData inputMessages, - DHTLogCubit inputMessagesCubit) { - singleFuture(_reconciledMessagesCubit!, () async { - // Get the position of our most recent - // reconciled message from this author - // XXX: For a group chat, this should find when the author - // was added to the membership so we don't just go back in time forever - final lastReconciledMessage = - await _reconciledMessagesCubit!.operate((arr) async { - var pos = arr.length - 1; - while (pos >= 0) { - final message = await arr.getProtobuf(proto.Message.fromBuffer, pos); - if (message == null) { - throw StateError('should have gotten last message'); - } - if (message.author.toVeilid() == author) { - return MessagePosition(message, pos); - } - pos--; - } - return null; - }); - - // Find oldest message we have not yet reconciled - final toReconcile = ListQueue(); - - // Go through batches of the input dhtlog starting with - // the current cubit state which is at the tail of the log - // Find the last reconciled message for this author - var currentInputPos = inputMessages.tail; - var currentInputElements = inputMessages.elements; - final inputBatchCount = inputMessages.count; - outer: - while (true) { - for (var rn = currentInputElements.length; - rn >= 0 && currentInputPos >= 0; - rn--, currentInputPos--) { - final elem = currentInputElements[rn]; - - // If we've found an input element that is older than our last - // reconciled message for this author, then we stop - if (lastReconciledMessage != null) { - if (elem.value.timestamp < - lastReconciledMessage.message.timestamp) { - break outer; - } - } - - // Drop the 'offline' elements because we don't reconcile - // anything until it has been confirmed to be committed to the DHT - if (elem.isOffline) { - continue; - } - - // Add to head of reconciliation queue - toReconcile.addFirst(elem.value); - if (toReconcile.length > _maxReconcileChunk) { - toReconcile.removeLast(); - } - } - if (currentInputPos < 0) { - break; - } - - // Get another input batch futher back - final nextInputBatch = await inputMessagesCubit.loadElements( - currentInputPos, inputBatchCount); - final asErr = nextInputBatch.asError; - if (asErr != null) { - emit(AsyncValue.error(asErr.error, asErr.stackTrace)); - return; - } - final asLoading = nextInputBatch.asLoading; - if (asLoading != null) { - // xxx: no need to block the cubit here for this - // xxx: might want to switch to a 'busy' state though - // xxx: to let the messages view show a spinner at the bottom - // xxx: while we reconcile... - // emit(const AsyncValue.loading()); - return; - } - currentInputElements = nextInputBatch.asData!.value; - } - - // Now iterate from our current input position in batches - // and reconcile the messages in the forward direction - var insertPosition = - (lastReconciledMessage != null) ? lastReconciledMessage.pos : 0; - var lastInsertTime = (lastReconciledMessage != null) - ? lastReconciledMessage.message.timestamp - : Int64.ZERO; - - // Insert this batch - xxx expand upon 'res' and iterate batches and update insert position/time - final res = await _reconciledMessagesCubit!.operate((arr) async => - _reconcileMessagesInner( - reconciledArray: arr, - toReconcile: toReconcile, - insertPosition: insertPosition, - lastInsertTime: lastInsertTime)); - - // Update the view - _renderState(); - }); - } - - Future _reconcileMessagesInner( - {required TableDBArray reconciledArray, - required Iterable toReconcile, - required int insertPosition, - required Int64 lastInsertTime}) 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 = await reconciledMessagesWriter - // .getItemRangeProtobuf(proto.Message.fromBuffer, 0); - // if (existingMessages == null) { - // throw Exception( - // 'Could not load existing reconciled messages at this time'); - // } - - // 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 reconciledMessagesWriter.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 reconciledMessagesWriter.tryAddItem(newMessage.writeToBuffer()); - // // Insert into local copy as well for this operation - // existingMessages.add(newMessage); - - // nPos++; - // } - } - // Produce a state for this cubit from the input cubits and queues void _renderState() { // Get all reconciled messages @@ -584,12 +413,12 @@ class SingleContactMessagesCubit extends Cubit { DHTLogCubit? _rcvdMessagesCubit; TableDBArrayCubit? _reconciledMessagesCubit; + late final MessageReconciliation _reconciliation; + late final PersistentQueue _sendingMessagesQueue; StreamSubscription>? _sentSubscription; StreamSubscription>? _rcvdSubscription; StreamSubscription>? _reconciledSubscription; - - static const int _maxReconcileChunk = 65536; } diff --git a/lib/proto/extensions.dart b/lib/proto/extensions.dart index e9fd9a2..1da55f9 100644 --- a/lib/proto/extensions.dart +++ b/lib/proto/extensions.dart @@ -23,4 +23,7 @@ extension MessageExt on proto.Message { } String get uniqueIdString => base64UrlNoPadEncode(uniqueIdBytes); + + static int compareTimestamp(proto.Message a, proto.Message b) => + a.timestamp.compareTo(b.timestamp); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index acdc6fe..985b11f 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -209,8 +209,7 @@ class DHTLog implements DHTDeleteable { OwnedDHTRecordPointer get recordPointer => _spine.recordPointer; /// Runs a closure allowing read-only access to the log - Future operate( - Future Function(DHTLogReadOperations) closure) async { + Future operate(Future Function(DHTLogReadOperations) closure) async { if (!isOpen) { throw StateError('log is not open"'); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index 010c76e..2f97b3f 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -108,6 +108,7 @@ class DHTLogCubit extends Cubit> elements: elements, tail: _tail, count: _count, follow: _follow))); } + // Tail is one past the last element to load Future>>> loadElements( int tail, int count, {bool forceRefresh = false}) async { @@ -184,8 +185,7 @@ class DHTLogCubit extends Cubit> await super.close(); } - Future operate( - Future Function(DHTLogReadOperations) closure) async { + Future operate(Future Function(DHTLogReadOperations) closure) async { await _initWait(); return _log.operate(closure); } diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index 4d5b9dd..8bcd146 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -662,4 +662,46 @@ extension TableDBArrayExt on TableDBArray { T Function(List) fromBuffer, int start, [int? end]) => getRange(start, end ?? _length) .then((out) => out.map(fromBuffer).toList()); + + /// Convenience function: + /// Like add but for a JSON value + Future addJson(T value) async => add(jsonEncodeBytes(value)); + + /// Convenience function: + /// Like add but for a Protobuf value + Future addProtobuf(T value) => + add(value.writeToBuffer()); + + /// Convenience function: + /// Like addAll but for a JSON value + Future addAllJson(List values) async => + addAll(values.map(jsonEncodeBytes).toList()); + + /// Convenience function: + /// Like addAll but for a Protobuf value + Future addAllProtobuf( + List values) async => + addAll(values.map((x) => x.writeToBuffer()).toList()); + + /// Convenience function: + /// Like insert but for a JSON value + Future insertJson(int pos, T value) async => + insert(pos, jsonEncodeBytes(value)); + + /// Convenience function: + /// Like insert but for a Protobuf value + Future insertProtobuf( + int pos, T value) async => + insert(pos, value.writeToBuffer()); + + /// Convenience function: + /// Like insertAll but for a JSON value + Future insertAllJson(int pos, List values) async => + insertAll(pos, values.map(jsonEncodeBytes).toList()); + + /// Convenience function: + /// Like insertAll but for a Protobuf value + Future insertAllProtobuf( + int pos, List values) async => + insertAll(pos, values.map((x) => x.writeToBuffer()).toList()); } diff --git a/pubspec.lock b/pubspec.lock index c6e754b..8a70f22 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1219,6 +1219,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + sorted_list: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: "090eb9be48ab85ff064a0a1d8175b4a72d79b139" + url: "https://gitlab.com/veilid/dart-sorted-list-improved.git" + source: git + version: "1.0.0" source_gen: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1cc893f..133d482 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,10 @@ dependencies: share_plus: ^9.0.0 shared_preferences: ^2.2.3 signal_strength_indicator: ^0.4.1 + sorted_list: + git: + url: https://gitlab.com/veilid/dart-sorted-list-improved.git + ref: main split_view: ^3.2.1 stack_trace: ^1.11.1 stream_transform: ^2.1.0 From fd63a0d5e01dc48bffec66fb67670afd84a40aee Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 31 May 2024 18:27:50 -0400 Subject: [PATCH 11/19] message integrity --- .../reconciliation/author_input_queue.dart | 268 ++++++++++-------- .../reconciliation/author_input_source.dart | 76 ++++- .../reconciliation/message_integrity.dart | 74 +++++ .../message_reconciliation.dart | 51 ++-- .../cubits/reconciliation/reconciliation.dart | 1 + .../cubits/single_contact_messages_cubit.dart | 71 ++--- lib/chat/views/chat_component.dart | 2 +- lib/proto/extensions.dart | 6 +- .../src/dht_log/dht_log_cubit.dart | 45 ++- .../lib/src/online_element_state.dart | 12 + .../veilid_support/lib/veilid_support.dart | 1 + 11 files changed, 370 insertions(+), 237 deletions(-) create mode 100644 lib/chat/cubits/reconciliation/message_integrity.dart create mode 100644 packages/veilid_support/lib/src/online_element_state.dart diff --git a/lib/chat/cubits/reconciliation/author_input_queue.dart b/lib/chat/cubits/reconciliation/author_input_queue.dart index b441e75..b9fd7d7 100644 --- a/lib/chat/cubits/reconciliation/author_input_queue.dart +++ b/lib/chat/cubits/reconciliation/author_input_queue.dart @@ -1,145 +1,191 @@ import 'dart:async'; -import 'dart:collection'; -import 'dart:math'; - -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../../proto/proto.dart' as proto; import 'author_input_source.dart'; +import 'message_integrity.dart'; import 'output_position.dart'; class AuthorInputQueue { - AuthorInputQueue({ - required this.author, - required this.inputSource, - required this.lastOutputPosition, - required this.onError, - }): - assert(inputSource.messages.count>0, 'no input source window length'), - assert(inputSource.messages.elements.isNotEmpty, 'no input source elements'), - assert(inputSource.messages.tail >= inputSource.messages.elements.length, 'tail is before initial messages end'), - assert(inputSource.messages.tail > 0, 'tail is not greater than zero'), - currentPosition = inputSource.messages.tail, - currentWindow = inputSource.messages.elements, - windowLength = inputSource.messages.count, - windowFirst = inputSource.messages.tail - inputSource.messages.elements.length, - windowLast = inputSource.messages.tail - 1; + AuthorInputQueue._({ + required TypedKey author, + required AuthorInputSource inputSource, + required OutputPosition? outputPosition, + required void Function(Object, StackTrace?) onError, + required MessageIntegrity messageIntegrity, + }) : _author = author, + _onError = onError, + _inputSource = inputSource, + _outputPosition = outputPosition, + _lastMessage = outputPosition?.message, + _messageIntegrity = messageIntegrity, + _currentPosition = inputSource.currentWindow.last; - //////////////////////////////////////////////////////////////////////////// - - bool get isEmpty => toReconcile.isEmpty; - - proto.Message? get current => toReconcile.firstOrNull; - - bool consume() { - toReconcile.removeFirst(); - return toReconcile.isNotEmpty; + static Future create({ + required TypedKey author, + required AuthorInputSource inputSource, + required OutputPosition? outputPosition, + required void Function(Object, StackTrace?) onError, + }) async { + final queue = AuthorInputQueue._( + author: author, + inputSource: inputSource, + outputPosition: outputPosition, + onError: onError, + messageIntegrity: await MessageIntegrity.create(author: author)); + if (!await queue._findStartOfWork()) { + return null; + } + return queue; } - Future prepareInputQueue() async { - // Go through batches of the input dhtlog starting with - // the current cubit state which is at the tail of the log - // Find the last reconciled message for this author + //////////////////////////////////////////////////////////////////////////// + // Public interface - outer: + // Check if there are no messages in this queue to reconcile + bool get isEmpty => _currentMessage == null; + + // Get the current message that needs reconciliation + proto.Message? get current => _currentMessage; + + // Get the earliest output position to start inserting + OutputPosition? get outputPosition => _outputPosition; + + // Get the author of this queue + TypedKey get author => _author; + + // Remove a reconciled message and move to the next message + // Returns true if there is more work to do + Future consume() async { while (true) { - for (var rn = currentWindow.length; - rn >= 0 && currentPosition >= 0; - rn--, currentPosition--) { - final elem = currentWindow[rn]; + _lastMessage = _currentMessage; - // If we've found an input element that is older than our last - // reconciled message for this author, then we stop - if (lastOutputPosition != null) { - if (elem.value.timestamp < lastOutputPosition!.message.timestamp) { - break outer; - } - } + _currentPosition++; - // Drop the 'offline' elements because we don't reconcile - // anything until it has been confirmed to be committed to the DHT - if (elem.isOffline) { + // Get more window if we need to + if (!await _updateWindow()) { + // Window is not available so this queue can't work right now + return false; + } + final nextMessage = _inputSource.currentWindow + .elements[_currentPosition - _inputSource.currentWindow.first]; + + // Drop the 'offline' elements because we don't reconcile + // anything until it has been confirmed to be committed to the DHT + if (nextMessage.isOffline) { + continue; + } + + if (_lastMessage != null) { + // Ensure the timestamp is not moving backward + if (nextMessage.value.timestamp < _lastMessage!.timestamp) { continue; } - - // Add to head of reconciliation queue - toReconcile.addFirst(elem.value); - if (toReconcile.length > _maxQueueChunk) { - toReconcile.removeLast(); - } - } - if (currentPosition < 0) { - break; } - xxx update window here and make this and other methods work + // Verify the id chain for the message + final matchId = await _messageIntegrity.generateMessageId(_lastMessage); + if (matchId.compare(nextMessage.value.idBytes) != 0) { + continue; + } + + // Verify the signature for the message + if (!await _messageIntegrity.verifyMessage(nextMessage.value)) { + continue; + } + + _currentMessage = nextMessage.value; + break; } return true; } + //////////////////////////////////////////////////////////////////////////// + // Internal implementation + + // Walk backward from the tail of the input queue to find the first + // message newer than our last reconcicled message from this author + // Returns false if no work is needed + Future _findStartOfWork() async { + // Iterate windows over the inputSource + outer: + while (true) { + // Iterate through current window backward + for (var i = _inputSource.currentWindow.elements.length; + i >= 0 && _currentPosition >= 0; + i--, _currentPosition--) { + final elem = _inputSource.currentWindow.elements[i]; + + // If we've found an input element that is older than our last + // reconciled message for this author, then we stop + if (_lastMessage != null) { + if (elem.value.timestamp < _lastMessage!.timestamp) { + break outer; + } + } + } + // If we're at the beginning of the inputSource then we stop + if (_currentPosition < 0) { + break; + } + + // Get more window if we need to + if (!await _updateWindow()) { + // Window is not available or things are empty so this + // queue can't work right now + return false; + } + } + + // The current position should be equal to the first message to process + // and the current window to process should not be empty + return _inputSource.currentWindow.elements.isNotEmpty; + } + // Slide the window toward the current position and load the batch around it - Future updateWindow() async { - - // Check if we are still in the window - if (currentPosition>=windowFirst && currentPosition <= windowLast) { - return true; - } - - // Get the length of the cubit - final inputLength = await inputSource.cubit.operate((r) async => r.length); - - // If not, slide the window - if (currentPosition _updateWindow() async { + // Check if we are still in the window + if (_currentPosition >= _inputSource.currentWindow.first && + _currentPosition <= _inputSource.currentWindow.last) { return true; + } + + // Get another input batch futher back + final avOk = + await _inputSource.updateWindow(_currentPosition, _maxWindowLength); + + final asErr = avOk.asError; + if (asErr != null) { + _onError(asErr.error, asErr.stackTrace); + return false; + } + final asLoading = avOk.asLoading; + if (asLoading != null) { + // xxx: no need to block the cubit here for this + // xxx: might want to switch to a 'busy' state though + // xxx: to let the messages view show a spinner at the bottom + // xxx: while we reconcile... + // emit(const AsyncValue.loading()); + return false; + } + return avOk.asData!.value; } //////////////////////////////////////////////////////////////////////////// - final TypedKey author; - final ListQueue toReconcile = ListQueue(); - final AuthorInputSource inputSource; - final OutputPosition? lastOutputPosition; - final void Function(Object, StackTrace?) onError; + final TypedKey _author; + final AuthorInputSource _inputSource; + final OutputPosition? _outputPosition; + final void Function(Object, StackTrace?) _onError; + final MessageIntegrity _messageIntegrity; + // The last message we've consumed + proto.Message? _lastMessage; // The current position in the input log that we are looking at - int currentPosition; - // The current input window elements - IList> currentWindow; - // The first position of the sliding input window - int windowFirst; - // The last position of the sliding input window - int windowLast; + int _currentPosition; + // The current message we're looking at + proto.Message? _currentMessage; // Desired maximum window length - int windowLength; - - static const int _maxQueueChunk = 256; + static const int _maxWindowLength = 256; } diff --git a/lib/chat/cubits/reconciliation/author_input_source.dart b/lib/chat/cubits/reconciliation/author_input_source.dart index 75f020e..1f67264 100644 --- a/lib/chat/cubits/reconciliation/author_input_source.dart +++ b/lib/chat/cubits/reconciliation/author_input_source.dart @@ -1,10 +1,76 @@ +import 'dart:math'; + +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../../proto/proto.dart' as proto; -class AuthorInputSource { - AuthorInputSource({required this.messages, required this.cubit}); - - final DHTLogStateData messages; - final DHTLogCubit cubit; +@immutable +class InputWindow { + const InputWindow( + {required this.elements, required this.first, required this.last}); + final IList> elements; + final int first; + final int last; +} + +class AuthorInputSource { + AuthorInputSource.fromCubit( + {required DHTLogStateData cubitState, + required this.cubit}) { + _currentWindow = InputWindow( + elements: cubitState.elements, + first: cubitState.tail - cubitState.elements.length, + last: cubitState.tail - 1); + } + + //////////////////////////////////////////////////////////////////////////// + + InputWindow get currentWindow => _currentWindow; + + Future> updateWindow( + int currentPosition, int windowLength) async => + cubit.operate((reader) async { + // See if we're beyond the input source + if (currentPosition < 0 || currentPosition >= reader.length) { + return const AsyncValue.data(false); + } + + // Slide the window if we need to + var first = _currentWindow.first; + var last = _currentWindow.last; + if (currentPosition < first) { + // Slide it backward, current position is now last + first = max((currentPosition - windowLength) + 1, 0); + last = currentPosition; + } else if (currentPosition > last) { + // Slide it forward, current position is now first + first = currentPosition; + last = min((currentPosition + windowLength) - 1, reader.length - 1); + } else { + return const AsyncValue.data(true); + } + + // Get another input batch futher back + final nextWindow = await cubit.loadElementsFromReader( + reader, last + 1, (last + 1) - first); + final asErr = nextWindow.asError; + if (asErr != null) { + return AsyncValue.error(asErr.error, asErr.stackTrace); + } + final asLoading = nextWindow.asLoading; + if (asLoading != null) { + return const AsyncValue.loading(); + } + _currentWindow = InputWindow( + elements: nextWindow.asData!.value, first: first, last: last); + return const AsyncValue.data(true); + }); + + //////////////////////////////////////////////////////////////////////////// + final DHTLogCubit cubit; + + late InputWindow _currentWindow; } diff --git a/lib/chat/cubits/reconciliation/message_integrity.dart b/lib/chat/cubits/reconciliation/message_integrity.dart new file mode 100644 index 0000000..2fd1956 --- /dev/null +++ b/lib/chat/cubits/reconciliation/message_integrity.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:protobuf/protobuf.dart'; +import 'package:veilid_support/veilid_support.dart'; +import '../../../proto/proto.dart' as proto; + +class MessageIntegrity { + MessageIntegrity._({ + required TypedKey author, + required VeilidCryptoSystem crypto, + }) : _author = author, + _crypto = crypto; + static Future create({required TypedKey author}) async { + final crypto = await Veilid.instance.getCryptoSystem(author.kind); + return MessageIntegrity._(author: author, crypto: crypto); + } + + //////////////////////////////////////////////////////////////////////////// + // Public interface + + Future generateMessageId(proto.Message? previous) async { + if (previous == null) { + // If there's no last sent message, + // we start at a hash of the identity public key + return _generateInitialId(); + } else { + // If there is a last message, we generate the hash + // of the last message's signature and use it as our next id + return _hashSignature(previous.signature); + } + } + + Future signMessage( + proto.Message message, + SecretKey authorSecret, + ) async { + // Ensure this message is not already signed + assert(!message.hasSignature(), 'should not sign message twice'); + // Generate data to sign + final data = Uint8List.fromList(utf8.encode(message.writeToJson())); + + // Sign with our identity + final signature = await _crypto.sign(_author.value, authorSecret, data); + + // Add to the message + message.signature = signature.toProto(); + } + + Future verifyMessage(proto.Message message) async { + // Ensure the message is signed + assert(message.hasSignature(), 'should not verify unsigned message'); + final signature = message.signature.toVeilid(); + + // Generate data to sign + final messageNoSig = message.deepCopy()..clearSignature(); + final data = Uint8List.fromList(utf8.encode(messageNoSig.writeToJson())); + + // Verify signature + return _crypto.verify(_author.value, data, signature); + } + + //////////////////////////////////////////////////////////////////////////// + // Private implementation + + Future _generateInitialId() async => + (await _crypto.generateHash(_author.decode())).decode(); + + Future _hashSignature(proto.Signature signature) async => + (await _crypto.generateHash(signature.toVeilid().decode())).decode(); + //////////////////////////////////////////////////////////////////////////// + final TypedKey _author; + final VeilidCryptoSystem _crypto; +} diff --git a/lib/chat/cubits/reconciliation/message_reconciliation.dart b/lib/chat/cubits/reconciliation/message_reconciliation.dart index 51cfe2b..1687f4d 100644 --- a/lib/chat/cubits/reconciliation/message_reconciliation.dart +++ b/lib/chat/cubits/reconciliation/message_reconciliation.dart @@ -21,14 +21,14 @@ class MessageReconciliation { void reconcileMessages( TypedKey author, - DHTLogStateData inputMessages, + DHTLogStateData inputMessagesCubitState, DHTLogCubit inputMessagesCubit) { - if (inputMessages.elements.isEmpty) { + if (inputMessagesCubitState.elements.isEmpty) { return; } - _inputSources[author] = - AuthorInputSource(messages: inputMessages, cubit: inputMessagesCubit); + _inputSources[author] = AuthorInputSource.fromCubit( + cubitState: inputMessagesCubitState, cubit: inputMessagesCubit); singleFuture(this, onError: _onError, () async { // Take entire list of input sources we have currently and process them @@ -63,14 +63,15 @@ class MessageReconciliation { {required TypedKey author, required AuthorInputSource inputSource}) async { // Get the position of our most recent reconciled message from this author - final lastReconciledMessage = - await _findNewestReconciledMessage(author: author); + final outputPosition = await _findLastOutputPosition(author: author); // Find oldest message we have not yet reconciled - final inputQueue = await _buildAuthorInputQueue( - author: author, - inputSource: inputSource, - lastOutputPosition: lastReconciledMessage); + final inputQueue = await AuthorInputQueue.create( + author: author, + inputSource: inputSource, + outputPosition: outputPosition, + onError: _onError, + ); return inputQueue; } @@ -78,7 +79,7 @@ class MessageReconciliation { // reconciled message from this author // XXX: For a group chat, this should find when the author // was added to the membership so we don't just go back in time forever - Future _findNewestReconciledMessage( + Future _findLastOutputPosition( {required TypedKey author}) async => _outputCubit.operate((arr) async { var pos = arr.length - 1; @@ -95,26 +96,6 @@ class MessageReconciliation { return null; }); - // Find oldest message we have not yet reconciled and build a queue forward - // from that position - Future _buildAuthorInputQueue( - {required TypedKey author, - required AuthorInputSource inputSource, - required OutputPosition? lastOutputPosition}) async { - // Make an author input queue - final authorInputQueue = AuthorInputQueue( - author: author, - inputSource: inputSource, - lastOutputPosition: lastOutputPosition, - onError: _onError); - - if (!await authorInputQueue.prepareInputQueue()) { - return null; - } - - return authorInputQueue; - } - // Process a list of author input queues and insert their messages // into the output array, performing validation steps along the way Future _reconcileInputQueues({ @@ -130,8 +111,8 @@ class MessageReconciliation { // Sort queues from earliest to latest and then by author // to ensure a deterministic insert order inputQueues.sort((a, b) { - final acmp = a.lastOutputPosition?.pos ?? -1; - final bcmp = b.lastOutputPosition?.pos ?? -1; + final acmp = a.outputPosition?.pos ?? -1; + final bcmp = b.outputPosition?.pos ?? -1; if (acmp == bcmp) { return a.author.toString().compareTo(b.author.toString()); } @@ -139,7 +120,7 @@ class MessageReconciliation { }); // Start at the earliest position we know about in all the queues - final firstOutputPos = inputQueues.first.lastOutputPosition?.pos; + final firstOutputPos = inputQueues.first.outputPosition?.pos; // Get the timestamp for this output position var currentOutputMessage = firstOutputPos == null ? null @@ -167,7 +148,7 @@ class MessageReconciliation { added = true; // Advance this queue - if (!inputQueue.consume()) { + if (!await inputQueue.consume()) { // Queue is empty now, run a queue purge someQueueEmpty = true; } diff --git a/lib/chat/cubits/reconciliation/reconciliation.dart b/lib/chat/cubits/reconciliation/reconciliation.dart index 2dc0b93..a8187cf 100644 --- a/lib/chat/cubits/reconciliation/reconciliation.dart +++ b/lib/chat/cubits/reconciliation/reconciliation.dart @@ -1 +1,2 @@ +export 'message_integrity.dart'; export 'message_reconciliation.dart'; diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 6c4fd8f..c444134 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -1,6 +1,4 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -10,7 +8,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; import '../models/models.dart'; -import 'message_reconciliation.dart'; +import 'reconciliation/reconciliation.dart'; class RenderStateElement { RenderStateElement( @@ -102,12 +100,11 @@ class SingleContactMessagesCubit extends Cubit { // Make crypto Future _initCrypto() async { - _messagesCrypto = await _activeAccountInfo + _conversationCrypto = await _activeAccountInfo .makeConversationCrypto(_remoteIdentityPublicKey); - _localMessagesCryptoSystem = - await Veilid.instance.getCryptoSystem(_localMessagesRecordKey.kind); - _identityCryptoSystem = - await _activeAccountInfo.localAccount.identityMaster.identityCrypto; + _senderMessageIntegrity = await MessageIntegrity.create( + author: _activeAccountInfo.localAccount.identityMaster + .identityPublicTypedKey()); } // Open local messages key @@ -119,7 +116,7 @@ class SingleContactMessagesCubit extends Cubit { debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::' 'SentMessages', parent: _localConversationRecordKey, - crypto: _messagesCrypto), + crypto: _conversationCrypto), decodeElement: proto.Message.fromBuffer); _sentSubscription = _sentMessagesCubit!.stream.listen(_updateSentMessagesState); @@ -133,7 +130,7 @@ class SingleContactMessagesCubit extends Cubit { debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::' 'RcvdMessages', parent: _remoteConversationRecordKey, - crypto: _messagesCrypto), + crypto: _conversationCrypto), decodeElement: proto.Message.fromBuffer); _rcvdSubscription = _rcvdMessagesCubit!.stream.listen(_updateRcvdMessagesState); @@ -222,31 +219,6 @@ class SingleContactMessagesCubit extends Cubit { _renderState(); } - Future _hashSignature(proto.Signature signature) async => - (await _localMessagesCryptoSystem - .generateHash(signature.toVeilid().decode())) - .decode(); - - Future _signMessage(proto.Message message) async { - // Generate data to sign - final data = Uint8List.fromList(utf8.encode(message.writeToJson())); - - // Sign with our identity - final signature = await _identityCryptoSystem.sign( - _activeAccountInfo.localAccount.identityMaster.identityPublicKey, - _activeAccountInfo.userLogin.identitySecret.value, - data); - - // Add to the message - message.signature = signature.toProto(); - } - - Future _generateInitialId( - {required PublicKey identityPublicKey}) async => - (await _localMessagesCryptoSystem - .generateHash(identityPublicKey.decode())) - .decode(); - Future _processMessageToSend( proto.Message message, proto.Message? previousMessage) async { // Get the previous message if we don't have one @@ -255,20 +227,12 @@ class SingleContactMessagesCubit extends Cubit { ? null : await r.getProtobuf(proto.Message.fromBuffer, r.length - 1)); - if (previousMessage == null) { - // If there's no last sent message, - // we start at a hash of the identity public key - message.id = await _generateInitialId( - identityPublicKey: - _activeAccountInfo.localAccount.identityMaster.identityPublicKey); - } else { - // If there is a last message, we generate the hash - // of the last message's signature and use it as our next id - message.id = await _hashSignature(previousMessage.signature); - } + message.id = + await _senderMessageIntegrity.generateMessageId(previousMessage); // Now sign it - await _signMessage(message); + await _senderMessageIntegrity.signMessage( + message, _activeAccountInfo.userLogin.identitySecret.value); } // Async process to send messages in the background @@ -303,17 +267,17 @@ class SingleContactMessagesCubit extends Cubit { // Generate state for each message final sentMessagesMap = - IMap>.fromValues( - keyMapper: (x) => x.value.uniqueIdString, + IMap>.fromValues( + keyMapper: (x) => x.value.authorUniqueIdString, values: sentMessages.elements, ); final reconciledMessagesMap = IMap.fromValues( - keyMapper: (x) => x.content.uniqueIdString, + keyMapper: (x) => x.content.authorUniqueIdString, values: reconciledMessages.elements, ); final sendingMessagesMap = IMap.fromValues( - keyMapper: (x) => x.uniqueIdString, + keyMapper: (x) => x.authorUniqueIdString, values: sendingMessages, ); @@ -405,9 +369,8 @@ class SingleContactMessagesCubit extends Cubit { final TypedKey _remoteConversationRecordKey; final TypedKey _remoteMessagesRecordKey; - late final VeilidCrypto _messagesCrypto; - late final VeilidCryptoSystem _localMessagesCryptoSystem; - late final VeilidCryptoSystem _identityCryptoSystem; + late final VeilidCrypto _conversationCrypto; + late final MessageIntegrity _senderMessageIntegrity; DHTLogCubit? _sentMessagesCubit; DHTLogCubit? _rcvdMessagesCubit; diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 7f549cb..1e296e9 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -127,7 +127,7 @@ class ChatComponent extends StatelessWidget { author: isLocal ? _localUser : _remoteUser, createdAt: (message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(), - id: message.content.uniqueIdString, + id: message.content.authorUniqueIdString, text: contextText.text, showStatus: status != null, status: status); diff --git a/lib/proto/extensions.dart b/lib/proto/extensions.dart index 1da55f9..25b8558 100644 --- a/lib/proto/extensions.dart +++ b/lib/proto/extensions.dart @@ -16,13 +16,15 @@ Map reconciledMessageToJson(proto.ReconciledMessage m) => m.writeToJsonMap(); extension MessageExt on proto.Message { - Uint8List get uniqueIdBytes { + Uint8List get idBytes => Uint8List.fromList(id); + + Uint8List get authorUniqueIdBytes { final author = this.author.toVeilid().decode(); final id = this.id; return Uint8List.fromList([...author, ...id]); } - String get uniqueIdString => base64UrlNoPadEncode(uniqueIdBytes); + String get authorUniqueIdString => base64UrlNoPadEncode(authorUniqueIdBytes); static int compareTimestamp(proto.Message a, proto.Message b) => a.timestamp.compareTo(b.timestamp); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index 2f97b3f..f70a34c 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -9,16 +9,6 @@ import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; -@immutable -class DHTLogElementState extends Equatable { - const DHTLogElementState({required this.value, required this.isOffline}); - final T value; - final bool isOffline; - - @override - List get props => [value, isOffline]; -} - @immutable class DHTLogStateData extends Equatable { const DHTLogStateData( @@ -28,7 +18,7 @@ class DHTLogStateData extends Equatable { required this.follow}); // The view of the elements in the dhtlog // Span is from [tail-length, tail) - final IList> elements; + final IList> elements; // One past the end of the last element final int tail; // The total number of elements to try to keep in 'elements' @@ -92,7 +82,8 @@ class DHTLogCubit extends Cubit> Future _refreshInner(void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { - final avElements = await loadElements(_tail, _count); + final avElements = await operate( + (reader) => loadElementsFromReader(reader, _tail, _count)); final err = avElements.asError; if (err != null) { emit(AsyncValue.error(err.error, err.stackTrace)); @@ -109,26 +100,22 @@ class DHTLogCubit extends Cubit> } // Tail is one past the last element to load - Future>>> loadElements( - int tail, int count, + Future>>> loadElementsFromReader( + DHTLogReadOperations reader, int tail, int count, {bool forceRefresh = false}) async { - await _initWait(); try { - final allItems = await _log.operate((reader) async { - final length = reader.length; - final end = ((tail - 1) % length) + 1; - final start = (count < end) ? end - count : 0; + final length = reader.length; + final end = ((tail - 1) % length) + 1; + final start = (count < end) ? end - count : 0; - final offlinePositions = await reader.getOfflinePositions(); - final allItems = (await reader.getRange(start, - length: end - start, forceRefresh: forceRefresh)) - ?.indexed - .map((x) => DHTLogElementState( - value: _decodeElement(x.$2), - isOffline: offlinePositions.contains(x.$1))) - .toIList(); - return allItems; - }); + final offlinePositions = await reader.getOfflinePositions(); + final allItems = (await reader.getRange(start, + length: end - start, forceRefresh: forceRefresh)) + ?.indexed + .map((x) => OnlineElementState( + value: _decodeElement(x.$2), + isOffline: offlinePositions.contains(x.$1))) + .toIList(); if (allItems == null) { return const AsyncValue.loading(); } diff --git a/packages/veilid_support/lib/src/online_element_state.dart b/packages/veilid_support/lib/src/online_element_state.dart new file mode 100644 index 0000000..8cbd38b --- /dev/null +++ b/packages/veilid_support/lib/src/online_element_state.dart @@ -0,0 +1,12 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +class OnlineElementState extends Equatable { + const OnlineElementState({required this.value, required this.isOffline}); + final T value; + final bool isOffline; + + @override + List get props => [value, isOffline]; +} diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index 42aa839..0f19bf3 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -10,6 +10,7 @@ export 'src/config.dart'; export 'src/identity.dart'; export 'src/json_tools.dart'; export 'src/memory_tools.dart'; +export 'src/online_element_state.dart'; export 'src/output.dart'; export 'src/persistent_queue.dart'; export 'src/protobuf_tools.dart'; From f780a60d69ca162add0a42f8fae4042d69afccc8 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 31 May 2024 18:55:44 -0400 Subject: [PATCH 12/19] fixing bugs --- lib/chat/cubits/single_contact_messages_cubit.dart | 3 ++- lib/chat/models/message_state.dart | 3 ++- packages/veilid_support/lib/src/table_db_array_cubit.dart | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index c444134..d2abb22 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -143,7 +143,8 @@ class SingleContactMessagesCubit extends Cubit { // Open reconciled chat record key Future _initReconciledMessagesCubit() async { - final tableName = _localConversationRecordKey.toString(); + final tableName = + _localConversationRecordKey.toString().replaceAll(':', '_'); final crypto = await _makeLocalMessagesCrypto(); diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart index f9952fa..8eacc8e 100644 --- a/lib/chat/models/message_state.dart +++ b/lib/chat/models/message_state.dart @@ -4,6 +4,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../proto/proto.dart' as proto; +import '../../proto/proto.dart' show messageFromJson, messageToJson; part 'message_state.freezed.dart'; part 'message_state.g.dart'; @@ -26,7 +27,7 @@ enum MessageSendState { class MessageState with _$MessageState { const factory MessageState({ // Content of the message - @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + @JsonKey(fromJson: messageFromJson, toJson: messageToJson) required proto.Message content, // Sent timestamp required Timestamp sentTimestamp, diff --git a/packages/veilid_support/lib/src/table_db_array_cubit.dart b/packages/veilid_support/lib/src/table_db_array_cubit.dart index 1cebab5..c4ff507 100644 --- a/packages/veilid_support/lib/src/table_db_array_cubit.dart +++ b/packages/veilid_support/lib/src/table_db_array_cubit.dart @@ -105,6 +105,9 @@ class TableDBArrayCubit extends Cubit> ) async { try { final length = _array.length; + if (length == 0) { + return AsyncValue.data(IList.empty()); + } final end = ((tail - 1) % length) + 1; final start = (count < end) ? end - count : 0; final allItems = From 0e4606f35e2c12d49c720ef399d3dfecf7b50619 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 2 Jun 2024 11:04:19 -0400 Subject: [PATCH 13/19] debugging --- assets/i18n/en.json | 1 + .../reconciliation/author_input_queue.dart | 38 ++- .../reconciliation/author_input_source.dart | 7 +- .../message_reconciliation.dart | 38 ++- .../reconciliation/output_position.dart | 2 +- .../cubits/single_contact_messages_cubit.dart | 134 +++++---- lib/chat/models/message_state.freezed.dart | 14 +- lib/chat/views/chat_component.dart | 24 +- lib/chat/views/no_conversation_widget.dart | 55 ++-- .../home_account_ready_chat.dart | 2 +- .../home_account_ready_main.dart | 2 +- lib/main.dart | 4 +- .../src/dht_log/dht_log_cubit.dart | 74 +++-- .../src/dht_log/dht_log_spine.dart | 105 ++++--- .../src/dht_short_array/dht_short_array.dart | 11 + .../lib/src/table_db_array.dart | 269 ++++++++++++------ ...art => table_db_array_protobuf_cubit.dart} | 40 +-- .../veilid_support/lib/veilid_support.dart | 2 +- packages/veilid_support/pubspec.lock | 14 +- packages/veilid_support/pubspec.yaml | 6 + 20 files changed, 521 insertions(+), 321 deletions(-) rename packages/veilid_support/lib/src/{table_db_array_cubit.dart => table_db_array_protobuf_cubit.dart} (81%) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index f074031..eeaa476 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -67,6 +67,7 @@ "new_chat": "New Chat" }, "chat": { + "start_a_conversation": "Start A Conversation", "say_something": "Say Something" }, "create_invitation_dialog": { diff --git a/lib/chat/cubits/reconciliation/author_input_queue.dart b/lib/chat/cubits/reconciliation/author_input_queue.dart index b9fd7d7..009604d 100644 --- a/lib/chat/cubits/reconciliation/author_input_queue.dart +++ b/lib/chat/cubits/reconciliation/author_input_queue.dart @@ -18,7 +18,7 @@ class AuthorInputQueue { _onError = onError, _inputSource = inputSource, _outputPosition = outputPosition, - _lastMessage = outputPosition?.message, + _lastMessage = outputPosition?.message.content, _messageIntegrity = messageIntegrity, _currentPosition = inputSource.currentWindow.last; @@ -43,8 +43,8 @@ class AuthorInputQueue { //////////////////////////////////////////////////////////////////////////// // Public interface - // Check if there are no messages in this queue to reconcile - bool get isEmpty => _currentMessage == null; + // Check if there are no messages left in this queue to reconcile + bool get isDone => _isDone; // Get the current message that needs reconciliation proto.Message? get current => _currentMessage; @@ -58,6 +58,9 @@ class AuthorInputQueue { // Remove a reconciled message and move to the next message // Returns true if there is more work to do Future consume() async { + if (_isDone) { + return false; + } while (true) { _lastMessage = _currentMessage; @@ -66,6 +69,7 @@ class AuthorInputQueue { // Get more window if we need to if (!await _updateWindow()) { // Window is not available so this queue can't work right now + _isDone = true; return false; } final nextMessage = _inputSource.currentWindow @@ -73,9 +77,9 @@ class AuthorInputQueue { // Drop the 'offline' elements because we don't reconcile // anything until it has been confirmed to be committed to the DHT - if (nextMessage.isOffline) { - continue; - } + // if (nextMessage.isOffline) { + // continue; + // } if (_lastMessage != null) { // Ensure the timestamp is not moving backward @@ -112,7 +116,7 @@ class AuthorInputQueue { outer: while (true) { // Iterate through current window backward - for (var i = _inputSource.currentWindow.elements.length; + for (var i = _inputSource.currentWindow.elements.length - 1; i >= 0 && _currentPosition >= 0; i--, _currentPosition--) { final elem = _inputSource.currentWindow.elements[i]; @@ -134,13 +138,24 @@ class AuthorInputQueue { if (!await _updateWindow()) { // Window is not available or things are empty so this // queue can't work right now + _isDone = true; return false; } } - // The current position should be equal to the first message to process - // and the current window to process should not be empty - return _inputSource.currentWindow.elements.isNotEmpty; + // _currentPosition points to either before the input source starts + // or the position of the previous element. We still need to set the + // _currentMessage to the previous element so consume() can compare + // against it if we can. + if (_currentPosition >= 0) { + _currentMessage = _inputSource.currentWindow + .elements[_currentPosition - _inputSource.currentWindow.first].value; + } + + // After this consume(), the currentPosition and _currentMessage should + // be equal to the first message to process and the current window to + // process should not be empty + return consume(); } // Slide the window toward the current position and load the batch around it @@ -186,6 +201,9 @@ class AuthorInputQueue { int _currentPosition; // The current message we're looking at proto.Message? _currentMessage; + // If we have reached the end + bool _isDone = false; + // Desired maximum window length static const int _maxWindowLength = 256; } diff --git a/lib/chat/cubits/reconciliation/author_input_source.dart b/lib/chat/cubits/reconciliation/author_input_source.dart index 1f67264..32a750e 100644 --- a/lib/chat/cubits/reconciliation/author_input_source.dart +++ b/lib/chat/cubits/reconciliation/author_input_source.dart @@ -21,9 +21,10 @@ class AuthorInputSource { {required DHTLogStateData cubitState, required this.cubit}) { _currentWindow = InputWindow( - elements: cubitState.elements, - first: cubitState.tail - cubitState.elements.length, - last: cubitState.tail - 1); + elements: cubitState.window, + first: (cubitState.windowTail - cubitState.window.length) % + cubitState.length, + last: (cubitState.windowTail - 1) % cubitState.length); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/chat/cubits/reconciliation/message_reconciliation.dart b/lib/chat/cubits/reconciliation/message_reconciliation.dart index 1687f4d..aa53f49 100644 --- a/lib/chat/cubits/reconciliation/message_reconciliation.dart +++ b/lib/chat/cubits/reconciliation/message_reconciliation.dart @@ -12,7 +12,7 @@ import 'output_position.dart'; class MessageReconciliation { MessageReconciliation( - {required TableDBArrayCubit output, + {required TableDBArrayProtobufCubit output, required void Function(Object, StackTrace?) onError}) : _outputCubit = output, _onError = onError; @@ -23,7 +23,7 @@ class MessageReconciliation { TypedKey author, DHTLogStateData inputMessagesCubitState, DHTLogCubit inputMessagesCubit) { - if (inputMessagesCubitState.elements.isEmpty) { + if (inputMessagesCubitState.window.isEmpty) { return; } @@ -84,11 +84,11 @@ class MessageReconciliation { _outputCubit.operate((arr) async { var pos = arr.length - 1; while (pos >= 0) { - final message = await arr.getProtobuf(proto.Message.fromBuffer, pos); + final message = await arr.get(pos); if (message == null) { throw StateError('should have gotten last message'); } - if (message.author.toVeilid() == author) { + if (message.content.author.toVeilid() == author) { return OutputPosition(message, pos); } pos--; @@ -99,11 +99,11 @@ class MessageReconciliation { // Process a list of author input queues and insert their messages // into the output array, performing validation steps along the way Future _reconcileInputQueues({ - required TableDBArray reconciledArray, + required TableDBArrayProtobuf reconciledArray, required List inputQueues, }) async { // Ensure queues all have something to do - inputQueues.removeWhere((q) => q.isEmpty); + inputQueues.removeWhere((q) => q.isDone); if (inputQueues.isEmpty) { return; } @@ -124,8 +124,7 @@ class MessageReconciliation { // Get the timestamp for this output position var currentOutputMessage = firstOutputPos == null ? null - : await reconciledArray.getProtobuf( - proto.Message.fromBuffer, firstOutputPos); + : await reconciledArray.get(firstOutputPos); var currentOutputPos = firstOutputPos ?? 0; @@ -143,7 +142,7 @@ class MessageReconciliation { for (final inputQueue in inputQueues) { final inputCurrent = inputQueue.current!; if (currentOutputMessage == null || - inputCurrent.timestamp <= currentOutputMessage.timestamp) { + inputCurrent.timestamp < currentOutputMessage.content.timestamp) { toInsert.add(inputCurrent); added = true; @@ -156,7 +155,7 @@ class MessageReconciliation { } // Remove empty queues now that we're done iterating if (someQueueEmpty) { - inputQueues.removeWhere((q) => q.isEmpty); + inputQueues.removeWhere((q) => q.isDone); } if (toInsert.length >= _maxReconcileChunk) { @@ -166,13 +165,24 @@ class MessageReconciliation { // Perform insertions in bulk if (toInsert.isNotEmpty) { - await reconciledArray.insertAllProtobuf(currentOutputPos, toInsert); + final reconciledTime = Veilid.instance.now().toInt64(); + + // Add reconciled timestamps + final reconciledInserts = toInsert + .map((message) => proto.ReconciledMessage() + ..reconciledTime = reconciledTime + ..content = message) + .toList(); + + await reconciledArray.insertAll(currentOutputPos, reconciledInserts); + toInsert.clear(); } else { // If there's nothing to insert at this position move to the next one currentOutputPos++; - currentOutputMessage = await reconciledArray.getProtobuf( - proto.Message.fromBuffer, currentOutputPos); + currentOutputMessage = (currentOutputPos == reconciledArray.length) + ? null + : await reconciledArray.get(currentOutputPos); } } } @@ -180,7 +190,7 @@ class MessageReconciliation { //////////////////////////////////////////////////////////////////////////// Map _inputSources = {}; - final TableDBArrayCubit _outputCubit; + final TableDBArrayProtobufCubit _outputCubit; final void Function(Object, StackTrace?) _onError; static const int _maxReconcileChunk = 65536; diff --git a/lib/chat/cubits/reconciliation/output_position.dart b/lib/chat/cubits/reconciliation/output_position.dart index 258259e..d983c95 100644 --- a/lib/chat/cubits/reconciliation/output_position.dart +++ b/lib/chat/cubits/reconciliation/output_position.dart @@ -6,7 +6,7 @@ import '../../../proto/proto.dart' as proto; @immutable class OutputPosition extends Equatable { const OutputPosition(this.message, this.pos); - final proto.Message message; + final proto.ReconciledMessage message; final int pos; @override List get props => [message, pos]; diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index d2abb22..bc2d8c5 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -22,14 +22,17 @@ class RenderStateElement { if (!isLocal) { return null; } - - if (sent && !sentOffline) { + if (reconciledTimestamp != null) { return MessageSendState.delivered; } - if (reconciledTimestamp != null) { - return MessageSendState.sent; + if (sent) { + if (!sentOffline) { + return MessageSendState.sent; + } else { + return MessageSendState.sending; + } } - return MessageSendState.sending; + return null; } proto.Message message; @@ -66,7 +69,7 @@ class SingleContactMessagesCubit extends Cubit { Future close() async { await _initWait(); - await _sendingMessagesQueue.close(); + await _unsentMessagesQueue.close(); await _sentSubscription?.cancel(); await _rcvdSubscription?.cancel(); await _reconciledSubscription?.cancel(); @@ -78,11 +81,11 @@ class SingleContactMessagesCubit extends Cubit { // Initialize everything Future _init() async { - _sendingMessagesQueue = PersistentQueue( - table: 'SingleContactSendingMessages', + _unsentMessagesQueue = PersistentQueue( + table: 'SingleContactUnsentMessages', key: _remoteConversationRecordKey.toString(), fromBuffer: proto.Message.fromBuffer, - closure: _processSendingMessages, + closure: _processUnsentMessages, ); // Make crypto @@ -144,13 +147,16 @@ class SingleContactMessagesCubit extends Cubit { // Open reconciled chat record key Future _initReconciledMessagesCubit() async { final tableName = - _localConversationRecordKey.toString().replaceAll(':', '_'); + _reconciledMessagesTableDBName(_localConversationRecordKey); final crypto = await _makeLocalMessagesCrypto(); - _reconciledMessagesCubit = TableDBArrayCubit( - open: () async => TableDBArray.make(table: tableName, crypto: crypto), - decodeElement: proto.ReconciledMessage.fromBuffer); + _reconciledMessagesCubit = TableDBArrayProtobufCubit( + open: () async => TableDBArrayProtobuf.make( + table: tableName, + crypto: crypto, + fromBuffer: proto.ReconciledMessage.fromBuffer), + ); _reconciliation = MessageReconciliation( output: _reconciledMessagesCubit!, @@ -200,6 +206,9 @@ class SingleContactMessagesCubit extends Cubit { _activeAccountInfo.localAccount.identityMaster.identityPublicTypedKey(), sentMessages, _sentMessagesCubit!); + + // Update the view + _renderState(); } // Called when the received messages cubit gets a change @@ -211,11 +220,14 @@ class SingleContactMessagesCubit extends Cubit { _reconciliation.reconcileMessages( _remoteIdentityPublicKey, rcvdMessages, _rcvdMessagesCubit!); + + // Update the view + _renderState(); } // Called when the reconciled messages window gets a change void _updateReconciledMessagesState( - TableDBArrayBusyState avmessages) { + TableDBArrayProtobufBusyState avmessages) { // Update the view _renderState(); } @@ -237,7 +249,7 @@ class SingleContactMessagesCubit extends Cubit { } // Async process to send messages in the background - Future _processSendingMessages(IList messages) async { + Future _processUnsentMessages(IList messages) async { // Go through and assign ids to all the messages in order proto.Message? previousMessage; final processedMessages = messages.toList(); @@ -258,7 +270,7 @@ class SingleContactMessagesCubit extends Cubit { // Get all sent messages final sentMessages = _sentMessagesCubit?.state.state.asData?.value; // Get all items in the unsent queue - final sendingMessages = _sendingMessagesQueue.queue; + // final unsentMessages = _unsentMessagesQueue.queue; // If we aren't ready to render a state, say we're loading if (reconciledMessages == null || sentMessages == null) { @@ -267,63 +279,49 @@ class SingleContactMessagesCubit extends Cubit { } // Generate state for each message + // final reconciledMessagesMap = + // IMap.fromValues( + // keyMapper: (x) => x.content.authorUniqueIdString, + // values: reconciledMessages.elements, + // ); final sentMessagesMap = IMap>.fromValues( keyMapper: (x) => x.value.authorUniqueIdString, - values: sentMessages.elements, - ); - final reconciledMessagesMap = - IMap.fromValues( - keyMapper: (x) => x.content.authorUniqueIdString, - values: reconciledMessages.elements, - ); - final sendingMessagesMap = IMap.fromValues( - keyMapper: (x) => x.authorUniqueIdString, - values: sendingMessages, + values: sentMessages.window, ); + // final unsentMessagesMap = IMap.fromValues( + // keyMapper: (x) => x.authorUniqueIdString, + // values: unsentMessages, + // ); - final renderedElements = {}; + final renderedElements = []; - for (final m in reconciledMessagesMap.entries) { - renderedElements[m.key] = RenderStateElement( - message: m.value.content, - isLocal: m.value.content.author.toVeilid() == - _activeAccountInfo.localAccount.identityMaster - .identityPublicTypedKey(), - reconciledTimestamp: Timestamp.fromInt64(m.value.reconciledTime), - ); - } - for (final m in sentMessagesMap.entries) { - renderedElements.putIfAbsent( - m.key, - () => RenderStateElement( - message: m.value.value, - isLocal: true, - )) - ..sent = true - ..sentOffline = m.value.isOffline; - } - for (final m in sendingMessagesMap.entries) { - renderedElements - .putIfAbsent( - m.key, - () => RenderStateElement( - message: m.value, - isLocal: true, - )) - .sent = false; + for (final m in reconciledMessages.elements) { + final isLocal = m.content.author.toVeilid() == + _activeAccountInfo.localAccount.identityMaster + .identityPublicTypedKey(); + 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, + )); } // Render the state - final messageKeys = renderedElements.entries - .toIList() - .sort((x, y) => x.key.compareTo(y.key)); - final renderedState = messageKeys + final renderedState = renderedElements .map((x) => MessageState( - content: x.value.message, - sentTimestamp: Timestamp.fromInt64(x.value.message.timestamp), - reconciledTimestamp: x.value.reconciledTimestamp, - sendState: x.value.sendState)) + content: x.message, + sentTimestamp: Timestamp.fromInt64(x.message.timestamp), + reconciledTimestamp: x.reconciledTimestamp, + sendState: x.sendState)) .toIList(); // Emit the rendered state @@ -340,7 +338,7 @@ class SingleContactMessagesCubit extends Cubit { ..timestamp = Veilid.instance.now().toInt64(); // Put in the queue - _sendingMessagesQueue.addSync(message); + _unsentMessagesQueue.addSync(message); // Update the view _renderState(); @@ -358,7 +356,7 @@ class SingleContactMessagesCubit extends Cubit { static String _reconciledMessagesTableDBName( TypedKey localConversationRecordKey) => - 'msg_$localConversationRecordKey'; + 'msg_${localConversationRecordKey.toString().replaceAll(':', '_')}'; ///////////////////////////////////////////////////////////////////////// @@ -375,14 +373,14 @@ class SingleContactMessagesCubit extends Cubit { DHTLogCubit? _sentMessagesCubit; DHTLogCubit? _rcvdMessagesCubit; - TableDBArrayCubit? _reconciledMessagesCubit; + TableDBArrayProtobufCubit? _reconciledMessagesCubit; late final MessageReconciliation _reconciliation; - late final PersistentQueue _sendingMessagesQueue; + late final PersistentQueue _unsentMessagesQueue; StreamSubscription>? _sentSubscription; StreamSubscription>? _rcvdSubscription; - StreamSubscription>? + StreamSubscription>? _reconciledSubscription; } diff --git a/lib/chat/models/message_state.freezed.dart b/lib/chat/models/message_state.freezed.dart index a99f937..96c98e2 100644 --- a/lib/chat/models/message_state.freezed.dart +++ b/lib/chat/models/message_state.freezed.dart @@ -21,7 +21,7 @@ MessageState _$MessageStateFromJson(Map json) { /// @nodoc mixin _$MessageState { // Content of the message - @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + @JsonKey(fromJson: messageFromJson, toJson: messageToJson) proto.Message get content => throw _privateConstructorUsedError; // Sent timestamp Timestamp get sentTimestamp => @@ -43,7 +43,7 @@ abstract class $MessageStateCopyWith<$Res> { _$MessageStateCopyWithImpl<$Res, MessageState>; @useResult $Res call( - {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) proto.Message content, Timestamp sentTimestamp, Timestamp? reconciledTimestamp, @@ -98,7 +98,7 @@ abstract class _$$MessageStateImplCopyWith<$Res> @override @useResult $Res call( - {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) proto.Message content, Timestamp sentTimestamp, Timestamp? reconciledTimestamp, @@ -146,7 +146,7 @@ class __$$MessageStateImplCopyWithImpl<$Res> @JsonSerializable() class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { const _$MessageStateImpl( - {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) required this.content, required this.sentTimestamp, required this.reconciledTimestamp, @@ -157,7 +157,7 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { // Content of the message @override - @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + @JsonKey(fromJson: messageFromJson, toJson: messageToJson) final proto.Message content; // Sent timestamp @override @@ -220,7 +220,7 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { abstract class _MessageState implements MessageState { const factory _MessageState( - {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) required final proto.Message content, required final Timestamp sentTimestamp, required final Timestamp? reconciledTimestamp, @@ -230,7 +230,7 @@ abstract class _MessageState implements MessageState { _$MessageStateImpl.fromJson; @override // Content of the message - @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + @JsonKey(fromJson: messageFromJson, toJson: messageToJson) proto.Message get content; @override // Sent timestamp Timestamp get sentTimestamp; diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 1e296e9..327b82e 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -271,18 +271,18 @@ class ChatComponent extends StatelessWidget { child: DecoratedBox( decoration: const BoxDecoration(), child: Chat( - theme: chatTheme, - // emojiEnlargementBehavior: - // EmojiEnlargementBehavior.multi, - messages: chatMessages, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, - onSendPressed: _handleSendPressed, - //showUserAvatars: false, - //showUserNames: true, - user: _localUser, - ), + theme: chatTheme, + // emojiEnlargementBehavior: + // EmojiEnlargementBehavior.multi, + messages: chatMessages, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, + onSendPressed: _handleSendPressed, + //showUserAvatars: false, + //showUserNames: true, + user: _localUser, + emptyState: const EmptyChatWidget()), ), ), ], diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart index 1b8545f..c19b535 100644 --- a/lib/chat/views/no_conversation_widget.dart +++ b/lib/chat/views/no_conversation_widget.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../theme/models/scale_scheme.dart'; class NoConversationWidget extends StatelessWidget { const NoConversationWidget({super.key}); @@ -7,28 +10,32 @@ class NoConversationWidget extends StatelessWidget { // ignore: prefer_expression_function_bodies Widget build( BuildContext context, - ) => - Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.emoji_people_outlined, - color: Theme.of(context).disabledColor, - size: 48, - ), - Text( - 'Choose A Conversation To Chat', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).disabledColor, - ), - ), - ], - ), - ); + ) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + return Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.diversity_3, + color: scale.primaryScale.subtleBorder, + size: 48, + ), + Text( + translate('chat.start_a_conversation'), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: scale.primaryScale.subtleBorder, + ), + ), + ], + ), + ); + } } diff --git a/lib/layout/home/home_account_ready/home_account_ready_chat.dart b/lib/layout/home/home_account_ready/home_account_ready_chat.dart index 087bb34..587828a 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_chat.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_chat.dart @@ -31,7 +31,7 @@ class HomeAccountReadyChatState extends State { final activeChatLocalConversationKey = context.watch().state; if (activeChatLocalConversationKey == null) { - return const EmptyChatWidget(); + return const NoConversationWidget(); } return ChatComponent.builder( localConversationRecordKey: activeChatLocalConversationKey); diff --git a/lib/layout/home/home_account_ready/home_account_ready_main.dart b/lib/layout/home/home_account_ready/home_account_ready_main.dart index 59eb00b..0a4b28e 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_main.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_main.dart @@ -69,7 +69,7 @@ class _HomeAccountReadyMainState extends State { final activeChatLocalConversationKey = context.watch().state; if (activeChatLocalConversationKey == null) { - return const EmptyChatWidget(); + return const NoConversationWidget(); } return ChatComponent.builder( localConversationRecordKey: activeChatLocalConversationKey); diff --git a/lib/main.dart b/lib/main.dart index ab3feed..d8bd6df 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:intl/date_symbol_data_local.dart'; +import 'package:stack_trace/stack_trace.dart'; import 'app.dart'; import 'settings/preferences_repository.dart'; @@ -52,7 +53,8 @@ void main() async { if (kDebugMode) { // In debug mode, run the app without catching exceptions for debugging - await mainFunc(); + // but do a much deeper async stack trace capture + await Chain.capture(mainFunc); } else { // Catch errors in production without killing the app await runZonedGuarded(mainFunc, (error, stackTrace) { diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index f70a34c..b5a728b 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -12,22 +12,25 @@ import '../../../veilid_support.dart'; @immutable class DHTLogStateData extends Equatable { const DHTLogStateData( - {required this.elements, - required this.tail, - required this.count, + {required this.length, + required this.window, + required this.windowTail, + required this.windowSize, required this.follow}); - // The view of the elements in the dhtlog - // Span is from [tail-length, tail) - final IList> elements; - // One past the end of the last element - final int tail; - // The total number of elements to try to keep in 'elements' - final int count; - // If we should have the tail following the log + // The total number of elements in the whole log + final int length; + // The view window of the elements in the dhtlog + // Span is from [tail - window.length, tail) + final IList> window; + // The position of the view window, one past the last element + final int windowTail; + // The total number of elements to try to keep in the window + final int windowSize; + // If we have the window following the log final bool follow; @override - List get props => [elements, tail, count, follow]; + List get props => [length, window, windowTail, windowSize, follow]; } typedef DHTLogState = AsyncValue>; @@ -58,13 +61,16 @@ class DHTLogCubit extends Cubit> // 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 setWindow( - {int? tail, int? count, bool? follow, bool forceRefresh = false}) async { + {int? windowTail, + int? windowSize, + bool? follow, + bool forceRefresh = false}) async { await _initWait(); - if (tail != null) { - _tail = tail; + if (windowTail != null) { + _windowTail = windowTail; } - if (count != null) { - _count = count; + if (windowSize != null) { + _windowSize = windowSize; } if (follow != null) { _follow = follow; @@ -82,8 +88,13 @@ class DHTLogCubit extends Cubit> Future _refreshInner(void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { - final avElements = await operate( - (reader) => loadElementsFromReader(reader, _tail, _count)); + late final AsyncValue>> avElements; + late final int length; + await _log.operate((reader) async { + length = reader.length; + avElements = + await loadElementsFromReader(reader, _windowTail, _windowSize); + }); final err = avElements.asError; if (err != null) { emit(AsyncValue.error(err.error, err.stackTrace)); @@ -94,9 +105,13 @@ class DHTLogCubit extends Cubit> emit(const AsyncValue.loading()); return; } - final elements = avElements.asData!.value; + final window = avElements.asData!.value; emit(AsyncValue.data(DHTLogStateData( - elements: elements, tail: _tail, count: _count, follow: _follow))); + length: length, + window: window, + windowTail: _windowTail, + windowSize: _windowSize, + follow: _follow))); } // Tail is one past the last element to load @@ -105,6 +120,9 @@ class DHTLogCubit extends Cubit> {bool forceRefresh = false}) async { try { final length = reader.length; + if (length == 0) { + return const AsyncValue.data(IList.empty()); + } final end = ((tail - 1) % length) + 1; final start = (count < end) ? end - count : 0; @@ -138,18 +156,18 @@ class DHTLogCubit extends Cubit> _sspUpdate.busyUpdate>(busy, (emit) async { // apply follow if (_follow) { - if (_tail <= 0) { + if (_windowTail <= 0) { // Negative tail is already following tail changes } else { // Positive tail is measured from the head, so apply deltas - _tail = (_tail + _tailDelta - _headDelta) % upd.length; + _windowTail = (_windowTail + _tailDelta - _headDelta) % upd.length; } } else { - if (_tail <= 0) { + if (_windowTail <= 0) { // Negative tail is following tail changes so apply deltas - var posTail = _tail + upd.length; + var posTail = _windowTail + upd.length; posTail = (posTail + _tailDelta - _headDelta) % upd.length; - _tail = posTail - upd.length; + _windowTail = posTail - upd.length; } else { // Positive tail is measured from head so not following tail } @@ -202,7 +220,7 @@ class DHTLogCubit extends Cubit> var _tailDelta = 0; // Cubit window into the DHTLog - var _tail = 0; - var _count = DHTShortArray.maxElements; + var _windowTail = 0; + var _windowSize = DHTShortArray.maxElements; var _follow = true; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index a47602a..6b5665d 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -451,48 +451,53 @@ class _DHTLogSpine { /////////////////////////////////////////// // API for public interfaces - Future<_DHTLogPosition?> lookupPosition(int pos) async { - assert(_spineMutex.isLocked, 'should be locked'); - return _spineCacheMutex.protect(() async { - // Check if our position is in bounds - final endPos = length; - if (pos < 0 || pos >= endPos) { - throw IndexError.withLength(pos, endPos); - } + Future<_DHTLogPosition?> lookupPositionBySegmentNumber( + int segmentNumber, int segmentPos) async => + _spineCacheMutex.protect(() async { + // Get the segment shortArray + final openedSegment = _openedSegments[segmentNumber]; + late final DHTShortArray shortArray; + if (openedSegment != null) { + openedSegment.openCount++; + shortArray = openedSegment.shortArray; + } else { + final newShortArray = (_spineRecord.writer == null) + ? await _openSegment(segmentNumber) + : await _openOrCreateSegment(segmentNumber); + if (newShortArray == null) { + return null; + } - // Calculate absolute position, ring-buffer style - final absolutePosition = (_head + pos) % _positionLimit; + _openedSegments[segmentNumber] = + _OpenedSegment._(shortArray: newShortArray); - // Determine the segment number and position within the segment - final segmentNumber = absolutePosition ~/ DHTShortArray.maxElements; - final segmentPos = absolutePosition % DHTShortArray.maxElements; - - // Get the segment shortArray - final openedSegment = _openedSegments[segmentNumber]; - late final DHTShortArray shortArray; - if (openedSegment != null) { - openedSegment.openCount++; - shortArray = openedSegment.shortArray; - } else { - final newShortArray = (_spineRecord.writer == null) - ? await _openSegment(segmentNumber) - : await _openOrCreateSegment(segmentNumber); - if (newShortArray == null) { - return null; + shortArray = newShortArray; } - _openedSegments[segmentNumber] = - _OpenedSegment._(shortArray: newShortArray); + return _DHTLogPosition._( + dhtLogSpine: this, + shortArray: shortArray, + pos: segmentPos, + segmentNumber: segmentNumber); + }); - shortArray = newShortArray; - } + Future<_DHTLogPosition?> lookupPosition(int pos) async { + assert(_spineMutex.isLocked, 'should be locked'); - return _DHTLogPosition._( - dhtLogSpine: this, - shortArray: shortArray, - pos: segmentPos, - segmentNumber: segmentNumber); - }); + // Check if our position is in bounds + final endPos = length; + if (pos < 0 || pos >= endPos) { + throw IndexError.withLength(pos, endPos); + } + + // Calculate absolute position, ring-buffer style + final absolutePosition = (_head + pos) % _positionLimit; + + // Determine the segment number and position within the segment + final segmentNumber = absolutePosition ~/ DHTShortArray.maxElements; + final segmentPos = absolutePosition % DHTShortArray.maxElements; + + return lookupPositionBySegmentNumber(segmentNumber, segmentPos); } Future _segmentClosed(int segmentNumber) async { @@ -660,6 +665,34 @@ class _DHTLogSpine { final oldHead = _head; final oldTail = _tail; await _updateHead(headData); + + // Lookup tail position segments that have changed + // and force their short arrays to refresh their heads + final segmentsToRefresh = <_DHTLogPosition>[]; + int? lastSegmentNumber; + for (var curTail = oldTail; + curTail != _tail; + curTail = (curTail + 1) % _positionLimit) { + final segmentNumber = curTail ~/ DHTShortArray.maxElements; + final segmentPos = curTail % DHTShortArray.maxElements; + if (segmentNumber == lastSegmentNumber) { + continue; + } + lastSegmentNumber = segmentNumber; + final dhtLogPosition = + await lookupPositionBySegmentNumber(segmentNumber, segmentPos); + if (dhtLogPosition == null) { + throw Exception('missing segment in dht log'); + } + segmentsToRefresh.add(dhtLogPosition); + } + + // Refresh the segments that have probably changed + await segmentsToRefresh.map((p) async { + await p.shortArray.refresh(); + await p.close(); + }).wait; + sendUpdate(oldHead, oldTail); }); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart index 0732255..a84f02d 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart @@ -185,6 +185,17 @@ class DHTShortArray implements DHTDeleteable { /// Get the record pointer foir this shortarray OwnedDHTRecordPointer get recordPointer => _head.recordPointer; + /// Refresh this DHTShortArray + /// Useful if you aren't 'watching' the array and want to poll for an update + Future refresh() async { + if (!isOpen) { + throw StateError('short array is not open"'); + } + await _head.operate((head) async { + await head._loadHead(); + }); + } + /// Runs a closure allowing read-only access to the shortarray Future operate( Future Function(DHTShortArrayReadOperations) closure) async { diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index 8bcd146..bc0eca5 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -23,8 +23,8 @@ class TableDBArrayUpdate extends Equatable { List get props => [headDelta, tailDelta, length]; } -class TableDBArray { - TableDBArray({ +class _TableDBArrayBase { + _TableDBArrayBase({ required String table, required VeilidCrypto crypto, }) : _table = table, @@ -32,14 +32,14 @@ class TableDBArray { _initWait.add(_init); } - static Future make({ - required String table, - required VeilidCrypto crypto, - }) async { - final out = TableDBArray(table: table, crypto: crypto); - await out._initWait(); - return out; - } + // static Future make({ + // required String table, + // required VeilidCrypto crypto, + // }) async { + // final out = TableDBArray(table: table, crypto: crypto); + // await out._initWait(); + // return out; + // } Future initWait() async { await _initWait(); @@ -99,27 +99,27 @@ class TableDBArray { bool get isOpen => _open; - Future add(Uint8List value) async { + Future _add(Uint8List value) async { await _initWait(); return _writeTransaction((t) async => _addInner(t, value)); } - Future addAll(List values) async { + Future _addAll(List values) async { await _initWait(); return _writeTransaction((t) async => _addAllInner(t, values)); } - Future insert(int pos, Uint8List value) async { + Future _insert(int pos, Uint8List value) async { await _initWait(); return _writeTransaction((t) async => _insertInner(t, pos, value)); } - Future insertAll(int pos, List values) async { + Future _insertAll(int pos, List values) async { await _initWait(); return _writeTransaction((t) async => _insertAllInner(t, pos, values)); } - Future get(int pos) async { + Future _get(int pos) async { await _initWait(); return _mutex.protect(() async { if (!_open) { @@ -129,7 +129,7 @@ class TableDBArray { }); } - Future> getRange(int start, [int? end]) async { + Future> _getRange(int start, [int? end]) async { await _initWait(); return _mutex.protect(() async { if (!_open) { @@ -139,12 +139,12 @@ class TableDBArray { }); } - Future remove(int pos, {Output? out}) async { + Future _remove(int pos, {Output? out}) async { await _initWait(); return _writeTransaction((t) async => _removeInner(t, pos, out: out)); } - Future removeRange(int start, int end, + Future _removeRange(int start, int end, {Output>? out}) async { await _initWait(); return _writeTransaction( @@ -374,7 +374,9 @@ class TableDBArray { Future _loadEntry(int entry) async { final encryptedValue = await _tableDB.load(0, _entryKey(entry)); - return (encryptedValue == null) ? null : _crypto.decrypt(encryptedValue); + return (encryptedValue == null) + ? null + : await _crypto.decrypt(encryptedValue); } Future _getIndexEntry(int pos) async { @@ -631,77 +633,170 @@ class TableDBArray { StreamController.broadcast(); } -extension TableDBArrayExt on TableDBArray { - /// Convenience function: - /// Like get but also parses the returned element as JSON - Future getJson( - T Function(dynamic) fromJson, +////////////////////////////////////////////////////////////////////////////// + +class TableDBArray extends _TableDBArrayBase { + TableDBArray({ + required super.table, + required super.crypto, + }); + + static Future make({ + required String table, + required VeilidCrypto crypto, + }) async { + final out = TableDBArray(table: table, crypto: crypto); + await out._initWait(); + return out; + } + + //////////////////////////////////////////////////////////// + // Public interface + + Future add(Uint8List value) => _add(value); + + Future addAll(List values) => _addAll(values); + + Future insert(int pos, Uint8List value) => _insert(pos, value); + + Future insertAll(int pos, List values) => + _insertAll(pos, values); + + Future get( int pos, ) => - get( - pos, - ).then((out) => jsonDecodeOptBytes(fromJson, out)); + _get(pos); - /// Convenience function: - /// Like getRange but also parses the returned elements as JSON - Future?> getRangeJson(T Function(dynamic) fromJson, int start, - [int? end]) => - getRange(start, end ?? _length).then((out) => out.map(fromJson).toList()); + Future> getRange(int start, [int? end]) => + _getRange(start, end); - /// Convenience function: - /// Like get but also parses the returned element as a protobuf object - Future getProtobuf( - T Function(List) fromBuffer, - int pos, - ) => - get(pos).then(fromBuffer); + Future remove(int pos, {Output? out}) => + _remove(pos, out: out); - /// Convenience function: - /// Like getRange but also parses the returned elements as protobuf objects - Future?> getRangeProtobuf( - T Function(List) fromBuffer, int start, [int? end]) => - getRange(start, end ?? _length) - .then((out) => out.map(fromBuffer).toList()); - - /// Convenience function: - /// Like add but for a JSON value - Future addJson(T value) async => add(jsonEncodeBytes(value)); - - /// Convenience function: - /// Like add but for a Protobuf value - Future addProtobuf(T value) => - add(value.writeToBuffer()); - - /// Convenience function: - /// Like addAll but for a JSON value - Future addAllJson(List values) async => - addAll(values.map(jsonEncodeBytes).toList()); - - /// Convenience function: - /// Like addAll but for a Protobuf value - Future addAllProtobuf( - List values) async => - addAll(values.map((x) => x.writeToBuffer()).toList()); - - /// Convenience function: - /// Like insert but for a JSON value - Future insertJson(int pos, T value) async => - insert(pos, jsonEncodeBytes(value)); - - /// Convenience function: - /// Like insert but for a Protobuf value - Future insertProtobuf( - int pos, T value) async => - insert(pos, value.writeToBuffer()); - - /// Convenience function: - /// Like insertAll but for a JSON value - Future insertAllJson(int pos, List values) async => - insertAll(pos, values.map(jsonEncodeBytes).toList()); - - /// Convenience function: - /// Like insertAll but for a Protobuf value - Future insertAllProtobuf( - int pos, List values) async => - insertAll(pos, values.map((x) => x.writeToBuffer()).toList()); + Future removeRange(int start, int end, + {Output>? out}) => + _removeRange(start, end, out: out); +} +////////////////////////////////////////////////////////////////////////////// + +class TableDBArrayJson extends _TableDBArrayBase { + TableDBArrayJson( + {required super.table, + required super.crypto, + required T Function(dynamic) fromJson}) + : _fromJson = fromJson; + + static Future> make( + {required String table, + required VeilidCrypto crypto, + required T Function(dynamic) fromJson}) async { + final out = + TableDBArrayJson(table: table, crypto: crypto, fromJson: fromJson); + await out._initWait(); + return out; + } + + //////////////////////////////////////////////////////////// + // Public interface + + Future add(T value) => _add(jsonEncodeBytes(value)); + + Future addAll(List values) async => + _addAll(values.map(jsonEncodeBytes).toList()); + + Future insert(int pos, T value) async => + _insert(pos, jsonEncodeBytes(value)); + + Future insertAll(int pos, List values) async => + _insertAll(pos, values.map(jsonEncodeBytes).toList()); + + Future get( + int pos, + ) => + _get(pos).then((out) => jsonDecodeOptBytes(_fromJson, out)); + + Future> getRange(int start, [int? end]) => + _getRange(start, end).then((out) => out.map(_fromJson).toList()); + + Future remove(int pos, {Output? out}) async { + final outJson = (out != null) ? Output() : null; + await _remove(pos, out: outJson); + if (outJson != null && outJson.value != null) { + out!.save(jsonDecodeBytes(_fromJson, outJson.value!)); + } + } + + Future removeRange(int start, int end, {Output>? out}) async { + final outJson = (out != null) ? Output>() : null; + await _removeRange(start, end, out: outJson); + if (outJson != null && outJson.value != null) { + out!.save( + outJson.value!.map((x) => jsonDecodeBytes(_fromJson, x)).toList()); + } + } + + //////////////////////////////////////////////////////////////////////////// + final T Function(dynamic) _fromJson; +} + +////////////////////////////////////////////////////////////////////////////// + +class TableDBArrayProtobuf + extends _TableDBArrayBase { + TableDBArrayProtobuf( + {required super.table, + required super.crypto, + required T Function(List) fromBuffer}) + : _fromBuffer = fromBuffer; + + static Future> make( + {required String table, + required VeilidCrypto crypto, + required T Function(List) fromBuffer}) async { + final out = TableDBArrayProtobuf( + table: table, crypto: crypto, fromBuffer: fromBuffer); + await out._initWait(); + return out; + } + + //////////////////////////////////////////////////////////// + // Public interface + + Future add(T value) => _add(value.writeToBuffer()); + + Future addAll(List values) async => + _addAll(values.map((x) => x.writeToBuffer()).toList()); + + Future insert(int pos, T value) async => + _insert(pos, value.writeToBuffer()); + + Future insertAll(int pos, List values) async => + _insertAll(pos, values.map((x) => x.writeToBuffer()).toList()); + + Future get( + int pos, + ) => + _get(pos).then(_fromBuffer); + + Future> getRange(int start, [int? end]) => + _getRange(start, end).then((out) => out.map(_fromBuffer).toList()); + + Future remove(int pos, {Output? out}) async { + final outProto = (out != null) ? Output() : null; + await _remove(pos, out: outProto); + if (outProto != null && outProto.value != null) { + out!.save(_fromBuffer(outProto.value!)); + } + } + + Future removeRange(int start, int end, {Output>? out}) async { + final outProto = (out != null) ? Output>() : null; + await _removeRange(start, end, out: outProto); + if (outProto != null && outProto.value != null) { + out!.save(outProto.value!.map(_fromBuffer).toList()); + } + } + + //////////////////////////////////////////////////////////////////////////// + final T Function(List) _fromBuffer; } diff --git a/packages/veilid_support/lib/src/table_db_array_cubit.dart b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart similarity index 81% rename from packages/veilid_support/lib/src/table_db_array_cubit.dart rename to packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart index c4ff507..927ca59 100644 --- a/packages/veilid_support/lib/src/table_db_array_cubit.dart +++ b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart @@ -6,12 +6,14 @@ import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:meta/meta.dart'; +import 'package:protobuf/protobuf.dart'; import '../../../veilid_support.dart'; @immutable -class TableDBArrayStateData extends Equatable { - const TableDBArrayStateData( +class TableDBArrayProtobufStateData + extends Equatable { + const TableDBArrayProtobufStateData( {required this.elements, required this.tail, required this.count, @@ -30,16 +32,17 @@ class TableDBArrayStateData extends Equatable { List get props => [elements, tail, count, follow]; } -typedef TableDBArrayState = AsyncValue>; -typedef TableDBArrayBusyState = BlocBusyState>; +typedef TableDBArrayProtobufState + = AsyncValue>; +typedef TableDBArrayProtobufBusyState + = BlocBusyState>; -class TableDBArrayCubit extends Cubit> - with BlocBusyWrapper> { - TableDBArrayCubit({ - required Future Function() open, - required T Function(List data) decodeElement, - }) : _decodeElement = decodeElement, - super(const BlocBusyState(AsyncValue.loading())) { +class TableDBArrayProtobufCubit + extends Cubit> + with BlocBusyWrapper> { + TableDBArrayProtobufCubit({ + required Future> Function() open, + }) : super(const BlocBusyState(AsyncValue.loading())) { _initWait.add(() async { // Open table db array _array = await open(); @@ -81,7 +84,7 @@ class TableDBArrayCubit extends Cubit> busy((emit) async => _refreshInner(emit, forceRefresh: forceRefresh)); Future _refreshInner( - void Function(AsyncValue>) emit, + void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { final avElements = await _loadElements(_tail, _count); final err = avElements.asError; @@ -95,7 +98,7 @@ class TableDBArrayCubit extends Cubit> return; } final elements = avElements.asData!.value; - emit(AsyncValue.data(TableDBArrayStateData( + emit(AsyncValue.data(TableDBArrayProtobufStateData( elements: elements, tail: _tail, count: _count, follow: _follow))); } @@ -110,8 +113,7 @@ class TableDBArrayCubit extends Cubit> } final end = ((tail - 1) % length) + 1; final start = (count < end) ? end - count : 0; - final allItems = - (await _array.getRange(start, end)).map(_decodeElement).toIList(); + final allItems = (await _array.getRange(start, end)).toIList(); return AsyncValue.data(allItems); } on Exception catch (e, st) { return AsyncValue.error(e, st); @@ -128,7 +130,7 @@ class TableDBArrayCubit extends Cubit> _headDelta += upd.headDelta; _tailDelta += upd.tailDelta; - _sspUpdate.busyUpdate>(busy, (emit) async { + _sspUpdate.busyUpdate>(busy, (emit) async { // apply follow if (_follow) { if (_tail <= 0) { @@ -165,14 +167,14 @@ class TableDBArrayCubit extends Cubit> await super.close(); } - Future operate(Future Function(TableDBArray) closure) async { + Future operate( + Future Function(TableDBArrayProtobuf) closure) async { await _initWait(); return closure(_array); } final WaitSet _initWait = WaitSet(); - late final TableDBArray _array; - final T Function(List data) _decodeElement; + late final TableDBArrayProtobuf _array; StreamSubscription? _subscription; bool _wantsCloseArray = false; final _sspUpdate = SingleStatelessProcessor(); diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index 0f19bf3..6d10049 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -16,6 +16,6 @@ export 'src/persistent_queue.dart'; export 'src/protobuf_tools.dart'; export 'src/table_db.dart'; export 'src/table_db_array.dart'; -export 'src/table_db_array_cubit.dart'; +export 'src/table_db_array_protobuf_cubit.dart'; export 'src/veilid_crypto.dart'; export 'src/veilid_log.dart' hide veilidLoggy; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index db70f07..f74ee28 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -36,10 +36,9 @@ packages: async_tools: dependency: "direct main" description: - name: async_tools - sha256: e783ac6ed5645c86da34240389bb3a000fc5e3ae6589c6a482eb24ece7217681 - url: "https://pub.dev" - source: hosted + path: "../../../dart_async_tools" + relative: true + source: path version: "0.1.1" bloc: dependency: "direct main" @@ -52,10 +51,9 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - name: bloc_advanced_tools - sha256: "09f8a121d950575f1f2980c8b10df46b2ac6c72c8cbe48cc145871e5882ed430" - url: "https://pub.dev" - source: hosted + path: "../../../bloc_advanced_tools" + relative: true + source: path version: "0.1.1" boolean_selector: dependency: transitive diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index e598f8c..eeab762 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -24,6 +24,12 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter +dependency_overrides: + async_tools: + path: ../../../dart_async_tools + bloc_advanced_tools: + path: ../../../bloc_advanced_tools + dev_dependencies: build_runner: ^2.4.10 freezed: ^2.5.2 From 4082d1dd767636039b405a487aa1e69146889d27 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 2 Jun 2024 12:53:52 -0400 Subject: [PATCH 14/19] bug cleanup --- .../reconciliation/author_input_queue.dart | 12 ++++--- .../message_reconciliation.dart | 36 +++++++++---------- .../src/dht_log/dht_log_spine.dart | 8 +++-- .../dht_short_array/dht_short_array_head.dart | 9 +++-- .../lib/src/table_db_array.dart | 6 ++-- 5 files changed, 41 insertions(+), 30 deletions(-) diff --git a/lib/chat/cubits/reconciliation/author_input_queue.dart b/lib/chat/cubits/reconciliation/author_input_queue.dart index 009604d..d7be3eb 100644 --- a/lib/chat/cubits/reconciliation/author_input_queue.dart +++ b/lib/chat/cubits/reconciliation/author_input_queue.dart @@ -109,7 +109,7 @@ class AuthorInputQueue { // Internal implementation // Walk backward from the tail of the input queue to find the first - // message newer than our last reconcicled message from this author + // message newer than our last reconciled message from this author // Returns false if no work is needed Future _findStartOfWork() async { // Iterate windows over the inputSource @@ -121,10 +121,14 @@ class AuthorInputQueue { i--, _currentPosition--) { final elem = _inputSource.currentWindow.elements[i]; - // If we've found an input element that is older than our last - // reconciled message for this author, then we stop + // If we've found an input element that is older or same time as our + // last reconciled message for this author, or we find the message + // itself then we stop if (_lastMessage != null) { - if (elem.value.timestamp < _lastMessage!.timestamp) { + if (elem.value.authorUniqueIdBytes + .compare(_lastMessage!.authorUniqueIdBytes) == + 0 || + elem.value.timestamp <= _lastMessage!.timestamp) { break outer; } } diff --git a/lib/chat/cubits/reconciliation/message_reconciliation.dart b/lib/chat/cubits/reconciliation/message_reconciliation.dart index aa53f49..f0b8c4c 100644 --- a/lib/chat/cubits/reconciliation/message_reconciliation.dart +++ b/lib/chat/cubits/reconciliation/message_reconciliation.dart @@ -75,8 +75,7 @@ class MessageReconciliation { return inputQueue; } - // Get the position of our most recent - // reconciled message from this author + // Get the position of our most recent reconciled message from this author // XXX: For a group chat, this should find when the author // was added to the membership so we don't just go back in time forever Future _findLastOutputPosition( @@ -85,9 +84,6 @@ class MessageReconciliation { var pos = arr.length - 1; while (pos >= 0) { final message = await arr.get(pos); - if (message == null) { - throw StateError('should have gotten last message'); - } if (message.content.author.toVeilid() == author) { return OutputPosition(message, pos); } @@ -120,13 +116,7 @@ class MessageReconciliation { }); // Start at the earliest position we know about in all the queues - final firstOutputPos = inputQueues.first.outputPosition?.pos; - // Get the timestamp for this output position - var currentOutputMessage = firstOutputPos == null - ? null - : await reconciledArray.get(firstOutputPos); - - var currentOutputPos = firstOutputPos ?? 0; + var currentOutputPosition = inputQueues.first.outputPosition; final toInsert = SortedList(proto.MessageExt.compareTimestamp); @@ -141,8 +131,9 @@ class MessageReconciliation { var someQueueEmpty = false; for (final inputQueue in inputQueues) { final inputCurrent = inputQueue.current!; - if (currentOutputMessage == null || - inputCurrent.timestamp < currentOutputMessage.content.timestamp) { + if (currentOutputPosition == null || + inputCurrent.timestamp < + currentOutputPosition.message.content.timestamp) { toInsert.add(inputCurrent); added = true; @@ -174,15 +165,22 @@ class MessageReconciliation { ..content = message) .toList(); - await reconciledArray.insertAll(currentOutputPos, reconciledInserts); + await reconciledArray.insertAll( + currentOutputPosition?.pos ?? reconciledArray.length, + reconciledInserts); toInsert.clear(); } else { // If there's nothing to insert at this position move to the next one - currentOutputPos++; - currentOutputMessage = (currentOutputPos == reconciledArray.length) - ? null - : await reconciledArray.get(currentOutputPos); + final nextOutputPos = (currentOutputPosition != null) + ? currentOutputPosition.pos + 1 + : reconciledArray.length; + if (nextOutputPos == reconciledArray.length) { + currentOutputPosition = null; + } else { + currentOutputPosition = OutputPosition( + await reconciledArray.get(nextOutputPos), nextOutputPos); + } } } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 6b5665d..bad7f80 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -200,8 +200,12 @@ class _DHTLogSpine { throw TimeoutException('timeout reached'); } } - if (await closure(this)) { - break; + try { + if (await closure(this)) { + break; + } + } on DHTExceptionTryAgain { + // } // Failed to write in closure resets state _head = oldHead; diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart index 501892d..68c2a18 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart @@ -139,9 +139,14 @@ class _DHTShortArrayHead { throw TimeoutException('timeout reached'); } } - if (await closure(this)) { - break; + try { + if (await closure(this)) { + break; + } + } on DHTExceptionTryAgain { + // } + // Failed to write in closure resets state _linkedRecords = List.of(oldLinkedRecords); _index = List.of(oldIndex); diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index bc0eca5..ad4c586 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -710,10 +710,10 @@ class TableDBArrayJson extends _TableDBArrayBase { Future insertAll(int pos, List values) async => _insertAll(pos, values.map(jsonEncodeBytes).toList()); - Future get( + Future get( int pos, ) => - _get(pos).then((out) => jsonDecodeOptBytes(_fromJson, out)); + _get(pos).then((out) => jsonDecodeBytes(_fromJson, out)); Future> getRange(int start, [int? end]) => _getRange(start, end).then((out) => out.map(_fromJson).toList()); @@ -773,7 +773,7 @@ class TableDBArrayProtobuf Future insertAll(int pos, List values) async => _insertAll(pos, values.map((x) => x.writeToBuffer()).toList()); - Future get( + Future get( int pos, ) => _get(pos).then(_fromBuffer); From 5473bd2ee40043ae5169bee2e0a207d339e88323 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 3 Jun 2024 21:20:00 -0400 Subject: [PATCH 15/19] pagination work --- .../cubits/single_contact_messages_cubit.dart | 60 +++- lib/chat/models/messages_state.dart | 27 ++ lib/chat/models/messages_state.freezed.dart | 268 ++++++++++++++++++ lib/chat/models/messages_state.g.dart | 28 ++ lib/chat/models/models.dart | 1 + lib/chat/views/chat_component.dart | 42 ++- lib/theme/models/radix_generator.dart | 23 ++ lib/tools/misc.dart | 18 ++ lib/tools/state_logger.dart | 3 +- lib/tools/tools.dart | 1 + .../src/table_db_array_protobuf_cubit.dart | 22 +- 11 files changed, 469 insertions(+), 24 deletions(-) create mode 100644 lib/chat/models/messages_state.dart create mode 100644 lib/chat/models/messages_state.freezed.dart create mode 100644 lib/chat/models/messages_state.g.dart create mode 100644 lib/tools/misc.dart diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index bc2d8c5..9ee68f7 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.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; +import '../../tools/tools.dart'; import '../models/models.dart'; import 'reconciliation/reconciliation.dart'; @@ -42,7 +44,7 @@ class RenderStateElement { bool sentOffline; } -typedef SingleContactMessagesState = AsyncValue>; +typedef SingleContactMessagesState = AsyncValue; // Cubit that processes single-contact chats // Builds the reconciled chat record from the local and remote conversation keys @@ -60,6 +62,7 @@ class SingleContactMessagesCubit extends Cubit { _localMessagesRecordKey = localMessagesRecordKey, _remoteConversationRecordKey = remoteConversationRecordKey, _remoteMessagesRecordKey = remoteMessagesRecordKey, + _commandController = StreamController(), super(const AsyncValue.loading()) { // Async Init _initWait.add(_init); @@ -69,6 +72,8 @@ class SingleContactMessagesCubit extends Cubit { Future close() async { await _initWait(); + await _commandController.close(); + await _commandRunnerFut; await _unsentMessagesQueue.close(); await _sentSubscription?.cancel(); await _rcvdSubscription?.cancel(); @@ -99,6 +104,9 @@ class SingleContactMessagesCubit extends Cubit { // Remote messages key await _initRcvdMessagesCubit(); + + // Command execution background process + _commandRunnerFut = Future.delayed(Duration.zero, _commandRunner); } // Make crypto @@ -191,6 +199,34 @@ class SingleContactMessagesCubit extends Cubit { _sendMessage(message: message); } + // 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.delayed(const Duration(milliseconds: 50)); + } + }); + } + //////////////////////////////////////////////////////////////////////////// // Internal implementation @@ -220,9 +256,6 @@ class SingleContactMessagesCubit extends Cubit { _reconciliation.reconcileMessages( _remoteIdentityPublicKey, rcvdMessages, _rcvdMessagesCubit!); - - // Update the view - _renderState(); } // Called when the reconciled messages window gets a change @@ -296,7 +329,7 @@ class SingleContactMessagesCubit extends Cubit { final renderedElements = []; - for (final m in reconciledMessages.elements) { + for (final m in reconciledMessages.windowElements) { final isLocal = m.content.author.toVeilid() == _activeAccountInfo.localAccount.identityMaster .identityPublicTypedKey(); @@ -316,7 +349,7 @@ class SingleContactMessagesCubit extends Cubit { } // Render the state - final renderedState = renderedElements + final messages = renderedElements .map((x) => MessageState( content: x.message, sentTimestamp: Timestamp.fromInt64(x.message.timestamp), @@ -325,7 +358,12 @@ class SingleContactMessagesCubit extends Cubit { .toIList(); // Emit the rendered state - emit(AsyncValue.data(renderedState)); + emit(AsyncValue.data(MessagesState( + windowMessages: messages, + length: reconciledMessages.length, + windowTail: reconciledMessages.windowTail, + windowCount: reconciledMessages.windowCount, + follow: reconciledMessages.follow))); } void _sendMessage({required proto.Message message}) { @@ -344,6 +382,12 @@ class SingleContactMessagesCubit extends Cubit { _renderState(); } + Future _commandRunner() async { + await for (final command in _commandController.stream) { + await command(); + } + } + ///////////////////////////////////////////////////////////////////////// // Static utility functions @@ -383,4 +427,6 @@ class SingleContactMessagesCubit extends Cubit { StreamSubscription>? _rcvdSubscription; StreamSubscription>? _reconciledSubscription; + final StreamController Function()> _commandController; + late final Future _commandRunnerFut; } diff --git a/lib/chat/models/messages_state.dart b/lib/chat/models/messages_state.dart new file mode 100644 index 0000000..4a08376 --- /dev/null +++ b/lib/chat/models/messages_state.dart @@ -0,0 +1,27 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'message_state.dart'; + +part 'messages_state.freezed.dart'; +part 'messages_state.g.dart'; + +@freezed +class MessagesState with _$MessagesState { + const factory MessagesState({ + // List of messages in the window + required IList windowMessages, + // Total number of messages + required int length, + // One past the end of the last element + required int windowTail, + // The total number of elements to try to keep in 'messages' + required int windowCount, + // If we should have the tail following the array + required bool follow, + }) = _MessagesState; + + factory MessagesState.fromJson(dynamic json) => + _$MessagesStateFromJson(json as Map); +} diff --git a/lib/chat/models/messages_state.freezed.dart b/lib/chat/models/messages_state.freezed.dart new file mode 100644 index 0000000..368ca94 --- /dev/null +++ b/lib/chat/models/messages_state.freezed.dart @@ -0,0 +1,268 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'messages_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +MessagesState _$MessagesStateFromJson(Map json) { + return _MessagesState.fromJson(json); +} + +/// @nodoc +mixin _$MessagesState { +// List of messages in the window + IList get windowMessages => + throw _privateConstructorUsedError; // Total number of messages + int get length => + throw _privateConstructorUsedError; // One past the end of the last element + int get windowTail => + throw _privateConstructorUsedError; // The total number of elements to try to keep in 'messages' + int get windowCount => + throw _privateConstructorUsedError; // If we should have the tail following the array + bool get follow => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MessagesStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MessagesStateCopyWith<$Res> { + factory $MessagesStateCopyWith( + MessagesState value, $Res Function(MessagesState) then) = + _$MessagesStateCopyWithImpl<$Res, MessagesState>; + @useResult + $Res call( + {IList windowMessages, + int length, + int windowTail, + int windowCount, + bool follow}); +} + +/// @nodoc +class _$MessagesStateCopyWithImpl<$Res, $Val extends MessagesState> + implements $MessagesStateCopyWith<$Res> { + _$MessagesStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? windowMessages = null, + Object? length = null, + Object? windowTail = null, + Object? windowCount = null, + Object? follow = null, + }) { + return _then(_value.copyWith( + windowMessages: null == windowMessages + ? _value.windowMessages + : windowMessages // ignore: cast_nullable_to_non_nullable + as IList, + length: null == length + ? _value.length + : length // ignore: cast_nullable_to_non_nullable + as int, + windowTail: null == windowTail + ? _value.windowTail + : windowTail // ignore: cast_nullable_to_non_nullable + as int, + windowCount: null == windowCount + ? _value.windowCount + : windowCount // ignore: cast_nullable_to_non_nullable + as int, + follow: null == follow + ? _value.follow + : follow // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MessagesStateImplCopyWith<$Res> + implements $MessagesStateCopyWith<$Res> { + factory _$$MessagesStateImplCopyWith( + _$MessagesStateImpl value, $Res Function(_$MessagesStateImpl) then) = + __$$MessagesStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {IList windowMessages, + int length, + int windowTail, + int windowCount, + bool follow}); +} + +/// @nodoc +class __$$MessagesStateImplCopyWithImpl<$Res> + extends _$MessagesStateCopyWithImpl<$Res, _$MessagesStateImpl> + implements _$$MessagesStateImplCopyWith<$Res> { + __$$MessagesStateImplCopyWithImpl( + _$MessagesStateImpl _value, $Res Function(_$MessagesStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? windowMessages = null, + Object? length = null, + Object? windowTail = null, + Object? windowCount = null, + Object? follow = null, + }) { + return _then(_$MessagesStateImpl( + windowMessages: null == windowMessages + ? _value.windowMessages + : windowMessages // ignore: cast_nullable_to_non_nullable + as IList, + length: null == length + ? _value.length + : length // ignore: cast_nullable_to_non_nullable + as int, + windowTail: null == windowTail + ? _value.windowTail + : windowTail // ignore: cast_nullable_to_non_nullable + as int, + windowCount: null == windowCount + ? _value.windowCount + : windowCount // ignore: cast_nullable_to_non_nullable + as int, + follow: null == follow + ? _value.follow + : follow // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MessagesStateImpl + with DiagnosticableTreeMixin + implements _MessagesState { + const _$MessagesStateImpl( + {required this.windowMessages, + required this.length, + required this.windowTail, + required this.windowCount, + required this.follow}); + + factory _$MessagesStateImpl.fromJson(Map json) => + _$$MessagesStateImplFromJson(json); + +// List of messages in the window + @override + final IList windowMessages; +// Total number of messages + @override + final int length; +// One past the end of the last element + @override + final int windowTail; +// The total number of elements to try to keep in 'messages' + @override + final int windowCount; +// If we should have the tail following the array + @override + final bool follow; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'MessagesState(windowMessages: $windowMessages, length: $length, windowTail: $windowTail, windowCount: $windowCount, follow: $follow)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'MessagesState')) + ..add(DiagnosticsProperty('windowMessages', windowMessages)) + ..add(DiagnosticsProperty('length', length)) + ..add(DiagnosticsProperty('windowTail', windowTail)) + ..add(DiagnosticsProperty('windowCount', windowCount)) + ..add(DiagnosticsProperty('follow', follow)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MessagesStateImpl && + const DeepCollectionEquality() + .equals(other.windowMessages, windowMessages) && + (identical(other.length, length) || other.length == length) && + (identical(other.windowTail, windowTail) || + other.windowTail == windowTail) && + (identical(other.windowCount, windowCount) || + other.windowCount == windowCount) && + (identical(other.follow, follow) || other.follow == follow)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(windowMessages), + length, + windowTail, + windowCount, + follow); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MessagesStateImplCopyWith<_$MessagesStateImpl> get copyWith => + __$$MessagesStateImplCopyWithImpl<_$MessagesStateImpl>(this, _$identity); + + @override + Map toJson() { + return _$$MessagesStateImplToJson( + this, + ); + } +} + +abstract class _MessagesState implements MessagesState { + const factory _MessagesState( + {required final IList windowMessages, + required final int length, + required final int windowTail, + required final int windowCount, + required final bool follow}) = _$MessagesStateImpl; + + factory _MessagesState.fromJson(Map json) = + _$MessagesStateImpl.fromJson; + + @override // List of messages in the window + IList get windowMessages; + @override // Total number of messages + int get length; + @override // One past the end of the last element + int get windowTail; + @override // The total number of elements to try to keep in 'messages' + int get windowCount; + @override // If we should have the tail following the array + bool get follow; + @override + @JsonKey(ignore: true) + _$$MessagesStateImplCopyWith<_$MessagesStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/chat/models/messages_state.g.dart b/lib/chat/models/messages_state.g.dart new file mode 100644 index 0000000..cf44e5b --- /dev/null +++ b/lib/chat/models/messages_state.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'messages_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$MessagesStateImpl _$$MessagesStateImplFromJson(Map json) => + _$MessagesStateImpl( + windowMessages: IList.fromJson( + json['window_messages'], (value) => MessageState.fromJson(value)), + length: (json['length'] as num).toInt(), + windowTail: (json['window_tail'] as num).toInt(), + windowCount: (json['window_count'] as num).toInt(), + follow: json['follow'] as bool, + ); + +Map _$$MessagesStateImplToJson(_$MessagesStateImpl instance) => + { + 'window_messages': instance.windowMessages.toJson( + (value) => value.toJson(), + ), + 'length': instance.length, + 'window_tail': instance.windowTail, + 'window_count': instance.windowCount, + 'follow': instance.follow, + }; diff --git a/lib/chat/models/models.dart b/lib/chat/models/models.dart index 2d92e01..3620563 100644 --- a/lib/chat/models/models.dart +++ b/lib/chat/models/models.dart @@ -1 +1,2 @@ export 'message_state.dart'; +export 'messages_state.dart'; diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 327b82e..ee339d3 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -1,8 +1,9 @@ -import 'dart:typed_data'; +import 'dart:math'; import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fixnum/fixnum.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; @@ -166,8 +167,9 @@ class ChatComponent extends StatelessWidget { _messagesCubit.sendTextMessage(messageText: protoMessageText); } - void _handleSendPressed(types.PartialText message) { + void _sendMessage(types.PartialText message) { final text = message.text; + final replyId = (message.repliedMessage != null) ? base64UrlNoPadDecode(message.repliedMessage!.id) : null; @@ -200,6 +202,17 @@ class ChatComponent extends StatelessWidget { attachments: attachments ?? []); } + void _handleSendPressed(types.PartialText message) { + final text = message.text; + + if (text.startsWith('/')) { + _messagesCubit.runCommand(text); + return; + } + + _sendMessage(message); + } + // void _handleAttachmentPressed() async { // // // } @@ -211,15 +224,15 @@ class ChatComponent extends StatelessWidget { final textTheme = Theme.of(context).textTheme; final chatTheme = makeChatTheme(scale, textTheme); - final messages = _messagesState.asData?.value; - if (messages == null) { + final messagesState = _messagesState.asData?.value; + if (messagesState == null) { return _messagesState.buildNotData(); } // Convert protobuf messages to chat messages final chatMessages = []; final tsSet = {}; - for (final message in messages) { + for (final message in messagesState.windowMessages) { final chatMessage = messageStateToChatMessage(message); if (chatMessage == null) { continue; @@ -228,12 +241,17 @@ class ChatComponent extends StatelessWidget { if (!tsSet.add(chatMessage.id)) { // ignore: avoid_print print('duplicate id found: ${chatMessage.id}:\n' - 'Messages:\n$messages\n' + 'Messages:\n${messagesState.windowMessages}\n' 'ChatMessages:\n$chatMessages'); assert(false, 'should not have duplicate id'); } } + final isLastPage = + (messagesState.windowTail - messagesState.windowMessages.length) <= 0; + final follow = messagesState.windowTail == 0 || + messagesState.windowTail == messagesState.length; xxx finish calculating pagination and get scroll position here somehow + return DefaultTextStyle( style: textTheme.bodySmall!, child: Align( @@ -272,9 +290,17 @@ class ChatComponent extends StatelessWidget { decoration: const BoxDecoration(), child: Chat( theme: chatTheme, - // emojiEnlargementBehavior: - // EmojiEnlargementBehavior.multi, messages: chatMessages, + onEndReached: () async { + final tail = await _messagesCubit.setWindow( + tail: max( + 0, + (messagesState.windowTail - + (messagesState.windowCount ~/ 2))), + count: messagesState.windowCount, + follow: follow); + }, + isLastPage: isLastPage, //onAttachmentPressed: _handleAttachmentPressed, //onMessageTap: _handleMessageTap, //onPreviewDataFetched: _handlePreviewDataFetched, diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index b1f510a..4bd593f 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -609,6 +609,29 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { final themeData = ThemeData.from( colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); return themeData.copyWith( + scrollbarTheme: themeData.scrollbarTheme.copyWith( + thumbColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return scaleScheme.primaryScale.border; + } else if (states.contains(WidgetState.hovered)) { + return scaleScheme.primaryScale.hoverBorder; + } + return scaleScheme.primaryScale.subtleBorder; + }), trackColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return scaleScheme.primaryScale.activeElementBackground; + } else if (states.contains(WidgetState.hovered)) { + return scaleScheme.primaryScale.hoverElementBackground; + } + return scaleScheme.primaryScale.elementBackground; + }), trackBorderColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return scaleScheme.primaryScale.subtleBorder; + } else if (states.contains(WidgetState.hovered)) { + return scaleScheme.primaryScale.subtleBorder; + } + return scaleScheme.primaryScale.subtleBorder; + })), bottomSheetTheme: themeData.bottomSheetTheme.copyWith( elevation: 0, modalElevation: 0, diff --git a/lib/tools/misc.dart b/lib/tools/misc.dart new file mode 100644 index 0000000..01dcbc0 --- /dev/null +++ b/lib/tools/misc.dart @@ -0,0 +1,18 @@ +extension StringExt on String { + (String, String?) splitOnce(Pattern p) { + final pos = indexOf(p); + if (pos == -1) { + return (this, null); + } + final rest = substring(pos); + var offset = 0; + while (true) { + final match = p.matchAsPrefix(rest, offset); + if (match == null) { + break; + } + offset = match.end; + } + return (substring(0, pos), rest.substring(offset)); + } +} diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index 4c8e17a..db0ea2a 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -6,8 +6,9 @@ const Map _blocChangeLogLevels = { 'ConnectionStateCubit': LogLevel.off, 'ActiveSingleContactChatBlocMapCubit': LogLevel.off, 'ActiveConversationsBlocMapCubit': LogLevel.off, - 'DHTShortArrayCubit': LogLevel.off, 'PersistentQueueCubit': LogLevel.off, + 'TableDBArrayProtobufCubit': LogLevel.off, + 'DHTLogCubit': LogLevel.off, 'SingleContactMessagesCubit': LogLevel.off, }; const Map _blocCreateCloseLogLevels = {}; diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index c556f98..6b48001 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -2,6 +2,7 @@ export 'animations.dart'; export 'enter_password.dart'; export 'enter_pin.dart'; export 'loggy.dart'; +export 'misc.dart'; export 'phono_byte.dart'; export 'pop_control.dart'; export 'responsive.dart'; diff --git a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart index 927ca59..702a2ad 100644 --- a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart +++ b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart @@ -14,22 +14,25 @@ import '../../../veilid_support.dart'; class TableDBArrayProtobufStateData extends Equatable { const TableDBArrayProtobufStateData( - {required this.elements, - required this.tail, - required this.count, + {required this.windowElements, + required this.length, + required this.windowTail, + required this.windowCount, required this.follow}); // The view of the elements in the dhtlog // Span is from [tail-length, tail) - final IList elements; + final IList windowElements; + // The length of the entire array + final int length; // One past the end of the last element - final int tail; + final int windowTail; // The total number of elements to try to keep in 'elements' - final int count; + final int windowCount; // If we should have the tail following the array final bool follow; @override - List get props => [elements, tail, count, follow]; + List get props => [windowElements, windowTail, windowCount, follow]; } typedef TableDBArrayProtobufState @@ -99,7 +102,10 @@ class TableDBArrayProtobufCubit } final elements = avElements.asData!.value; emit(AsyncValue.data(TableDBArrayProtobufStateData( - elements: elements, tail: _tail, count: _count, follow: _follow))); + windowElements: elements, + windowTail: _tail, + windowCount: _count, + follow: _follow))); } Future>> _loadElements( From 2c38fc64894a74efa312bc549dbb237378fc3538 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 6 Jun 2024 00:19:07 -0400 Subject: [PATCH 16/19] pagination --- .../models/active_account_info.dart | 2 - lib/chat/cubits/active_chat_cubit.dart | 3 + lib/chat/cubits/chat_component_cubit.dart | 272 +++++++++++++++ lib/chat/cubits/cubits.dart | 1 + .../cubits/single_contact_messages_cubit.dart | 9 +- lib/chat/models/chat_component_state.dart | 34 ++ .../models/chat_component_state.freezed.dart | 267 +++++++++++++++ lib/chat/models/messages_state.dart | 27 -- lib/chat/models/messages_state.g.dart | 28 -- lib/chat/models/models.dart | 3 +- lib/chat/models/window_state.dart | 27 ++ ...freezed.dart => window_state.freezed.dart} | 147 ++++---- lib/chat/views/chat_component.dart | 320 ------------------ lib/chat/views/chat_component_widget.dart | 294 ++++++++++++++++ lib/chat/views/views.dart | 2 +- .../home_account_ready_chat.dart | 5 +- .../home_account_ready_main.dart | 15 +- lib/tools/state_logger.dart | 2 + .../src/dht_log/dht_log_write.dart | 22 +- .../src/table_db_array_protobuf_cubit.dart | 1 + pubspec.lock | 95 +++--- pubspec.yaml | 40 ++- 22 files changed, 1071 insertions(+), 545 deletions(-) create mode 100644 lib/chat/cubits/chat_component_cubit.dart create mode 100644 lib/chat/models/chat_component_state.dart create mode 100644 lib/chat/models/chat_component_state.freezed.dart delete mode 100644 lib/chat/models/messages_state.dart delete mode 100644 lib/chat/models/messages_state.g.dart create mode 100644 lib/chat/models/window_state.dart rename lib/chat/models/{messages_state.freezed.dart => window_state.freezed.dart} (59%) delete mode 100644 lib/chat/views/chat_component.dart create mode 100644 lib/chat/views/chat_component_widget.dart diff --git a/lib/account_manager/models/active_account_info.dart b/lib/account_manager/models/active_account_info.dart index e4a5beb..0e1a0ef 100644 --- a/lib/account_manager/models/active_account_info.dart +++ b/lib/account_manager/models/active_account_info.dart @@ -11,7 +11,6 @@ class ActiveAccountInfo { const ActiveAccountInfo({ required this.localAccount, required this.userLogin, - //required this.accountRecord, }); // @@ -41,5 +40,4 @@ class ActiveAccountInfo { // final LocalAccount localAccount; final UserLogin userLogin; - //final DHTRecord accountRecord; } diff --git a/lib/chat/cubits/active_chat_cubit.dart b/lib/chat/cubits/active_chat_cubit.dart index a1872c2..2e72abc 100644 --- a/lib/chat/cubits/active_chat_cubit.dart +++ b/lib/chat/cubits/active_chat_cubit.dart @@ -1,6 +1,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:veilid_support/veilid_support.dart'; +// XXX: if we ever want to have more than one chat 'open', we should put the +// operations and state for that here. + class ActiveChatCubit extends Cubit { ActiveChatCubit(super.initialState); diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart new file mode 100644 index 0000000..6ac2726 --- /dev/null +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -0,0 +1,272 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart' as types; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../chat_list/chat_list.dart'; +import '../../proto/proto.dart' as proto; +import '../models/chat_component_state.dart'; +import '../models/message_state.dart'; +import '../models/window_state.dart'; +import 'cubits.dart'; + +const metadataKeyIdentityPublicKey = 'identityPublicKey'; +const metadataKeyExpirationDuration = 'expiration'; +const metadataKeyViewLimit = 'view_limit'; +const metadataKeyAttachments = 'attachments'; + +class ChatComponentCubit extends Cubit { + ChatComponentCubit._({ + required SingleContactMessagesCubit messagesCubit, + required types.User localUser, + required IMap remoteUsers, + }) : _messagesCubit = messagesCubit, + super(ChatComponentState( + chatKey: GlobalKey(), + scrollController: AutoScrollController(), + localUser: localUser, + remoteUsers: remoteUsers, + messageWindow: const AsyncLoading(), + title: '', + )) { + // Async Init + _initWait.add(_init); + } + + // ignore: prefer_constructors_over_static_methods + static ChatComponentCubit singleContact( + {required ActiveAccountInfo activeAccountInfo, + required proto.Account accountRecordInfo, + required ActiveConversationState activeConversationState, + required SingleContactMessagesCubit messagesCubit}) { + // Make local 'User' + final localUserIdentityKey = + activeAccountInfo.localAccount.identityMaster.identityPublicTypedKey(); + final localUser = types.User( + id: localUserIdentityKey.toString(), + firstName: accountRecordInfo.profile.name, + metadata: {metadataKeyIdentityPublicKey: localUserIdentityKey}); + // Make remote 'User's + final remoteUsers = { + activeConversationState.contact.identityPublicKey.toVeilid(): types.User( + id: activeConversationState.contact.identityPublicKey + .toVeilid() + .toString(), + firstName: activeConversationState.contact.editedProfile.name, + metadata: { + metadataKeyIdentityPublicKey: + activeConversationState.contact.identityPublicKey.toVeilid() + }) + }.toIMap(); + + return ChatComponentCubit._( + messagesCubit: messagesCubit, + localUser: localUser, + remoteUsers: remoteUsers, + ); + } + + Future _init() async { + _messagesSubscription = _messagesCubit.stream.listen((messagesState) { + emit(state.copyWith( + messageWindow: _convertMessages(messagesState), + )); + }); + emit(state.copyWith( + messageWindow: _convertMessages(_messagesCubit.state), + title: _getTitle(), + )); + } + + @override + Future close() async { + await _initWait(); + await _messagesSubscription.cancel(); + await super.close(); + } + + //////////////////////////////////////////////////////////////////////////// + // Public Interface + + // 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 setWindow( + {int? tail, int? count, bool? follow, bool forceRefresh = false}) async { + //await _initWait(); + await _messagesCubit.setWindow( + tail: tail, count: count, follow: follow, forceRefresh: forceRefresh); + } + + // Send a message + void sendMessage(types.PartialText message) { + final text = message.text; + + final replyId = (message.repliedMessage != null) + ? base64UrlNoPadDecode(message.repliedMessage!.id) + : null; + Timestamp? expiration; + int? viewLimit; + List? attachments; + final metadata = message.metadata; + if (metadata != null) { + final expirationValue = + metadata[metadataKeyExpirationDuration] as TimestampDuration?; + if (expirationValue != null) { + expiration = Veilid.instance.now().offset(expirationValue); + } + final viewLimitValue = metadata[metadataKeyViewLimit] as int?; + if (viewLimitValue != null) { + viewLimit = viewLimitValue; + } + final attachmentsValue = + metadata[metadataKeyAttachments] as List?; + if (attachmentsValue != null) { + attachments = attachmentsValue; + } + } + + _addTextMessage( + text: text, + replyId: replyId, + expiration: expiration, + viewLimit: viewLimit, + attachments: attachments ?? []); + } + + // Run a chat command + void runCommand(String command) { + _messagesCubit.runCommand(command); + } + + //////////////////////////////////////////////////////////////////////////// + // Private Implementation + + String _getTitle() { + if (state.remoteUsers.length == 1) { + final remoteUser = state.remoteUsers.values.first; + return remoteUser.firstName ?? ''; + } else { + return ''; + } + } + + types.Message? _messageStateToChatMessage(MessageState message) { + final authorIdentityPublicKey = message.content.author.toVeilid(); + final author = + state.remoteUsers[authorIdentityPublicKey] ?? state.localUser; + + types.Status? status; + if (message.sendState != null) { + assert(author == state.localUser, + 'send state should only be on sent messages'); + switch (message.sendState!) { + case MessageSendState.sending: + status = types.Status.sending; + case MessageSendState.sent: + status = types.Status.sent; + case MessageSendState.delivered: + status = types.Status.delivered; + } + } + + switch (message.content.whichKind()) { + case proto.Message_Kind.text: + final contextText = message.content.text; + final textMessage = types.TextMessage( + author: author, + createdAt: + (message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(), + id: message.content.authorUniqueIdString, + text: contextText.text, + showStatus: status != null, + status: status); + return textMessage; + case proto.Message_Kind.secret: + case proto.Message_Kind.delete: + case proto.Message_Kind.erase: + case proto.Message_Kind.settings: + case proto.Message_Kind.permissions: + case proto.Message_Kind.membership: + case proto.Message_Kind.moderation: + case proto.Message_Kind.notSet: + return null; + } + } + + AsyncValue> _convertMessages( + AsyncValue> avMessagesState) { + final asError = avMessagesState.asError; + if (asError != null) { + return AsyncValue.error(asError.error, asError.stackTrace); + } else if (avMessagesState.asLoading != null) { + return const AsyncValue.loading(); + } + final messagesState = avMessagesState.asData!.value; + + // Convert protobuf messages to chat messages + final chatMessages = []; + final tsSet = {}; + for (final message in messagesState.window) { + final chatMessage = _messageStateToChatMessage(message); + if (chatMessage == null) { + continue; + } + chatMessages.insert(0, chatMessage); + if (!tsSet.add(chatMessage.id)) { + // ignore: avoid_print + print('duplicate id found: ${chatMessage.id}:\n' + 'Messages:\n${messagesState.window}\n' + 'ChatMessages:\n$chatMessages'); + assert(false, 'should not have duplicate id'); + } + } + return AsyncValue.data(WindowState( + window: chatMessages.toIList(), + length: messagesState.length, + windowTail: messagesState.windowTail, + windowCount: messagesState.windowCount, + follow: messagesState.follow)); + } + + void _addTextMessage( + {required String text, + String? topic, + Uint8List? replyId, + Timestamp? expiration, + int? viewLimit, + List attachments = const []}) { + final protoMessageText = proto.Message_Text()..text = text; + if (topic != null) { + protoMessageText.topic = topic; + } + if (replyId != null) { + protoMessageText.replyId = replyId; + } + protoMessageText + ..expiration = expiration?.toInt64() ?? Int64.ZERO + ..viewLimit = viewLimit ?? 0; + protoMessageText.attachments.addAll(attachments); + + _messagesCubit.sendTextMessage(messageText: protoMessageText); + } + + //////////////////////////////////////////////////////////////////////////// + + final _initWait = WaitSet(); + final SingleContactMessagesCubit _messagesCubit; + late StreamSubscription _messagesSubscription; + double scrollOffset = 0; +} diff --git a/lib/chat/cubits/cubits.dart b/lib/chat/cubits/cubits.dart index b80767f..ae6b95d 100644 --- a/lib/chat/cubits/cubits.dart +++ b/lib/chat/cubits/cubits.dart @@ -1,2 +1,3 @@ export 'active_chat_cubit.dart'; +export 'chat_component_cubit.dart'; export 'single_contact_messages_cubit.dart'; diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 9ee68f7..3fc8e91 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -44,7 +44,7 @@ class RenderStateElement { bool sentOffline; } -typedef SingleContactMessagesState = AsyncValue; +typedef SingleContactMessagesState = AsyncValue>; // Cubit that processes single-contact chats // Builds the reconciled chat record from the local and remote conversation keys @@ -189,6 +189,9 @@ class SingleContactMessagesCubit extends Cubit { Future setWindow( {int? tail, int? count, bool? follow, bool forceRefresh = false}) async { await _initWait(); + + print('setWindow: tail=$tail count=$count, follow=$follow'); + await _reconciledMessagesCubit!.setWindow( tail: tail, count: count, follow: follow, forceRefresh: forceRefresh); } @@ -358,8 +361,8 @@ class SingleContactMessagesCubit extends Cubit { .toIList(); // Emit the rendered state - emit(AsyncValue.data(MessagesState( - windowMessages: messages, + emit(AsyncValue.data(WindowState( + window: messages, length: reconciledMessages.length, windowTail: reconciledMessages.windowTail, windowCount: reconciledMessages.windowCount, diff --git a/lib/chat/models/chat_component_state.dart b/lib/chat/models/chat_component_state.dart new file mode 100644 index 0000000..b8da8d4 --- /dev/null +++ b/lib/chat/models/chat_component_state.dart @@ -0,0 +1,34 @@ +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart' show Message, User; +import 'package:flutter_chat_ui/flutter_chat_ui.dart' show ChatState; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import 'window_state.dart'; + +part 'chat_component_state.freezed.dart'; + +@freezed +class ChatComponentState with _$ChatComponentState { + const factory ChatComponentState( + { + // GlobalKey for the chat + required GlobalKey chatKey, + // ScrollController for the chat + required AutoScrollController scrollController, + // Local user + required User localUser, + // Remote users + required IMap remoteUsers, + // Messages state + required AsyncValue> messageWindow, + // Title of the chat + required String title}) = _ChatComponentState; +} + +extension ChatComponentStateExt on ChatComponentState { + // +} diff --git a/lib/chat/models/chat_component_state.freezed.dart b/lib/chat/models/chat_component_state.freezed.dart new file mode 100644 index 0000000..859f363 --- /dev/null +++ b/lib/chat/models/chat_component_state.freezed.dart @@ -0,0 +1,267 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'chat_component_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ChatComponentState { +// GlobalKey for the chat + GlobalKey get chatKey => + throw _privateConstructorUsedError; // ScrollController for the chat + AutoScrollController get scrollController => + throw _privateConstructorUsedError; // Local user + User get localUser => throw _privateConstructorUsedError; // Remote users + IMap, User> get remoteUsers => + throw _privateConstructorUsedError; // Messages state + AsyncValue> get messageWindow => + throw _privateConstructorUsedError; // Title of the chat + String get title => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ChatComponentStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChatComponentStateCopyWith<$Res> { + factory $ChatComponentStateCopyWith( + ChatComponentState value, $Res Function(ChatComponentState) then) = + _$ChatComponentStateCopyWithImpl<$Res, ChatComponentState>; + @useResult + $Res call( + {GlobalKey chatKey, + AutoScrollController scrollController, + User localUser, + IMap, User> remoteUsers, + AsyncValue> messageWindow, + String title}); + + $AsyncValueCopyWith, $Res> get messageWindow; +} + +/// @nodoc +class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState> + implements $ChatComponentStateCopyWith<$Res> { + _$ChatComponentStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? chatKey = null, + Object? scrollController = null, + Object? localUser = null, + Object? remoteUsers = null, + Object? messageWindow = null, + Object? title = null, + }) { + return _then(_value.copyWith( + chatKey: null == chatKey + ? _value.chatKey + : chatKey // ignore: cast_nullable_to_non_nullable + as GlobalKey, + scrollController: null == scrollController + ? _value.scrollController + : scrollController // ignore: cast_nullable_to_non_nullable + as AutoScrollController, + localUser: null == localUser + ? _value.localUser + : localUser // ignore: cast_nullable_to_non_nullable + as User, + remoteUsers: null == remoteUsers + ? _value.remoteUsers + : remoteUsers // ignore: cast_nullable_to_non_nullable + as IMap, User>, + messageWindow: null == messageWindow + ? _value.messageWindow + : messageWindow // ignore: cast_nullable_to_non_nullable + as AsyncValue>, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $AsyncValueCopyWith, $Res> get messageWindow { + return $AsyncValueCopyWith, $Res>(_value.messageWindow, + (value) { + return _then(_value.copyWith(messageWindow: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$ChatComponentStateImplCopyWith<$Res> + implements $ChatComponentStateCopyWith<$Res> { + factory _$$ChatComponentStateImplCopyWith(_$ChatComponentStateImpl value, + $Res Function(_$ChatComponentStateImpl) then) = + __$$ChatComponentStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {GlobalKey chatKey, + AutoScrollController scrollController, + User localUser, + IMap, User> remoteUsers, + AsyncValue> messageWindow, + String title}); + + @override + $AsyncValueCopyWith, $Res> get messageWindow; +} + +/// @nodoc +class __$$ChatComponentStateImplCopyWithImpl<$Res> + extends _$ChatComponentStateCopyWithImpl<$Res, _$ChatComponentStateImpl> + implements _$$ChatComponentStateImplCopyWith<$Res> { + __$$ChatComponentStateImplCopyWithImpl(_$ChatComponentStateImpl _value, + $Res Function(_$ChatComponentStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? chatKey = null, + Object? scrollController = null, + Object? localUser = null, + Object? remoteUsers = null, + Object? messageWindow = null, + Object? title = null, + }) { + return _then(_$ChatComponentStateImpl( + chatKey: null == chatKey + ? _value.chatKey + : chatKey // ignore: cast_nullable_to_non_nullable + as GlobalKey, + scrollController: null == scrollController + ? _value.scrollController + : scrollController // ignore: cast_nullable_to_non_nullable + as AutoScrollController, + localUser: null == localUser + ? _value.localUser + : localUser // ignore: cast_nullable_to_non_nullable + as User, + remoteUsers: null == remoteUsers + ? _value.remoteUsers + : remoteUsers // ignore: cast_nullable_to_non_nullable + as IMap, User>, + messageWindow: null == messageWindow + ? _value.messageWindow + : messageWindow // ignore: cast_nullable_to_non_nullable + as AsyncValue>, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$ChatComponentStateImpl implements _ChatComponentState { + const _$ChatComponentStateImpl( + {required this.chatKey, + required this.scrollController, + required this.localUser, + required this.remoteUsers, + required this.messageWindow, + required this.title}); + +// GlobalKey for the chat + @override + final GlobalKey chatKey; +// ScrollController for the chat + @override + final AutoScrollController scrollController; +// Local user + @override + final User localUser; +// Remote users + @override + final IMap, User> remoteUsers; +// Messages state + @override + final AsyncValue> messageWindow; +// Title of the chat + @override + final String title; + + @override + String toString() { + return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, localUser: $localUser, remoteUsers: $remoteUsers, messageWindow: $messageWindow, title: $title)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChatComponentStateImpl && + (identical(other.chatKey, chatKey) || other.chatKey == chatKey) && + (identical(other.scrollController, scrollController) || + other.scrollController == scrollController) && + (identical(other.localUser, localUser) || + other.localUser == localUser) && + (identical(other.remoteUsers, remoteUsers) || + other.remoteUsers == remoteUsers) && + (identical(other.messageWindow, messageWindow) || + other.messageWindow == messageWindow) && + (identical(other.title, title) || other.title == title)); + } + + @override + int get hashCode => Object.hash(runtimeType, chatKey, scrollController, + localUser, remoteUsers, messageWindow, title); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ChatComponentStateImplCopyWith<_$ChatComponentStateImpl> get copyWith => + __$$ChatComponentStateImplCopyWithImpl<_$ChatComponentStateImpl>( + this, _$identity); +} + +abstract class _ChatComponentState implements ChatComponentState { + const factory _ChatComponentState( + {required final GlobalKey chatKey, + required final AutoScrollController scrollController, + required final User localUser, + required final IMap, User> remoteUsers, + required final AsyncValue> messageWindow, + required final String title}) = _$ChatComponentStateImpl; + + @override // GlobalKey for the chat + GlobalKey get chatKey; + @override // ScrollController for the chat + AutoScrollController get scrollController; + @override // Local user + User get localUser; + @override // Remote users + IMap, User> get remoteUsers; + @override // Messages state + AsyncValue> get messageWindow; + @override // Title of the chat + String get title; + @override + @JsonKey(ignore: true) + _$$ChatComponentStateImplCopyWith<_$ChatComponentStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/chat/models/messages_state.dart b/lib/chat/models/messages_state.dart deleted file mode 100644 index 4a08376..0000000 --- a/lib/chat/models/messages_state.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'message_state.dart'; - -part 'messages_state.freezed.dart'; -part 'messages_state.g.dart'; - -@freezed -class MessagesState with _$MessagesState { - const factory MessagesState({ - // List of messages in the window - required IList windowMessages, - // Total number of messages - required int length, - // One past the end of the last element - required int windowTail, - // The total number of elements to try to keep in 'messages' - required int windowCount, - // If we should have the tail following the array - required bool follow, - }) = _MessagesState; - - factory MessagesState.fromJson(dynamic json) => - _$MessagesStateFromJson(json as Map); -} diff --git a/lib/chat/models/messages_state.g.dart b/lib/chat/models/messages_state.g.dart deleted file mode 100644 index cf44e5b..0000000 --- a/lib/chat/models/messages_state.g.dart +++ /dev/null @@ -1,28 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'messages_state.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$MessagesStateImpl _$$MessagesStateImplFromJson(Map json) => - _$MessagesStateImpl( - windowMessages: IList.fromJson( - json['window_messages'], (value) => MessageState.fromJson(value)), - length: (json['length'] as num).toInt(), - windowTail: (json['window_tail'] as num).toInt(), - windowCount: (json['window_count'] as num).toInt(), - follow: json['follow'] as bool, - ); - -Map _$$MessagesStateImplToJson(_$MessagesStateImpl instance) => - { - 'window_messages': instance.windowMessages.toJson( - (value) => value.toJson(), - ), - 'length': instance.length, - 'window_tail': instance.windowTail, - 'window_count': instance.windowCount, - 'follow': instance.follow, - }; diff --git a/lib/chat/models/models.dart b/lib/chat/models/models.dart index 3620563..30698cd 100644 --- a/lib/chat/models/models.dart +++ b/lib/chat/models/models.dart @@ -1,2 +1,3 @@ +export 'chat_component_state.dart'; export 'message_state.dart'; -export 'messages_state.dart'; +export 'window_state.dart'; diff --git a/lib/chat/models/window_state.dart b/lib/chat/models/window_state.dart new file mode 100644 index 0000000..91cde8a --- /dev/null +++ b/lib/chat/models/window_state.dart @@ -0,0 +1,27 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'window_state.freezed.dart'; + +@freezed +class WindowState with _$WindowState { + const factory WindowState({ + // List of objects in the window + required IList window, + // Total number of objects (windowTail max) + required int length, + // One past the end of the last element + required int windowTail, + // The total number of elements to try to keep in the window + required int windowCount, + // If we should have the tail following the array + required bool follow, + }) = _WindowState; +} + +extension WindowStateExt on WindowState { + int get windowEnd => (length == 0) ? -1 : (windowTail - 1) % length; + int get windowStart => + (length == 0) ? 0 : (windowTail - window.length) % length; +} diff --git a/lib/chat/models/messages_state.freezed.dart b/lib/chat/models/window_state.freezed.dart similarity index 59% rename from lib/chat/models/messages_state.freezed.dart rename to lib/chat/models/window_state.freezed.dart index 368ca94..604931d 100644 --- a/lib/chat/models/messages_state.freezed.dart +++ b/lib/chat/models/window_state.freezed.dart @@ -3,7 +3,7 @@ // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark -part of 'messages_state.dart'; +part of 'window_state.dart'; // ************************************************************************** // FreezedGenerator @@ -14,37 +14,32 @@ T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); -MessagesState _$MessagesStateFromJson(Map json) { - return _MessagesState.fromJson(json); -} - /// @nodoc -mixin _$MessagesState { -// List of messages in the window - IList get windowMessages => - throw _privateConstructorUsedError; // Total number of messages +mixin _$WindowState { +// List of objects in the window + IList get window => + throw _privateConstructorUsedError; // Total number of objects (windowTail max) int get length => throw _privateConstructorUsedError; // One past the end of the last element int get windowTail => - throw _privateConstructorUsedError; // The total number of elements to try to keep in 'messages' + throw _privateConstructorUsedError; // The total number of elements to try to keep in the window int get windowCount => throw _privateConstructorUsedError; // If we should have the tail following the array bool get follow => throw _privateConstructorUsedError; - Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $MessagesStateCopyWith get copyWith => + $WindowStateCopyWith> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class $MessagesStateCopyWith<$Res> { - factory $MessagesStateCopyWith( - MessagesState value, $Res Function(MessagesState) then) = - _$MessagesStateCopyWithImpl<$Res, MessagesState>; +abstract class $WindowStateCopyWith { + factory $WindowStateCopyWith( + WindowState value, $Res Function(WindowState) then) = + _$WindowStateCopyWithImpl>; @useResult $Res call( - {IList windowMessages, + {IList window, int length, int windowTail, int windowCount, @@ -52,9 +47,9 @@ abstract class $MessagesStateCopyWith<$Res> { } /// @nodoc -class _$MessagesStateCopyWithImpl<$Res, $Val extends MessagesState> - implements $MessagesStateCopyWith<$Res> { - _$MessagesStateCopyWithImpl(this._value, this._then); +class _$WindowStateCopyWithImpl> + implements $WindowStateCopyWith { + _$WindowStateCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; @@ -64,17 +59,17 @@ class _$MessagesStateCopyWithImpl<$Res, $Val extends MessagesState> @pragma('vm:prefer-inline') @override $Res call({ - Object? windowMessages = null, + Object? window = null, Object? length = null, Object? windowTail = null, Object? windowCount = null, Object? follow = null, }) { return _then(_value.copyWith( - windowMessages: null == windowMessages - ? _value.windowMessages - : windowMessages // ignore: cast_nullable_to_non_nullable - as IList, + window: null == window + ? _value.window + : window // ignore: cast_nullable_to_non_nullable + as IList, length: null == length ? _value.length : length // ignore: cast_nullable_to_non_nullable @@ -96,15 +91,15 @@ class _$MessagesStateCopyWithImpl<$Res, $Val extends MessagesState> } /// @nodoc -abstract class _$$MessagesStateImplCopyWith<$Res> - implements $MessagesStateCopyWith<$Res> { - factory _$$MessagesStateImplCopyWith( - _$MessagesStateImpl value, $Res Function(_$MessagesStateImpl) then) = - __$$MessagesStateImplCopyWithImpl<$Res>; +abstract class _$$WindowStateImplCopyWith + implements $WindowStateCopyWith { + factory _$$WindowStateImplCopyWith(_$WindowStateImpl value, + $Res Function(_$WindowStateImpl) then) = + __$$WindowStateImplCopyWithImpl; @override @useResult $Res call( - {IList windowMessages, + {IList window, int length, int windowTail, int windowCount, @@ -112,27 +107,27 @@ abstract class _$$MessagesStateImplCopyWith<$Res> } /// @nodoc -class __$$MessagesStateImplCopyWithImpl<$Res> - extends _$MessagesStateCopyWithImpl<$Res, _$MessagesStateImpl> - implements _$$MessagesStateImplCopyWith<$Res> { - __$$MessagesStateImplCopyWithImpl( - _$MessagesStateImpl _value, $Res Function(_$MessagesStateImpl) _then) +class __$$WindowStateImplCopyWithImpl + extends _$WindowStateCopyWithImpl> + implements _$$WindowStateImplCopyWith { + __$$WindowStateImplCopyWithImpl( + _$WindowStateImpl _value, $Res Function(_$WindowStateImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @override $Res call({ - Object? windowMessages = null, + Object? window = null, Object? length = null, Object? windowTail = null, Object? windowCount = null, Object? follow = null, }) { - return _then(_$MessagesStateImpl( - windowMessages: null == windowMessages - ? _value.windowMessages - : windowMessages // ignore: cast_nullable_to_non_nullable - as IList, + return _then(_$WindowStateImpl( + window: null == window + ? _value.window + : window // ignore: cast_nullable_to_non_nullable + as IList, length: null == length ? _value.length : length // ignore: cast_nullable_to_non_nullable @@ -154,30 +149,27 @@ class __$$MessagesStateImplCopyWithImpl<$Res> } /// @nodoc -@JsonSerializable() -class _$MessagesStateImpl + +class _$WindowStateImpl with DiagnosticableTreeMixin - implements _MessagesState { - const _$MessagesStateImpl( - {required this.windowMessages, + implements _WindowState { + const _$WindowStateImpl( + {required this.window, required this.length, required this.windowTail, required this.windowCount, required this.follow}); - factory _$MessagesStateImpl.fromJson(Map json) => - _$$MessagesStateImplFromJson(json); - -// List of messages in the window +// List of objects in the window @override - final IList windowMessages; -// Total number of messages + final IList window; +// Total number of objects (windowTail max) @override final int length; // One past the end of the last element @override final int windowTail; -// The total number of elements to try to keep in 'messages' +// The total number of elements to try to keep in the window @override final int windowCount; // If we should have the tail following the array @@ -186,15 +178,15 @@ class _$MessagesStateImpl @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'MessagesState(windowMessages: $windowMessages, length: $length, windowTail: $windowTail, windowCount: $windowCount, follow: $follow)'; + return 'WindowState<$T>(window: $window, length: $length, windowTail: $windowTail, windowCount: $windowCount, follow: $follow)'; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('type', 'MessagesState')) - ..add(DiagnosticsProperty('windowMessages', windowMessages)) + ..add(DiagnosticsProperty('type', 'WindowState<$T>')) + ..add(DiagnosticsProperty('window', window)) ..add(DiagnosticsProperty('length', length)) ..add(DiagnosticsProperty('windowTail', windowTail)) ..add(DiagnosticsProperty('windowCount', windowCount)) @@ -205,9 +197,8 @@ class _$MessagesStateImpl bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$MessagesStateImpl && - const DeepCollectionEquality() - .equals(other.windowMessages, windowMessages) && + other is _$WindowStateImpl && + const DeepCollectionEquality().equals(other.window, window) && (identical(other.length, length) || other.length == length) && (identical(other.windowTail, windowTail) || other.windowTail == windowTail) && @@ -216,11 +207,10 @@ class _$MessagesStateImpl (identical(other.follow, follow) || other.follow == follow)); } - @JsonKey(ignore: true) @override int get hashCode => Object.hash( runtimeType, - const DeepCollectionEquality().hash(windowMessages), + const DeepCollectionEquality().hash(window), length, windowTail, windowCount, @@ -229,40 +219,31 @@ class _$MessagesStateImpl @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$MessagesStateImplCopyWith<_$MessagesStateImpl> get copyWith => - __$$MessagesStateImplCopyWithImpl<_$MessagesStateImpl>(this, _$identity); - - @override - Map toJson() { - return _$$MessagesStateImplToJson( - this, - ); - } + _$$WindowStateImplCopyWith> get copyWith => + __$$WindowStateImplCopyWithImpl>( + this, _$identity); } -abstract class _MessagesState implements MessagesState { - const factory _MessagesState( - {required final IList windowMessages, +abstract class _WindowState implements WindowState { + const factory _WindowState( + {required final IList window, required final int length, required final int windowTail, required final int windowCount, - required final bool follow}) = _$MessagesStateImpl; + required final bool follow}) = _$WindowStateImpl; - factory _MessagesState.fromJson(Map json) = - _$MessagesStateImpl.fromJson; - - @override // List of messages in the window - IList get windowMessages; - @override // Total number of messages + @override // List of objects in the window + IList get window; + @override // Total number of objects (windowTail max) int get length; @override // One past the end of the last element int get windowTail; - @override // The total number of elements to try to keep in 'messages' + @override // The total number of elements to try to keep in the window int get windowCount; @override // If we should have the tail following the array bool get follow; @override @JsonKey(ignore: true) - _$$MessagesStateImplCopyWith<_$MessagesStateImpl> get copyWith => + _$$WindowStateImplCopyWith> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart deleted file mode 100644 index ee339d3..0000000 --- a/lib/chat/views/chat_component.dart +++ /dev/null @@ -1,320 +0,0 @@ -import 'dart:math'; - -import 'package:async_tools/async_tools.dart'; -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_types/flutter_chat_types.dart' as types; -import 'package:flutter_chat_ui/flutter_chat_ui.dart'; -import 'package:veilid_support/veilid_support.dart'; - -import '../../account_manager/account_manager.dart'; -import '../../chat_list/chat_list.dart'; -import '../../contacts/contacts.dart'; -import '../../proto/proto.dart' as proto; -import '../../theme/theme.dart'; -import '../chat.dart'; - -const String metadataKeyExpirationDuration = 'expiration'; -const String metadataKeyViewLimit = 'view_limit'; -const String metadataKeyAttachments = 'attachments'; - -class ChatComponent extends StatelessWidget { - const ChatComponent._( - {required TypedKey localUserIdentityKey, - required SingleContactMessagesCubit messagesCubit, - required SingleContactMessagesState messagesState, - required types.User localUser, - required types.User remoteUser, - super.key}) - : _localUserIdentityKey = localUserIdentityKey, - _messagesCubit = messagesCubit, - _messagesState = messagesState, - _localUser = localUser, - _remoteUser = remoteUser; - - final TypedKey _localUserIdentityKey; - final SingleContactMessagesCubit _messagesCubit; - final SingleContactMessagesState _messagesState; - final types.User _localUser; - final types.User _remoteUser; - - // Builder wrapper function that takes care of state management requirements - static Widget builder( - {required TypedKey localConversationRecordKey, Key? key}) => - Builder(builder: (context) { - // Get all watched dependendies - final activeAccountInfo = context.watch(); - final accountRecordInfo = - context.watch().state.asData?.value; - if (accountRecordInfo == null) { - return debugPage('should always have an account record here'); - } - final contactList = - context.watch().state.state.asData?.value; - if (contactList == null) { - return debugPage('should always have a contact list here'); - } - final avconversation = context.select?>( - (x) => x.state[localConversationRecordKey]); - if (avconversation == null) { - return waitingPage(); - } - final conversation = avconversation.asData?.value; - if (conversation == null) { - return avconversation.buildNotData(); - } - - // Make flutter_chat_ui 'User's - final localUserIdentityKey = activeAccountInfo - .localAccount.identityMaster - .identityPublicTypedKey(); - - final localUser = types.User( - id: localUserIdentityKey.toString(), - firstName: accountRecordInfo.profile.name, - ); - final editedName = conversation.contact.editedProfile.name; - final remoteUser = types.User( - id: conversation.contact.identityPublicKey.toVeilid().toString(), - firstName: editedName); - - // Get the messages cubit - final messages = context.select( - (x) => x.tryOperate(localConversationRecordKey, - closure: (cubit) => (cubit, cubit.state))); - - // Get the messages to display - // and ensure it is safe to operate() on the MessageCubit for this chat - if (messages == null) { - return waitingPage(); - } - - return ChatComponent._( - localUserIdentityKey: localUserIdentityKey, - messagesCubit: messages.$1, - messagesState: messages.$2, - localUser: localUser, - remoteUser: remoteUser, - key: key); - }); - - ///////////////////////////////////////////////////////////////////// - - types.Message? messageStateToChatMessage(MessageState message) { - final isLocal = message.content.author.toVeilid() == _localUserIdentityKey; - - types.Status? status; - if (message.sendState != null) { - assert(isLocal, 'send state should only be on sent messages'); - switch (message.sendState!) { - case MessageSendState.sending: - status = types.Status.sending; - case MessageSendState.sent: - status = types.Status.sent; - case MessageSendState.delivered: - status = types.Status.delivered; - } - } - - switch (message.content.whichKind()) { - case proto.Message_Kind.text: - final contextText = message.content.text; - final textMessage = types.TextMessage( - author: isLocal ? _localUser : _remoteUser, - createdAt: - (message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(), - id: message.content.authorUniqueIdString, - text: contextText.text, - showStatus: status != null, - status: status); - return textMessage; - case proto.Message_Kind.secret: - case proto.Message_Kind.delete: - case proto.Message_Kind.erase: - case proto.Message_Kind.settings: - case proto.Message_Kind.permissions: - case proto.Message_Kind.membership: - case proto.Message_Kind.moderation: - case proto.Message_Kind.notSet: - return null; - } - } - - void _addTextMessage( - {required String text, - String? topic, - Uint8List? replyId, - Timestamp? expiration, - int? viewLimit, - List attachments = const []}) { - final protoMessageText = proto.Message_Text()..text = text; - if (topic != null) { - protoMessageText.topic = topic; - } - if (replyId != null) { - protoMessageText.replyId = replyId; - } - protoMessageText - ..expiration = expiration?.toInt64() ?? Int64.ZERO - ..viewLimit = viewLimit ?? 0; - protoMessageText.attachments.addAll(attachments); - - _messagesCubit.sendTextMessage(messageText: protoMessageText); - } - - void _sendMessage(types.PartialText message) { - final text = message.text; - - final replyId = (message.repliedMessage != null) - ? base64UrlNoPadDecode(message.repliedMessage!.id) - : null; - Timestamp? expiration; - int? viewLimit; - List? attachments; - final metadata = message.metadata; - if (metadata != null) { - final expirationValue = - metadata[metadataKeyExpirationDuration] as TimestampDuration?; - if (expirationValue != null) { - expiration = Veilid.instance.now().offset(expirationValue); - } - final viewLimitValue = metadata[metadataKeyViewLimit] as int?; - if (viewLimitValue != null) { - viewLimit = viewLimitValue; - } - final attachmentsValue = - metadata[metadataKeyAttachments] as List?; - if (attachmentsValue != null) { - attachments = attachmentsValue; - } - } - - _addTextMessage( - text: text, - replyId: replyId, - expiration: expiration, - viewLimit: viewLimit, - attachments: attachments ?? []); - } - - void _handleSendPressed(types.PartialText message) { - final text = message.text; - - if (text.startsWith('/')) { - _messagesCubit.runCommand(text); - return; - } - - _sendMessage(message); - } - - // void _handleAttachmentPressed() async { - // // - // } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final scale = theme.extension()!; - final textTheme = Theme.of(context).textTheme; - final chatTheme = makeChatTheme(scale, textTheme); - - final messagesState = _messagesState.asData?.value; - if (messagesState == null) { - return _messagesState.buildNotData(); - } - - // Convert protobuf messages to chat messages - final chatMessages = []; - final tsSet = {}; - for (final message in messagesState.windowMessages) { - final chatMessage = messageStateToChatMessage(message); - if (chatMessage == null) { - continue; - } - chatMessages.insert(0, chatMessage); - if (!tsSet.add(chatMessage.id)) { - // ignore: avoid_print - print('duplicate id found: ${chatMessage.id}:\n' - 'Messages:\n${messagesState.windowMessages}\n' - 'ChatMessages:\n$chatMessages'); - assert(false, 'should not have duplicate id'); - } - } - - final isLastPage = - (messagesState.windowTail - messagesState.windowMessages.length) <= 0; - final follow = messagesState.windowTail == 0 || - messagesState.windowTail == messagesState.length; xxx finish calculating pagination and get scroll position here somehow - - return DefaultTextStyle( - style: textTheme.bodySmall!, - child: Align( - alignment: AlignmentDirectional.centerEnd, - child: Stack( - children: [ - Column( - children: [ - Container( - height: 48, - decoration: BoxDecoration( - color: scale.primaryScale.subtleBorder, - ), - child: Row(children: [ - Align( - alignment: AlignmentDirectional.centerStart, - child: Padding( - padding: const EdgeInsetsDirectional.fromSTEB( - 16, 0, 16, 0), - child: Text(_remoteUser.firstName!, - textAlign: TextAlign.start, - style: textTheme.titleMedium!.copyWith( - color: scale.primaryScale.borderText)), - )), - const Spacer(), - IconButton( - icon: Icon(Icons.close, - color: scale.primaryScale.borderText), - onPressed: () async { - context.read().setActiveChat(null); - }).paddingLTRB(16, 0, 16, 0) - ]), - ), - Expanded( - child: DecoratedBox( - decoration: const BoxDecoration(), - child: Chat( - theme: chatTheme, - messages: chatMessages, - onEndReached: () async { - final tail = await _messagesCubit.setWindow( - tail: max( - 0, - (messagesState.windowTail - - (messagesState.windowCount ~/ 2))), - count: messagesState.windowCount, - follow: follow); - }, - isLastPage: isLastPage, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, - onSendPressed: _handleSendPressed, - //showUserAvatars: false, - //showUserNames: true, - user: _localUser, - emptyState: const EmptyChatWidget()), - ), - ), - ], - ), - ], - ), - )); - } -} diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart new file mode 100644 index 0000000..a3b2e33 --- /dev/null +++ b/lib/chat/views/chat_component_widget.dart @@ -0,0 +1,294 @@ +import 'dart:math'; + +import 'package:async_tools/async_tools.dart'; +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart' as types; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../chat_list/chat_list.dart'; +import '../../theme/theme.dart'; +import '../chat.dart'; + +const onEndReachedThreshold = 0.75; + +class ChatComponentWidget extends StatelessWidget { + const ChatComponentWidget._({required super.key}); + + // Builder wrapper function that takes care of state management requirements + static Widget builder( + {required TypedKey localConversationRecordKey, Key? key}) => + Builder(builder: (context) { + // Get all watched dependendies + final activeAccountInfo = context.watch(); + final accountRecordInfo = + context.watch().state.asData?.value; + if (accountRecordInfo == null) { + return debugPage('should always have an account record here'); + } + + final avconversation = context.select?>( + (x) => x.state[localConversationRecordKey]); + if (avconversation == null) { + return waitingPage(); + } + + final activeConversationState = avconversation.asData?.value; + if (activeConversationState == null) { + return avconversation.buildNotData(); + } + + // Get the messages cubit + final messagesCubit = context.select< + ActiveSingleContactChatBlocMapCubit, + SingleContactMessagesCubit?>( + (x) => x.tryOperate(localConversationRecordKey, + closure: (cubit) => cubit)); + if (messagesCubit == null) { + return waitingPage(); + } + + // Make chat component state + return BlocProvider( + create: (context) => ChatComponentCubit.singleContact( + activeAccountInfo: activeAccountInfo, + accountRecordInfo: accountRecordInfo, + activeConversationState: activeConversationState, + messagesCubit: messagesCubit, + ), + child: ChatComponentWidget._(key: key)); + }); + + ///////////////////////////////////////////////////////////////////// + + void _handleSendPressed( + ChatComponentCubit chatComponentCubit, types.PartialText message) { + final text = message.text; + + if (text.startsWith('/')) { + chatComponentCubit.runCommand(text); + return; + } + + chatComponentCubit.sendMessage(message); + } + + // void _handleAttachmentPressed() async { + // // + // } + + Future _handlePageForward( + ChatComponentCubit chatComponentCubit, + WindowState messageWindow, + ScrollNotification notification) async { + print( + '_handlePageForward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); + + // Go forward a page + final tail = min(messageWindow.length, + messageWindow.windowTail + (messageWindow.windowCount ~/ 4)) % + messageWindow.length; + + // Set follow + final follow = messageWindow.length == 0 || + tail == 0; // xxx incorporate scroll position + + // final scrollOffset = (notification.metrics.maxScrollExtent - + // notification.metrics.minScrollExtent) * + // (1.0 - onEndReachedThreshold); + + // chatComponentCubit.scrollOffset = scrollOffset; + + await chatComponentCubit.setWindow( + tail: tail, count: messageWindow.windowCount, follow: follow); + + // chatComponentCubit.state.scrollController.position.jumpTo( + // chatComponentCubit.state.scrollController.offset + scrollOffset); + + //chatComponentCubit.scrollOffset = 0; + } + + Future _handlePageBackward( + ChatComponentCubit chatComponentCubit, + WindowState messageWindow, + ScrollNotification notification, + ) async { + print( + '_handlePageBackward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); + + // Go back a page + final tail = max( + messageWindow.windowCount, + (messageWindow.windowTail - (messageWindow.windowCount ~/ 4)) % + messageWindow.length); + + // Set follow + final follow = messageWindow.length == 0 || + tail == 0; // xxx incorporate scroll position + + // final scrollOffset = -(notification.metrics.maxScrollExtent - + // notification.metrics.minScrollExtent) * + // (1.0 - onEndReachedThreshold); + + // chatComponentCubit.scrollOffset = scrollOffset; + + await chatComponentCubit.setWindow( + tail: tail, count: messageWindow.windowCount, follow: follow); + + // chatComponentCubit.scrollOffset = scrollOffset; + + // chatComponentCubit.state.scrollController.position.jumpTo( + // chatComponentCubit.state.scrollController.offset + scrollOffset); + + //chatComponentCubit.scrollOffset = 0; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final textTheme = Theme.of(context).textTheme; + final chatTheme = makeChatTheme(scale, textTheme); + + // Get the enclosing chat component cubit that contains our state + // (created by ChatComponentWidget.builder()) + final chatComponentCubit = context.watch(); + final chatComponentState = chatComponentCubit.state; + + final messageWindow = chatComponentState.messageWindow.asData?.value; + if (messageWindow == null) { + return chatComponentState.messageWindow.buildNotData(); + } + final isLastPage = messageWindow.windowStart == 0; + final isFirstPage = messageWindow.windowEnd == messageWindow.length - 1; + final title = chatComponentState.title; + + if (chatComponentCubit.scrollOffset != 0) { + chatComponentState.scrollController.position.correctPixels( + chatComponentState.scrollController.position.pixels + + chatComponentCubit.scrollOffset); + + chatComponentCubit.scrollOffset = 0; + } + + return DefaultTextStyle( + style: textTheme.bodySmall!, + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: Stack( + children: [ + Column( + children: [ + Container( + height: 48, + decoration: BoxDecoration( + color: scale.primaryScale.subtleBorder, + ), + child: Row(children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsetsDirectional.fromSTEB( + 16, 0, 16, 0), + child: Text(title, + textAlign: TextAlign.start, + style: textTheme.titleMedium!.copyWith( + color: scale.primaryScale.borderText)), + )), + const Spacer(), + IconButton( + icon: Icon(Icons.close, + color: scale.primaryScale.borderText), + onPressed: () async { + context.read().setActiveChat(null); + }).paddingLTRB(16, 0, 16, 0) + ]), + ), + Expanded( + child: DecoratedBox( + decoration: const BoxDecoration(), + child: NotificationListener( + onNotification: (notification) { + if (chatComponentCubit.scrollOffset != 0) { + return false; + } + + if (!isFirstPage && + notification.metrics.pixels <= + ((notification.metrics.maxScrollExtent - + notification + .metrics.minScrollExtent) * + (1.0 - onEndReachedThreshold) + + notification.metrics.minScrollExtent)) { + // + final scrollOffset = (notification + .metrics.maxScrollExtent - + notification.metrics.minScrollExtent) * + (1.0 - onEndReachedThreshold); + + chatComponentCubit.scrollOffset = scrollOffset; + + // + singleFuture(chatComponentState.chatKey, + () async { + await _handlePageForward(chatComponentCubit, + messageWindow, notification); + }); + } else if (!isLastPage && + notification.metrics.pixels >= + ((notification.metrics.maxScrollExtent - + notification + .metrics.minScrollExtent) * + onEndReachedThreshold + + notification.metrics.minScrollExtent)) { + // + final scrollOffset = -(notification + .metrics.maxScrollExtent - + notification.metrics.minScrollExtent) * + (1.0 - onEndReachedThreshold); + + chatComponentCubit.scrollOffset = scrollOffset; + // + singleFuture(chatComponentState.chatKey, + () async { + await _handlePageBackward(chatComponentCubit, + messageWindow, notification); + }); + } + return false; + }, + child: Chat( + key: chatComponentState.chatKey, + theme: chatTheme, + messages: messageWindow.window.toList(), + scrollToBottomOnSend: isFirstPage, + scrollController: + chatComponentState.scrollController, + // isLastPage: isLastPage, + // onEndReached: () async { + // await _handlePageBackward( + // chatComponentCubit, messageWindow); + // }, + //onEndReachedThreshold: onEndReachedThreshold, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, + onSendPressed: (pt) => + _handleSendPressed(chatComponentCubit, pt), + //showUserAvatars: false, + //showUserNames: true, + user: chatComponentState.localUser, + emptyState: const EmptyChatWidget())), + ), + ), + ], + ), + ], + ), + )); + } +} diff --git a/lib/chat/views/views.dart b/lib/chat/views/views.dart index 1999862..7e8adce 100644 --- a/lib/chat/views/views.dart +++ b/lib/chat/views/views.dart @@ -1,4 +1,4 @@ -export 'chat_component.dart'; +export 'chat_component_widget.dart'; export 'empty_chat_widget.dart'; export 'new_chat_bottom_sheet.dart'; export 'no_conversation_widget.dart'; diff --git a/lib/layout/home/home_account_ready/home_account_ready_chat.dart b/lib/layout/home/home_account_ready/home_account_ready_chat.dart index 587828a..6e1868c 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_chat.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_chat.dart @@ -33,8 +33,9 @@ class HomeAccountReadyChatState extends State { if (activeChatLocalConversationKey == null) { return const NoConversationWidget(); } - return ChatComponent.builder( - localConversationRecordKey: activeChatLocalConversationKey); + return ChatComponentWidget.builder( + localConversationRecordKey: activeChatLocalConversationKey, + key: ValueKey(activeChatLocalConversationKey)); } @override diff --git a/lib/layout/home/home_account_ready/home_account_ready_main.dart b/lib/layout/home/home_account_ready/home_account_ready_main.dart index 0a4b28e..9fec3ce 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_main.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_main.dart @@ -40,12 +40,10 @@ class _HomeAccountReadyMainState extends State { color: scale.secondaryScale.borderText, constraints: const BoxConstraints.expand(height: 64, width: 64), style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - scale.primaryScale.hoverBorder), - shape: MaterialStateProperty.all( - const RoundedRectangleBorder( - borderRadius: - BorderRadius.all(Radius.circular(16))))), + backgroundColor: + WidgetStateProperty.all(scale.primaryScale.hoverBorder), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16))))), tooltip: translate('app_bar.settings_tooltip'), onPressed: () async { await GoRouterHelper(context).push('/settings'); @@ -71,8 +69,9 @@ class _HomeAccountReadyMainState extends State { if (activeChatLocalConversationKey == null) { return const NoConversationWidget(); } - return ChatComponent.builder( - localConversationRecordKey: activeChatLocalConversationKey); + return ChatComponentWidget.builder( + localConversationRecordKey: activeChatLocalConversationKey, + ); } // ignore: prefer_expression_function_bodies diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index db0ea2a..08e32b3 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -10,7 +10,9 @@ const Map _blocChangeLogLevels = { 'TableDBArrayProtobufCubit': LogLevel.off, 'DHTLogCubit': LogLevel.off, 'SingleContactMessagesCubit': LogLevel.off, + 'ChatComponentCubit': LogLevel.off, }; + const Map _blocCreateCloseLogLevels = {}; const Map _blocErrorLogLevels = {}; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index 49acce0..a688453 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -39,11 +39,13 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } final bLookup = await _spine.lookupPosition(bPos); if (bLookup == null) { + await aLookup.close(); throw StateError("can't lookup position b in swap of dht log"); } // Swap items in the segments if (aLookup.shortArray == bLookup.shortArray) { + await bLookup.close(); await aLookup.scope((sa) => sa.operateWriteEventual((aWrite) async { await aWrite.swap(aLookup.pos, bLookup.pos); return true; @@ -76,7 +78,9 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } // Write item to the segment - return lookup.scope((sa) => sa.operateWrite((write) async { + return lookup.scope((sa) async { + try { + return sa.operateWrite((write) async { // If this a new segment, then clear it in case we have wrapped around if (lookup.pos == 0) { await write.clear(); @@ -85,7 +89,11 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { throw StateError('appending should be at the end'); } return write.tryAdd(value); - })); + }); + } on DHTExceptionTryAgain { + return false; + } + }); } @override @@ -110,7 +118,9 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { final sublistValues = values.sublist(valueIdx, valueIdx + sacount); dws.add(() async { - final ok = await lookup.scope((sa) => sa.operateWrite((write) async { + final ok = await lookup.scope((sa) async { + try { + return sa.operateWrite((write) async { // If this a new segment, then clear it in // case we have wrapped around if (lookup.pos == 0) { @@ -120,7 +130,11 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { throw StateError('appending should be at the end'); } return write.tryAddAll(sublistValues); - })); + }); + } on DHTExceptionTryAgain { + return false; + } + }); if (!ok) { success = false; } diff --git a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart index 702a2ad..606ded5 100644 --- a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart +++ b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart @@ -103,6 +103,7 @@ class TableDBArrayProtobufCubit final elements = avElements.asData!.value; emit(AsyncValue.data(TableDBArrayProtobufStateData( windowElements: elements, + length: _array.length, windowTail: _tail, windowCount: _count, follow: _follow))); diff --git a/pubspec.lock b/pubspec.lock index 8a70f22..8bf7560 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct main" description: name: archive - sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "3.6.1" args: dependency: transitive description: @@ -155,18 +155,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa" + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.4.11" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.1" built_collection: dependency: transitive description: @@ -219,10 +219,10 @@ packages: dependency: transitive description: name: camera_android - sha256: b350ac087f111467e705b2b76cc1322f7f5bdc122aa83b4b243b0872f390d229 + sha256: "3af7f0b55f184d392d2eec238aaa30552ebeef2915e5e094f5488bf50d6d7ca2" url: "https://pub.dev" source: hosted - version: "0.10.9+2" + version: "0.10.9+3" camera_avfoundation: dependency: transitive description: @@ -251,10 +251,10 @@ packages: dependency: "direct main" description: name: change_case - sha256: "47c48c36f95f20c6d0ba03efabceff261d05026cca322cc2c4c01c343371b5bb" + sha256: "99cfdf2018c627c8a3af5a23ea4c414eb69c75c31322d23b9660ebc3cf30b514" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" characters: dependency: transitive description: @@ -403,10 +403,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "533806a7f0c624c2e479d05d3fdce4c87109a7cd0db39b8cc3830d3a2e8dedc7" + sha256: c3c73f4f989d3302066e4ec94e6ec73b5dc872592d02194f49f1352d64126b8c url: "https://pub.dev" source: hosted - version: "10.2.3" + version: "10.2.4" ffi: dependency: transitive description: @@ -471,19 +471,18 @@ packages: flutter_chat_ui: dependency: "direct main" description: - name: flutter_chat_ui - sha256: "40fb37acc328dd179eadc3d67bf8bd2d950dc0da34464aa8d48e8707e0234c09" - url: "https://pub.dev" - source: hosted + path: "../flutter_chat_ui" + relative: true + source: path version: "1.6.13" flutter_form_builder: dependency: "direct main" description: name: flutter_form_builder - sha256: "560eb5e367d81170c6ade1e7ae63ecc5167936ae2cdfaae8a345e91bce19d2f2" + sha256: "447f8808f68070f7df968e8063aada3c9d2e90e789b5b70f3b44e4b315212656" url: "https://pub.dev" source: hosted - version: "9.2.1" + version: "9.3.0" flutter_hooks: dependency: "direct main" description: @@ -533,10 +532,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.0.20" flutter_shaders: dependency: transitive description: @@ -573,10 +572,10 @@ packages: dependency: "direct main" description: name: flutter_translate - sha256: "8b1c449bf6d17753e6f188185f735ebc0a328d21d745878a43be66857de8ebb3" + sha256: bc09db690098879e3f90eb3aac3499e5282f32d5f9d8f1cc597d67bbc1e065ef url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -586,10 +585,10 @@ packages: dependency: "direct main" description: name: form_builder_validators - sha256: "19aa5282b7cede82d0025ab031a98d0554b84aa2ba40f12013471a3b3e22bf02" + sha256: "475853a177bfc832ec12551f752fd0001278358a6d42d2364681ff15f48f67cf" url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "10.0.1" freezed: dependency: "direct dev" description: @@ -634,10 +633,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: aa073287b8f43553678e6fa9e8bb9c83212ff76e09542129a8099bbc8db4df65 + sha256: abec47eb8c8c36ebf41d0a4c64dbbe7f956e39a012b3aafc530e951bdc12fe3f url: "https://pub.dev" source: hosted - version: "14.1.2" + version: "14.1.4" graphs: dependency: transitive description: @@ -706,10 +705,10 @@ packages: dependency: "direct main" description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.2.0" intl: dependency: "direct main" description: @@ -826,10 +825,10 @@ packages: dependency: "direct main" description: name: motion_toast - sha256: "4763b2aa3499d0bf00ffd9737479b73141d0397f532542893156efb4a5ac1994" + sha256: "8dc8af93c606d0a08f2592591164f4a761099c5470e589f25689de6c601f124e" url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.10.0" nested: dependency: transitive description: @@ -890,10 +889,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.5" path_provider_foundation: dependency: transitive description: @@ -1018,10 +1017,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" qr: dependency: transitive description: @@ -1095,7 +1094,7 @@ packages: source: hosted version: "0.1.9" scroll_to_index: - dependency: transitive + dependency: "direct main" description: name: scroll_to_index sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 @@ -1106,10 +1105,10 @@ packages: dependency: "direct main" description: name: searchable_listview - sha256: dfa6358f5e097f45b5b51a160cb6189e112e3abe0f728f4740349cd3b6575617 + sha256: "5e547f2a3f88f99a798cfe64882cfce51496d8f3177bea4829dd7bcf3fa8910d" url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.14.0" share_plus: dependency: "direct main" description: @@ -1138,10 +1137,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_foundation: dependency: transitive description: @@ -1392,26 +1391,26 @@ packages: dependency: transitive description: name: universal_platform - sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" url: "https://pub.dev" source: hosted - version: "1.0.0+1" + version: "1.1.0" url_launcher: dependency: transitive description: name: url_launcher - sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.2.6" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" + sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_ios: dependency: transitive description: @@ -1542,10 +1541,10 @@ packages: dependency: transitive description: name: web_socket - sha256: "217f49b5213796cb508d6a942a5dc604ce1cb6a0a6b3d8cb3f0c314f0ecea712" + sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078" url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.1.5" web_socket_channel: dependency: transitive description: @@ -1628,4 +1627,4 @@ packages: version: "1.1.2" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.1" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 133d482..32c8121 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,30 +10,33 @@ environment: dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 - archive: ^3.5.1 + archive: ^3.6.1 async_tools: ^0.1.1 - awesome_extensions: ^2.0.14 + awesome_extensions: ^2.0.16 badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 bloc_advanced_tools: ^0.1.1 blurry_modal_progress_hud: ^1.1.1 - change_case: ^2.0.1 + change_case: ^2.1.0 charcode: ^1.3.1 circular_profile_avatar: ^2.0.5 circular_reveal_animation: ^2.0.1 cool_dropdown: ^2.1.0 cupertino_icons: ^1.0.8 equatable: ^2.0.5 - fast_immutable_collections: ^10.2.2 + fast_immutable_collections: ^10.2.4 fixnum: ^1.1.0 flutter: sdk: flutter flutter_animate: ^4.5.0 flutter_bloc: ^8.1.5 flutter_chat_types: ^3.6.2 - flutter_chat_ui: ^1.6.12 - flutter_form_builder: ^9.2.1 + flutter_chat_ui: + git: + url: https://gitlab.com/veilid/flutter_chat_ui.git + ref: main + flutter_form_builder: ^9.3.0 flutter_hooks: ^0.20.5 flutter_localizations: sdk: flutter @@ -41,18 +44,18 @@ dependencies: flutter_slidable: ^3.1.0 flutter_spinkit: ^5.2.1 flutter_svg: ^2.0.10+1 - flutter_translate: ^4.0.4 - form_builder_validators: ^9.1.0 + flutter_translate: ^4.1.0 + form_builder_validators: ^10.0.1 freezed_annotation: ^2.4.1 - go_router: ^14.1.2 + go_router: ^14.1.4 hydrated_bloc: ^9.1.5 - image: ^4.1.7 - intl: ^0.18.1 + image: ^4.2.0 + intl: ^0.19.0 json_annotation: ^4.9.0 loggy: ^2.0.3 - meta: ^1.11.0 + meta: ^1.12.0 mobile_scanner: ^5.1.1 - motion_toast: ^2.9.1 + motion_toast: ^2.10.0 pasteboard: ^0.2.0 path: ^1.9.0 path_provider: ^2.1.3 @@ -65,7 +68,8 @@ dependencies: quickalert: ^1.1.0 radix_colors: ^1.0.4 reorderable_grid: ^1.0.10 - searchable_listview: ^2.12.0 + scroll_to_index: ^3.0.1 + searchable_listview: ^2.14.0 share_plus: ^9.0.0 shared_preferences: ^2.2.3 signal_strength_indicator: ^0.4.1 @@ -83,7 +87,7 @@ dependencies: path: ../veilid/veilid-flutter veilid_support: path: packages/veilid_support - window_manager: ^0.3.8 + window_manager: ^0.3.9 xterm: ^4.0.0 zxing2: ^0.2.3 @@ -92,12 +96,12 @@ dependency_overrides: path: ../dart_async_tools bloc_advanced_tools: path: ../bloc_advanced_tools - # REMOVE ONCE form_builder_validators HAS A FIX UPSTREAM - intl: 0.19.0 + flutter_chat_ui: + path: ../flutter_chat_ui dev_dependencies: - build_runner: ^2.4.9 + build_runner: ^2.4.11 freezed: ^2.5.2 icons_launcher: ^2.1.7 json_serializable: ^6.8.0 From f4119e077ae78fcc3014c418c202131ca8dabc78 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 6 Jun 2024 07:48:37 -0400 Subject: [PATCH 17/19] update pubs and versions --- pubspec.lock | 2 +- pubspec.yaml | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 8bf7560..eae680c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -102,7 +102,7 @@ packages: path: "../bloc_advanced_tools" relative: true source: path - version: "0.1.1" + version: "0.1.2" blurry_modal_progress_hud: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 32c8121..082c556 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ version: 0.2.0+10 environment: sdk: '>=3.2.0 <4.0.0' - flutter: '>=3.19.1' + flutter: '>=3.22.1' dependencies: animated_theme_switcher: ^2.0.10 @@ -16,7 +16,7 @@ dependencies: badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.1 + bloc_advanced_tools: ^0.1.2 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.1.0 charcode: ^1.3.1 @@ -91,14 +91,13 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -dependency_overrides: - async_tools: - path: ../dart_async_tools - bloc_advanced_tools: - path: ../bloc_advanced_tools - flutter_chat_ui: - path: ../flutter_chat_ui - +# dependency_overrides: +# async_tools: +# path: ../dart_async_tools +# bloc_advanced_tools: +# path: ../bloc_advanced_tools +# flutter_chat_ui: +# path: ../flutter_chat_ui dev_dependencies: build_runner: ^2.4.11 From 4d32d51dd7e712252943237cbca1ab80c2b72050 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 6 Jun 2024 07:51:30 -0400 Subject: [PATCH 18/19] fix readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 82631b7..dc02697 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,17 @@ While this is still in development, you must have a clone of the Veilid source c ### For Linux Systems: ``` -./setup_linux.sh +./dev-setup/setup_linux.sh ``` ### For Mac Systems: ``` -./setup_macos.sh +./dev-setup/setup_macos.sh ``` ## Updating Code ### To update the WASM binary from `veilid-wasm`: -* Debug WASM: run `./wasm_update.sh` -* Release WASM: run `/wasm_update.sh release` +* Debug WASM: run `./dev-setup/wasm_update.sh` +* Release WASM: run `./dev-setup/wasm_update.sh release` From 7f4b0166fe9d94e63afcd082af821969a44ee98d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 6 Jun 2024 07:55:49 -0400 Subject: [PATCH 19/19] fix pubspec --- pubspec.lock | 24 ++++++++++++++---------- pubspec.yaml | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index eae680c..bfd5be1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -60,9 +60,10 @@ packages: async_tools: dependency: "direct main" description: - path: "../dart_async_tools" - relative: true - source: path + name: async_tools + sha256: e783ac6ed5645c86da34240389bb3a000fc5e3ae6589c6a482eb24ece7217681 + url: "https://pub.dev" + source: hosted version: "0.1.1" awesome_extensions: dependency: "direct main" @@ -99,9 +100,10 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - path: "../bloc_advanced_tools" - relative: true - source: path + name: bloc_advanced_tools + sha256: "0cf9b3a73a67addfe22ec3f97a1ac240f6ad53870d6b21a980260f390d7901cd" + url: "https://pub.dev" + source: hosted version: "0.1.2" blurry_modal_progress_hud: dependency: "direct main" @@ -471,9 +473,11 @@ packages: flutter_chat_ui: dependency: "direct main" description: - path: "../flutter_chat_ui" - relative: true - source: path + path: "." + ref: main + resolved-ref: "6712d897e25041de38fb53ec06dc7a12cc6bff7d" + url: "https://gitlab.com/veilid/flutter-chat-ui.git" + source: git version: "1.6.13" flutter_form_builder: dependency: "direct main" @@ -1627,4 +1631,4 @@ packages: version: "1.1.2" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + flutter: ">=3.22.1" diff --git a/pubspec.yaml b/pubspec.yaml index 082c556..56c7685 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: flutter_chat_types: ^3.6.2 flutter_chat_ui: git: - url: https://gitlab.com/veilid/flutter_chat_ui.git + url: https://gitlab.com/veilid/flutter-chat-ui.git ref: main flutter_form_builder: ^9.3.0 flutter_hooks: ^0.20.5