mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-05-14 20:22:15 -04:00
messages work
This commit is contained in:
parent
f2caa7a0b3
commit
09ae8ff6bb
15 changed files with 246 additions and 414 deletions
|
@ -1 +1,2 @@
|
||||||
export 'active_chat_cubit.dart';
|
export 'active_chat_cubit.dart';
|
||||||
|
export 'messages_cubit.dart';
|
||||||
|
|
|
@ -0,0 +1,209 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
|
import '../../account_manager/account_manager.dart';
|
||||||
|
import '../../proto/proto.dart' as proto;
|
||||||
|
|
||||||
|
class _MessageQueueEntry {
|
||||||
|
_MessageQueueEntry(
|
||||||
|
{required this.localMessages, required this.remoteMessages});
|
||||||
|
IList<proto.Message> localMessages;
|
||||||
|
IList<proto.Message> remoteMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessagesCubit extends Cubit<AsyncValue<IList<proto.Message>>> {
|
||||||
|
MessagesCubit(
|
||||||
|
{required ActiveAccountInfo activeAccountInfo,
|
||||||
|
required TypedKey remoteIdentityPublicKey,
|
||||||
|
required TypedKey localConversationRecordKey,
|
||||||
|
required TypedKey localMessagesRecordKey,
|
||||||
|
required TypedKey remoteConversationRecordKey,
|
||||||
|
required TypedKey remoteMessagesRecordKey})
|
||||||
|
: _activeAccountInfo = activeAccountInfo,
|
||||||
|
_localMessagesRecordKey = localMessagesRecordKey,
|
||||||
|
_remoteIdentityPublicKey = remoteIdentityPublicKey,
|
||||||
|
_remoteMessagesRecordKey = remoteMessagesRecordKey,
|
||||||
|
_remoteMessagesQueue = StreamController(),
|
||||||
|
super(const AsyncValue.loading()) {
|
||||||
|
// Local messages key
|
||||||
|
Future.delayed(Duration.zero, () async {
|
||||||
|
final crypto = await getMessagesCrypto();
|
||||||
|
final writer = _activeAccountInfo.conversationWriter;
|
||||||
|
final record = await DHTShortArray.openWrite(
|
||||||
|
_localMessagesRecordKey, writer,
|
||||||
|
parent: localConversationRecordKey, crypto: crypto);
|
||||||
|
await _setLocalMessages(record);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remote messages key
|
||||||
|
Future.delayed(Duration.zero, () async {
|
||||||
|
// Open remote record key if it is specified
|
||||||
|
final crypto = await getMessagesCrypto();
|
||||||
|
final record = await DHTShortArray.openRead(_remoteMessagesRecordKey,
|
||||||
|
parent: remoteConversationRecordKey, crypto: crypto);
|
||||||
|
await _setRemoteMessages(record);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remote messages listener
|
||||||
|
Future.delayed(Duration.zero, () async {
|
||||||
|
await for (final entry in _remoteMessagesQueue.stream) {
|
||||||
|
await _updateRemoteMessagesStateAsync(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateLocalMessagesState(AsyncValue<IList<proto.Message>> avmessages) {
|
||||||
|
// Updated local messages from online just update the state immediately
|
||||||
|
emit(avmessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateRemoteMessagesStateAsync(_MessageQueueEntry entry) async {
|
||||||
|
// Updated remote messages need to be merged with the local messages state
|
||||||
|
|
||||||
|
// Ensure remoteMessages is sorted by timestamp
|
||||||
|
final remoteMessages =
|
||||||
|
entry.remoteMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
|
|
||||||
|
// Existing messages will always be sorted by timestamp so merging is easy
|
||||||
|
var localMessages = entry.localMessages;
|
||||||
|
var pos = 0;
|
||||||
|
for (final newMessage in remoteMessages) {
|
||||||
|
var skip = false;
|
||||||
|
while (pos < localMessages.length) {
|
||||||
|
final m = localMessages[pos];
|
||||||
|
|
||||||
|
// If timestamp to insert is less than
|
||||||
|
// the current position, insert it here
|
||||||
|
final newTs = Timestamp.fromInt64(newMessage.timestamp);
|
||||||
|
final curTs = Timestamp.fromInt64(m.timestamp);
|
||||||
|
final cmp = newTs.compareTo(curTs);
|
||||||
|
if (cmp < 0) {
|
||||||
|
break;
|
||||||
|
} else if (cmp == 0) {
|
||||||
|
skip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
// Insert at this position
|
||||||
|
if (!skip) {
|
||||||
|
// Insert into dht backing array
|
||||||
|
await _localMessagesCubit!.shortArray
|
||||||
|
.tryInsertItem(pos, newMessage.writeToBuffer());
|
||||||
|
// Insert into local copy as well for this operation
|
||||||
|
localMessages = localMessages.insert(pos, newMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateRemoteMessagesState(AsyncValue<IList<proto.Message>> avmessages) {
|
||||||
|
final remoteMessages = avmessages.data?.value;
|
||||||
|
if (remoteMessages == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final localMessages = state.data?.value;
|
||||||
|
if (localMessages == null) {
|
||||||
|
// No local messages means remote messages
|
||||||
|
// are all we have so merging is easy
|
||||||
|
emit(AsyncValue.data(remoteMessages));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_remoteMessagesQueue.add(_MessageQueueEntry(
|
||||||
|
localMessages: localMessages, remoteMessages: remoteMessages));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open local messages key
|
||||||
|
Future<void> _setLocalMessages(DHTShortArray localMessagesRecord) async {
|
||||||
|
assert(_localMessagesCubit == null, 'shoud not set local messages twice');
|
||||||
|
_localMessagesCubit = DHTShortArrayCubit.value(
|
||||||
|
shortArray: localMessagesRecord,
|
||||||
|
decodeElement: proto.Message.fromBuffer);
|
||||||
|
_localMessagesCubit!.stream.listen(updateLocalMessagesState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open remote messages key
|
||||||
|
Future<void> _setRemoteMessages(DHTShortArray remoteMessagesRecord) async {
|
||||||
|
assert(_remoteMessagesCubit == null, 'shoud not set remote messages twice');
|
||||||
|
_remoteMessagesCubit = DHTShortArrayCubit.value(
|
||||||
|
shortArray: remoteMessagesRecord,
|
||||||
|
decodeElement: proto.Message.fromBuffer);
|
||||||
|
_remoteMessagesCubit!.stream.listen(updateRemoteMessagesState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize local messages
|
||||||
|
static Future<T> initLocalMessages<T>({
|
||||||
|
required ActiveAccountInfo activeAccountInfo,
|
||||||
|
required TypedKey remoteIdentityPublicKey,
|
||||||
|
required TypedKey localConversationKey,
|
||||||
|
required FutureOr<T> Function(DHTShortArray) callback,
|
||||||
|
}) async {
|
||||||
|
final crypto =
|
||||||
|
await _makeMessagesCrypto(activeAccountInfo, remoteIdentityPublicKey);
|
||||||
|
final writer = activeAccountInfo.conversationWriter;
|
||||||
|
|
||||||
|
return (await DHTShortArray.create(
|
||||||
|
parent: localConversationKey, crypto: crypto, smplWriter: writer))
|
||||||
|
.deleteScope((messages) async => await callback(messages));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force refresh of messages
|
||||||
|
Future<void> refresh() async {
|
||||||
|
final lcc = _localMessagesCubit;
|
||||||
|
final rcc = _remoteMessagesCubit;
|
||||||
|
|
||||||
|
if (lcc != null) {
|
||||||
|
await lcc.refresh();
|
||||||
|
}
|
||||||
|
if (rcc != null) {
|
||||||
|
await rcc.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addMessage({required proto.Message message}) async {
|
||||||
|
await _localMessagesCubit!.shortArray.tryAddItem(message.writeToBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DHTRecordCrypto> getMessagesCrypto() async {
|
||||||
|
var messagesCrypto = _messagesCrypto;
|
||||||
|
if (messagesCrypto != null) {
|
||||||
|
return messagesCrypto;
|
||||||
|
}
|
||||||
|
messagesCrypto =
|
||||||
|
await _makeMessagesCrypto(_activeAccountInfo, _remoteIdentityPublicKey);
|
||||||
|
_messagesCrypto = messagesCrypto;
|
||||||
|
return messagesCrypto;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<DHTRecordCrypto> _makeMessagesCrypto(
|
||||||
|
ActiveAccountInfo activeAccountInfo,
|
||||||
|
TypedKey remoteIdentityPublicKey) async {
|
||||||
|
final identitySecret = activeAccountInfo.userLogin.identitySecret;
|
||||||
|
final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind);
|
||||||
|
final sharedSecret =
|
||||||
|
await cs.cachedDH(remoteIdentityPublicKey.value, identitySecret.value);
|
||||||
|
|
||||||
|
final messagesCrypto = await DHTRecordCryptoPrivate.fromSecret(
|
||||||
|
identitySecret.kind, sharedSecret);
|
||||||
|
return messagesCrypto;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ActiveAccountInfo _activeAccountInfo;
|
||||||
|
final TypedKey _remoteIdentityPublicKey;
|
||||||
|
final TypedKey _localMessagesRecordKey;
|
||||||
|
final TypedKey _remoteMessagesRecordKey;
|
||||||
|
DHTShortArrayCubit<proto.Message>? _localMessagesCubit;
|
||||||
|
DHTShortArrayCubit<proto.Message>? _remoteMessagesCubit;
|
||||||
|
final StreamController<_MessageQueueEntry> _remoteMessagesQueue;
|
||||||
|
//
|
||||||
|
DHTRecordCrypto? _messagesCrypto;
|
||||||
|
}
|
|
@ -1,37 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../../tools/tools.dart';
|
|
||||||
|
|
||||||
Widget buildChatComponent() {
|
|
||||||
// final contactList = ref.watch(fetchContactListProvider).asData?.value ??
|
|
||||||
// const IListConst([]);
|
|
||||||
|
|
||||||
// final activeChat = ref.watch(activeChatStateProvider);
|
|
||||||
// if (activeChat == null) {
|
|
||||||
// return const EmptyChatWidget();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// final activeAccountInfo =
|
|
||||||
// ref.watch(fetchActiveAccountProvider).asData?.value;
|
|
||||||
// if (activeAccountInfo == null) {
|
|
||||||
// return const EmptyChatWidget();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// final activeChatContactIdx = contactList.indexWhere(
|
|
||||||
// (c) =>
|
|
||||||
// proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) ==
|
|
||||||
// activeChat,
|
|
||||||
// );
|
|
||||||
// if (activeChatContactIdx == -1) {
|
|
||||||
// ref.read(activeChatStateProvider.notifier).state = null;
|
|
||||||
// return const EmptyChatWidget();
|
|
||||||
// }
|
|
||||||
// final activeChatContact = contactList[activeChatContactIdx];
|
|
||||||
|
|
||||||
// return ChatComponent(
|
|
||||||
// activeAccountInfo: activeAccountInfo,
|
|
||||||
// activeChat: activeChat,
|
|
||||||
// activeChatContact: activeChatContact);
|
|
||||||
// }
|
|
||||||
return Builder(builder: waitingPage);
|
|
||||||
}
|
|
|
@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||||
|
|
||||||
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../theme/theme.dart';
|
import '../../theme/theme.dart';
|
||||||
import '../chat.dart';
|
import '../chat.dart';
|
||||||
|
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
export 'build_chat_component.dart';
|
export 'chat_component.dart';
|
||||||
|
export 'empty_chat_widget.dart';
|
||||||
|
export 'no_conversation_widget.dart';
|
||||||
|
|
|
@ -330,17 +330,20 @@ class ContactInvitationListCubit
|
||||||
final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(
|
final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(
|
||||||
contactResponse.remoteConversationRecordKey);
|
contactResponse.remoteConversationRecordKey);
|
||||||
|
|
||||||
final conversation = ConversationManager(
|
final conversation = ConversationCubit(
|
||||||
activeAccountInfo: _activeAccountInfo,
|
activeAccountInfo: _activeAccountInfo,
|
||||||
remoteIdentityPublicKey:
|
remoteIdentityPublicKey:
|
||||||
contactIdentityMaster.identityPublicTypedKey(),
|
contactIdentityMaster.identityPublicTypedKey(),
|
||||||
remoteConversationRecordKey: remoteConversationRecordKey);
|
remoteConversationRecordKey: remoteConversationRecordKey);
|
||||||
|
await conversation.refresh();
|
||||||
|
|
||||||
final remoteConversation = await conversation.readRemoteConversation();
|
final remoteConversation =
|
||||||
|
conversation.state.data?.value.remoteConversation;
|
||||||
if (remoteConversation == null) {
|
if (remoteConversation == null) {
|
||||||
log.info('Remote conversation could not be read. Waiting...');
|
log.info('Remote conversation could not be read. Waiting...');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete the local conversation now that we have the remote profile
|
// Complete the local conversation now that we have the remote profile
|
||||||
final localConversationRecordKey = proto.TypedKeyProto.fromProto(
|
final localConversationRecordKey = proto.TypedKeyProto.fromProto(
|
||||||
contactInvitationRecord.localConversationRecordKey);
|
contactInvitationRecord.localConversationRecordKey);
|
||||||
|
|
|
@ -42,7 +42,7 @@ class ValidContactInvitation {
|
||||||
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
||||||
// Create local conversation key for this
|
// Create local conversation key for this
|
||||||
// contact and send via contact response
|
// contact and send via contact response
|
||||||
final conversation = ConversationManager(
|
final conversation = ConversationCubit(
|
||||||
activeAccountInfo: _activeAccountInfo,
|
activeAccountInfo: _activeAccountInfo,
|
||||||
remoteIdentityPublicKey:
|
remoteIdentityPublicKey:
|
||||||
_contactIdentityMaster.identityPublicTypedKey());
|
_contactIdentityMaster.identityPublicTypedKey());
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export 'cubits/cubits.dart';
|
export 'cubits/cubits.dart';
|
||||||
export 'models/models.dart';
|
|
||||||
export 'views/views.dart';
|
export 'views/views.dart';
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'package:meta/meta.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../account_manager/account_manager.dart';
|
import '../../account_manager/account_manager.dart';
|
||||||
|
import '../../chat/chat.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
@ -181,11 +182,11 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||||
// ignore: prefer_expression_function_bodies
|
// ignore: prefer_expression_function_bodies
|
||||||
.deleteScope((localConversation) async {
|
.deleteScope((localConversation) async {
|
||||||
// Make messages log
|
// Make messages log
|
||||||
return (await DHTShortArray.create(
|
return MessagesCubit.initLocalMessages(
|
||||||
parent: localConversation.key,
|
activeAccountInfo: _activeAccountInfo,
|
||||||
crypto: crypto,
|
remoteIdentityPublicKey: _remoteIdentityPublicKey,
|
||||||
smplWriter: writer))
|
localConversationKey: localConversation.key,
|
||||||
.deleteScope((messages) async {
|
callback: (messages) async {
|
||||||
// Create initial local conversation key contents
|
// Create initial local conversation key contents
|
||||||
final conversation = proto.Conversation()
|
final conversation = proto.Conversation()
|
||||||
..profile = profile
|
..profile = profile
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export 'contact_list_cubit.dart';
|
export 'contact_list_cubit.dart';
|
||||||
|
export 'conversation_cubit.dart';
|
||||||
|
|
|
@ -1,345 +0,0 @@
|
||||||
// A Conversation is a type of Chat that is 1:1 between two Contacts only
|
|
||||||
// Each Contact in the ContactList has at most one Conversation between the
|
|
||||||
// remote contact and the local account
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
|
||||||
|
|
||||||
import '../../account_manager/account_manager.dart';
|
|
||||||
import '../../proto/proto.dart' as proto;
|
|
||||||
import '../../tools/tools.dart';
|
|
||||||
|
|
||||||
class ConversationManager {
|
|
||||||
ConversationManager(
|
|
||||||
{required ActiveAccountInfo activeAccountInfo,
|
|
||||||
required TypedKey remoteIdentityPublicKey,
|
|
||||||
TypedKey? localConversationRecordKey,
|
|
||||||
TypedKey? remoteConversationRecordKey})
|
|
||||||
: _activeAccountInfo = activeAccountInfo,
|
|
||||||
_localConversationRecordKey = localConversationRecordKey,
|
|
||||||
_remoteIdentityPublicKey = remoteIdentityPublicKey,
|
|
||||||
_remoteConversationRecordKey = remoteConversationRecordKey;
|
|
||||||
|
|
||||||
// Initialize a local conversation
|
|
||||||
// If we were the initiator of the conversation there may be an
|
|
||||||
// incomplete 'existingConversationRecord' that we need to fill
|
|
||||||
// in now that we have the remote identity key
|
|
||||||
Future<T> initLocalConversation<T>(
|
|
||||||
{required proto.Profile profile,
|
|
||||||
required FutureOr<T> Function(DHTRecord) callback,
|
|
||||||
TypedKey? existingConversationRecordKey}) async {
|
|
||||||
final pool = DHTRecordPool.instance;
|
|
||||||
final accountRecordKey =
|
|
||||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
|
||||||
|
|
||||||
final crypto = await getConversationCrypto();
|
|
||||||
final writer = _activeAccountInfo.conversationWriter;
|
|
||||||
|
|
||||||
// Open with SMPL scheme for identity writer
|
|
||||||
late final DHTRecord localConversationRecord;
|
|
||||||
if (existingConversationRecordKey != null) {
|
|
||||||
localConversationRecord = await pool.openWrite(
|
|
||||||
existingConversationRecordKey, writer,
|
|
||||||
parent: accountRecordKey, crypto: crypto);
|
|
||||||
} else {
|
|
||||||
final localConversationRecordCreate = await pool.create(
|
|
||||||
parent: accountRecordKey,
|
|
||||||
crypto: crypto,
|
|
||||||
schema: DHTSchema.smpl(
|
|
||||||
oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)]));
|
|
||||||
await localConversationRecordCreate.close();
|
|
||||||
localConversationRecord = await pool.openWrite(
|
|
||||||
localConversationRecordCreate.key, writer,
|
|
||||||
parent: accountRecordKey, crypto: crypto);
|
|
||||||
}
|
|
||||||
final out = localConversationRecord
|
|
||||||
// ignore: prefer_expression_function_bodies
|
|
||||||
.deleteScope((localConversation) async {
|
|
||||||
// Make messages log
|
|
||||||
return (await DHTShortArray.create(
|
|
||||||
parent: localConversation.key,
|
|
||||||
crypto: crypto,
|
|
||||||
smplWriter: writer))
|
|
||||||
.deleteScope((messages) async {
|
|
||||||
// Write local conversation key
|
|
||||||
final conversation = proto.Conversation()
|
|
||||||
..profile = profile
|
|
||||||
..identityMasterJson = jsonEncode(
|
|
||||||
_activeAccountInfo.localAccount.identityMaster.toJson())
|
|
||||||
..messages = messages.record.key.toProto();
|
|
||||||
|
|
||||||
//
|
|
||||||
final update = await localConversation.tryWriteProtobuf(
|
|
||||||
proto.Conversation.fromBuffer, conversation);
|
|
||||||
if (update != null) {
|
|
||||||
throw Exception('Failed to write local conversation');
|
|
||||||
}
|
|
||||||
return await callback(localConversation);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// If success, save the new local conversation record key in this object
|
|
||||||
_localConversationRecordKey = localConversationRecord.key;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<proto.Conversation?> readRemoteConversation() async {
|
|
||||||
final accountRecordKey =
|
|
||||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
|
||||||
final pool = DHTRecordPool.instance;
|
|
||||||
|
|
||||||
final crypto = await getConversationCrypto();
|
|
||||||
return (await pool.openRead(_remoteConversationRecordKey!,
|
|
||||||
parent: accountRecordKey, crypto: crypto))
|
|
||||||
.scope((remoteConversation) async {
|
|
||||||
//
|
|
||||||
final conversation =
|
|
||||||
await remoteConversation.getProtobuf(proto.Conversation.fromBuffer);
|
|
||||||
return conversation;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<proto.Conversation?> readLocalConversation() async {
|
|
||||||
final accountRecordKey =
|
|
||||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
|
||||||
final pool = DHTRecordPool.instance;
|
|
||||||
|
|
||||||
final crypto = await getConversationCrypto();
|
|
||||||
return (await pool.openRead(_localConversationRecordKey!,
|
|
||||||
parent: accountRecordKey, crypto: crypto))
|
|
||||||
.scope((localConversation) async {
|
|
||||||
//
|
|
||||||
final update =
|
|
||||||
await localConversation.getProtobuf(proto.Conversation.fromBuffer);
|
|
||||||
if (update != null) {
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<proto.Conversation?> writeLocalConversation({
|
|
||||||
required proto.Conversation conversation,
|
|
||||||
}) async {
|
|
||||||
final accountRecordKey =
|
|
||||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
|
||||||
final pool = DHTRecordPool.instance;
|
|
||||||
|
|
||||||
final crypto = await getConversationCrypto();
|
|
||||||
final writer = _activeAccountInfo.conversationWriter;
|
|
||||||
|
|
||||||
return (await pool.openWrite(_localConversationRecordKey!, writer,
|
|
||||||
parent: accountRecordKey, crypto: crypto))
|
|
||||||
.scope((localConversation) async {
|
|
||||||
//
|
|
||||||
final update = await localConversation.tryWriteProtobuf(
|
|
||||||
proto.Conversation.fromBuffer, conversation);
|
|
||||||
if (update != null) {
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addLocalConversationMessage(
|
|
||||||
{required proto.Message message}) async {
|
|
||||||
final conversation = await readLocalConversation();
|
|
||||||
if (conversation == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final messagesRecordKey =
|
|
||||||
proto.TypedKeyProto.fromProto(conversation.messages);
|
|
||||||
final crypto = await getConversationCrypto();
|
|
||||||
final writer = _activeAccountInfo.conversationWriter;
|
|
||||||
|
|
||||||
await (await DHTShortArray.openWrite(messagesRecordKey, writer,
|
|
||||||
parent: _localConversationRecordKey, crypto: crypto))
|
|
||||||
.scope((messages) async {
|
|
||||||
await messages.tryAddItem(message.writeToBuffer());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> mergeLocalConversationMessages(
|
|
||||||
{required IList<proto.Message> newMessages}) async {
|
|
||||||
final conversation = await readLocalConversation();
|
|
||||||
if (conversation == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var changed = false;
|
|
||||||
final messagesRecordKey =
|
|
||||||
proto.TypedKeyProto.fromProto(conversation.messages);
|
|
||||||
final crypto = await getConversationCrypto();
|
|
||||||
final writer = _activeAccountInfo.conversationWriter;
|
|
||||||
|
|
||||||
newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp)
|
|
||||||
.compareTo(Timestamp.fromInt64(b.timestamp)));
|
|
||||||
|
|
||||||
await (await DHTShortArray.openWrite(messagesRecordKey, writer,
|
|
||||||
parent: _localConversationRecordKey, crypto: crypto))
|
|
||||||
.scope((messages) async {
|
|
||||||
// Ensure newMessages is sorted by timestamp
|
|
||||||
newMessages =
|
|
||||||
newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
|
||||||
|
|
||||||
// Existing messages will always be sorted by timestamp so merging is easy
|
|
||||||
var pos = 0;
|
|
||||||
outer:
|
|
||||||
for (final newMessage in newMessages) {
|
|
||||||
var skip = false;
|
|
||||||
while (pos < messages.length) {
|
|
||||||
final m =
|
|
||||||
await messages.getItemProtobuf(proto.Message.fromBuffer, pos);
|
|
||||||
if (m == null) {
|
|
||||||
log.error('unable to get message #$pos');
|
|
||||||
break outer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If timestamp to insert is less than
|
|
||||||
// the current position, insert it here
|
|
||||||
final newTs = Timestamp.fromInt64(newMessage.timestamp);
|
|
||||||
final curTs = Timestamp.fromInt64(m.timestamp);
|
|
||||||
final cmp = newTs.compareTo(curTs);
|
|
||||||
if (cmp < 0) {
|
|
||||||
break;
|
|
||||||
} else if (cmp == 0) {
|
|
||||||
skip = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
pos++;
|
|
||||||
}
|
|
||||||
// Insert at this position
|
|
||||||
if (!skip) {
|
|
||||||
await messages.tryInsertItem(pos, newMessage.writeToBuffer());
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<IList<proto.Message>?> getRemoteConversationMessages() async {
|
|
||||||
final conversation = await readRemoteConversation();
|
|
||||||
if (conversation == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final messagesRecordKey =
|
|
||||||
proto.TypedKeyProto.fromProto(conversation.messages);
|
|
||||||
final crypto = await getConversationCrypto();
|
|
||||||
|
|
||||||
return (await DHTShortArray.openRead(messagesRecordKey,
|
|
||||||
parent: _remoteConversationRecordKey, crypto: crypto))
|
|
||||||
.scope((messages) async {
|
|
||||||
var out = IList<proto.Message>();
|
|
||||||
for (var i = 0; i < messages.length; i++) {
|
|
||||||
final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i);
|
|
||||||
if (msg == null) {
|
|
||||||
throw Exception('Failed to get message');
|
|
||||||
}
|
|
||||||
out = out.add(msg);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
Future<DHTRecordCrypto> getConversationCrypto() async {
|
|
||||||
var conversationCrypto = _conversationCrypto;
|
|
||||||
if (conversationCrypto != null) {
|
|
||||||
return conversationCrypto;
|
|
||||||
}
|
|
||||||
final identitySecret = _activeAccountInfo.userLogin.identitySecret;
|
|
||||||
final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind);
|
|
||||||
final sharedSecret =
|
|
||||||
await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value);
|
|
||||||
|
|
||||||
conversationCrypto = await DHTRecordCryptoPrivate.fromSecret(
|
|
||||||
identitySecret.kind, sharedSecret);
|
|
||||||
_conversationCrypto = conversationCrypto;
|
|
||||||
return conversationCrypto;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<IList<proto.Message>?> getLocalConversationMessages() async {
|
|
||||||
final conversation = await readLocalConversation();
|
|
||||||
if (conversation == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final messagesRecordKey =
|
|
||||||
proto.TypedKeyProto.fromProto(conversation.messages);
|
|
||||||
final crypto = await getConversationCrypto();
|
|
||||||
|
|
||||||
return (await DHTShortArray.openRead(messagesRecordKey,
|
|
||||||
parent: _localConversationRecordKey, crypto: crypto))
|
|
||||||
.scope((messages) async {
|
|
||||||
var out = IList<proto.Message>();
|
|
||||||
for (var i = 0; i < messages.length; i++) {
|
|
||||||
final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i);
|
|
||||||
if (msg == null) {
|
|
||||||
throw Exception('Failed to get message');
|
|
||||||
}
|
|
||||||
out = out.add(msg);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final ActiveAccountInfo _activeAccountInfo;
|
|
||||||
final TypedKey _remoteIdentityPublicKey;
|
|
||||||
TypedKey? _localConversationRecordKey;
|
|
||||||
TypedKey? _remoteConversationRecordKey;
|
|
||||||
//
|
|
||||||
DHTRecordCrypto? _conversationCrypto;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// //
|
|
||||||
// //
|
|
||||||
// //
|
|
||||||
// //
|
|
||||||
|
|
||||||
// @riverpod
|
|
||||||
// class ActiveConversationMessages extends _$ActiveConversationMessages {
|
|
||||||
// /// Get message for active conversation
|
|
||||||
// @override
|
|
||||||
// FutureOr<IList<proto.Message>?> build() async {
|
|
||||||
// await eventualVeilid.future;
|
|
||||||
|
|
||||||
// final activeChat = ref.watch(activeChatStateProvider);
|
|
||||||
// if (activeChat == null) {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// final activeAccountInfo =
|
|
||||||
// await ref.watch(fetchActiveAccountProvider.future);
|
|
||||||
// if (activeAccountInfo == null) {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// final contactList = ref.watch(fetchContactListProvider).asData?.value ??
|
|
||||||
// const IListConst([]);
|
|
||||||
|
|
||||||
// final activeChatContactIdx = contactList.indexWhere(
|
|
||||||
// (c) =>
|
|
||||||
// proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) ==
|
|
||||||
// activeChat,
|
|
||||||
// );
|
|
||||||
// if (activeChatContactIdx == -1) {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// final activeChatContact = contactList[activeChatContactIdx];
|
|
||||||
// final remoteIdentityPublicKey =
|
|
||||||
// proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey);
|
|
||||||
// // final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(
|
|
||||||
// // activeChatContact.remoteConversationRecordKey);
|
|
||||||
// final localConversationRecordKey = proto.TypedKeyProto.fromProto(
|
|
||||||
// activeChatContact.localConversationRecordKey);
|
|
||||||
|
|
||||||
// return await getLocalConversationMessages(
|
|
||||||
// activeAccountInfo: activeAccountInfo,
|
|
||||||
// localConversationRecordKey: localConversationRecordKey,
|
|
||||||
// remoteIdentityPublicKey: remoteIdentityPublicKey,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -1 +0,0 @@
|
||||||
export 'conversation.dart';
|
|
|
@ -35,6 +35,6 @@ class ChatOnlyPageState extends State<ChatOnlyPage>
|
||||||
Widget build(BuildContext context) => SafeArea(
|
Widget build(BuildContext context) => SafeArea(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode),
|
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode),
|
||||||
child: buildChatComponent(),
|
child: const ChatComponent(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ class HomeAccountReadyState extends State<HomeAccountReady>
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
Material(color: Colors.transparent, child: buildUserPanel()));
|
Material(color: Colors.transparent, child: buildUserPanel()));
|
||||||
|
|
||||||
Widget buildTabletRightPane(BuildContext context) => buildChatComponent();
|
Widget buildTabletRightPane(BuildContext context) => const ChatComponent();
|
||||||
|
|
||||||
// ignore: prefer_expression_function_bodies
|
// ignore: prefer_expression_function_bodies
|
||||||
Widget buildTablet(BuildContext context) {
|
Widget buildTablet(BuildContext context) {
|
||||||
|
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
|
|
||||||
import '../../veilid_support.dart';
|
import '../../veilid_support.dart';
|
||||||
|
|
||||||
|
@ -105,7 +104,6 @@ class DHTShortArrayCubit<T> extends Cubit<AsyncValue<IList<T>>> {
|
||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
|
||||||
DHTShortArray get shortArray => _shortArray;
|
DHTShortArray get shortArray => _shortArray;
|
||||||
|
|
||||||
late final DHTShortArray _shortArray;
|
late final DHTShortArray _shortArray;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue