more refactor, conversation work

This commit is contained in:
Christien Rioux 2024-01-28 21:31:53 -05:00
parent 20047a956f
commit 7bd426ce98
15 changed files with 204 additions and 390 deletions

View File

@ -1,2 +1,3 @@
export 'repository/contact_invitation_repository.dart'; export 'cubits/cubits.dart';
export 'models/models.dart';
export 'views/views.dart'; export 'views/views.dart';

View File

@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.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 '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../models/models.dart'; import '../models/models.dart';
@ -33,19 +33,19 @@ class InvitationStatus {
////////////////////////////////////////////////// //////////////////////////////////////////////////
// Mutable state for per-account contact invitations // Mutable state for per-account contact invitations
class ContactInvitationListCubit extends DHTShortArrayCubit<proto.ContactInvitation> { class ContactInvitationListCubit
extends DHTShortArrayCubit<proto.ContactInvitationRecord> {
ContactInvitationListCubit({ ContactInvitationListCubit({
required ActiveAccountInfo activeAccountInfo, required ActiveAccountInfo activeAccountInfo,
required proto.Account account, required proto.Account account,
required DHTShortArray dhtRecord,
}) : _activeAccountInfo = activeAccountInfo, }) : _activeAccountInfo = activeAccountInfo,
_account = account, _account = account,
_dhtRecord = dhtRecord, super(
super(shortArray: dhtRecord, decodeElement: proto.ContactInvitation.fromBuffer); open: () => _open(activeAccountInfo, account),
xxx convert the rest of this to cubit decodeElement: proto.ContactInvitationRecord.fromBuffer);
static Future<ContactInvitationRepository> open(
ActiveAccountInfo activeAccountInfo, proto.Account account) async {
static Future<DHTShortArray> _open(
ActiveAccountInfo activeAccountInfo, proto.Account account) async {
final accountRecordKey = final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
@ -57,28 +57,9 @@ xxx convert the rest of this to cubit
contactInvitationListRecordKey, contactInvitationListRecordKey,
parent: accountRecordKey); parent: accountRecordKey);
return ContactInvitationRepository._( return dhtRecord;
activeAccountInfo: activeAccountInfo,
account: account,
dhtRecord: dhtRecord);
} }
@override
Future<void> close() async {
await _dhtRecord.close();
await super.close();
}
// Future<void> refresh() async {
// for (var i = 0; i < _dhtRecord.length; i++) {
// final cir = await _dhtRecord.getItem(i);
// if (cir == null) {
// throw Exception('Failed to get contact invitation record');
// }
// _records = _records.add(proto.ContactInvitationRecord.fromBuffer(cir));
// }
// }
Future<Uint8List> createInvitation( Future<Uint8List> createInvitation(
{required EncryptionKeyType encryptionKeyType, {required EncryptionKeyType encryptionKeyType,
required String encryptionKey, required String encryptionKey,
@ -98,7 +79,8 @@ xxx convert the rest of this to cubit
encryptionKey: encryptionKey, encryptionKey: encryptionKey,
); );
// Create local chat DHT record with the account record key as its parent // Create local conversation DHT record with the account record key as its
// parent.
// Do not set the encryption of this key yet as it will not yet be written // Do not set the encryption of this key yet as it will not yet be written
// to and it will be eventually encrypted with the DH of the contact's // to and it will be eventually encrypted with the DH of the contact's
// identity key // identity key
@ -163,7 +145,7 @@ xxx convert the rest of this to cubit
// Add ContactInvitationRecord to account's list // Add ContactInvitationRecord to account's list
// if this fails, don't keep retrying, user can try again later // if this fails, don't keep retrying, user can try again later
if (await _dhtRecord.tryAddItem(cinvrec.writeToBuffer()) == false) { if (await shortArray.tryAddItem(cinvrec.writeToBuffer()) == false) {
throw Exception('Failed to add contact invitation record'); throw Exception('Failed to add contact invitation record');
} }
}); });
@ -180,15 +162,15 @@ xxx convert the rest of this to cubit
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Remove ContactInvitationRecord from account's list // Remove ContactInvitationRecord from account's list
for (var i = 0; i < _dhtRecord.length; i++) { for (var i = 0; i < shortArray.length; i++) {
final item = await _dhtRecord.getItemProtobuf( final item = await shortArray.getItemProtobuf(
proto.ContactInvitationRecord.fromBuffer, i); proto.ContactInvitationRecord.fromBuffer, i);
if (item == null) { if (item == null) {
throw Exception('Failed to get contact invitation record'); throw Exception('Failed to get contact invitation record');
} }
if (item.contactRequestInbox.recordKey == if (item.contactRequestInbox.recordKey ==
contactInvitationRecord.contactRequestInbox.recordKey) { contactInvitationRecord.contactRequestInbox.recordKey) {
await _dhtRecord.tryRemoveItem(i); await shortArray.tryRemoveItem(i);
break; break;
} }
} }
@ -229,11 +211,11 @@ xxx convert the rest of this to cubit
final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
// See if we're chatting to ourselves, if so, don't delete it here // Compare the invitation's contact request
final isSelf = _contactIdentityMaster.identityPublicKey == // inbox with our list of extant invitations
_activeAccountInfo.localAccount.identityMaster.identityPublicKey; // If we're chatting to ourselves,
xxx this doesn't work and the upper one doesnt either // we are validating an invitation we have created
final isSelf = _records.indexWhere((cir) => final isSelf = state.data!.value.indexWhere((cir) =>
proto.TypedKeyProto.fromProto(cir.contactRequestInbox.recordKey) == proto.TypedKeyProto.fromProto(cir.contactRequestInbox.recordKey) ==
contactRequestInboxKey) != contactRequestInboxKey) !=
-1; -1;
@ -283,10 +265,7 @@ xxx this doesn't work and the upper one doesnt either
out = ValidContactInvitation( out = ValidContactInvitation(
activeAccountInfo: _activeAccountInfo, activeAccountInfo: _activeAccountInfo,
signedContactInvitation: signedContactInvitation,
contactInvitation: contactInvitation,
contactRequestInboxKey: contactRequestInboxKey, contactRequestInboxKey: contactRequestInboxKey,
contactRequest: contactRequest,
contactRequestPrivate: contactRequestPrivate, contactRequestPrivate: contactRequestPrivate,
contactIdentityMaster: contactIdentityMaster, contactIdentityMaster: contactIdentityMaster,
writer: writer); writer: writer);
@ -296,13 +275,12 @@ xxx this doesn't work and the upper one doesnt either
} }
Future<InvitationStatus?> checkInvitationStatus( Future<InvitationStatus?> checkInvitationStatus(
{required ActiveAccountInfo activeAccountInfo, {required proto.ContactInvitationRecord contactInvitationRecord}) async {
required proto.ContactInvitationRecord contactInvitationRecord}) async {
// Open the contact request inbox // Open the contact request inbox
try { try {
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
final accountRecordKey = final accountRecordKey = _activeAccountInfo
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; .userLogin.accountRecordInfo.accountRecord.recordKey;
final writerKey = final writerKey =
proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerKey); proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerKey);
final writerSecret = final writerSecret =
@ -350,11 +328,14 @@ xxx this doesn't work and the upper one doesnt either
// Pull profile from remote conversation key // Pull profile from remote conversation key
final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(
contactResponse.remoteConversationRecordKey); contactResponse.remoteConversationRecordKey);
final remoteConversation = await readRemoteConversation(
activeAccountInfo: activeAccountInfo, final conversation = ConversationManager(
activeAccountInfo: _activeAccountInfo,
remoteIdentityPublicKey: remoteIdentityPublicKey:
contactIdentityMaster.identityPublicTypedKey(), contactIdentityMaster.identityPublicTypedKey(),
remoteConversationRecordKey: remoteConversationRecordKey); remoteConversationRecordKey: remoteConversationRecordKey);
final remoteConversation = await conversation.readRemoteConversation();
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;
@ -362,11 +343,9 @@ xxx this doesn't work and the upper one doesnt either
// 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);
return createConversation( return conversation.initLocalConversation(
activeAccountInfo: activeAccountInfo,
remoteIdentityPublicKey:
contactIdentityMaster.identityPublicTypedKey(),
existingConversationRecordKey: localConversationRecordKey, existingConversationRecordKey: localConversationRecordKey,
profile: xxx LOCAL PROFILE HERE NOT REMOTE
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
callback: (localConversation) async { callback: (localConversation) async {
return InvitationStatus( return InvitationStatus(
@ -400,9 +379,6 @@ xxx this doesn't work and the upper one doesnt either
} }
// //
final ActiveAccountInfo _activeAccountInfo; final ActiveAccountInfo _activeAccountInfo;
final proto.Account _account; final proto.Account _account;
final DHTShortArray _dhtRecord;
//IList<proto.ContactInvitationRecord> _records;
} }

View File

@ -1,2 +1,2 @@
export 'accepted_contact.dart'; export 'accepted_contact.dart';
export '../repository/valid_contact_invitation.dart'; export 'valid_contact_invitation.dart';

View File

@ -4,8 +4,8 @@ import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../models/models.dart'; import 'models.dart';
import 'contact_invitation_repository.dart'; import '../cubits/contact_invitation_list_cubit.dart';
////////////////////////////////////////////////// //////////////////////////////////////////////////
/// ///
@ -14,18 +14,12 @@ class ValidContactInvitation {
@internal @internal
ValidContactInvitation( ValidContactInvitation(
{required ActiveAccountInfo activeAccountInfo, {required ActiveAccountInfo activeAccountInfo,
required proto.SignedContactInvitation signedContactInvitation,
required proto.ContactInvitation contactInvitation,
required TypedKey contactRequestInboxKey, required TypedKey contactRequestInboxKey,
required proto.ContactRequest contactRequest,
required proto.ContactRequestPrivate contactRequestPrivate, required proto.ContactRequestPrivate contactRequestPrivate,
required IdentityMaster contactIdentityMaster, required IdentityMaster contactIdentityMaster,
required KeyPair writer}) required KeyPair writer})
: _activeAccountInfo = activeAccountInfo, : _activeAccountInfo = activeAccountInfo,
_signedContactInvitation = signedContactInvitation,
_contactInvitation = contactInvitation,
_contactRequestInboxKey = contactRequestInboxKey, _contactRequestInboxKey = contactRequestInboxKey,
_contactRequest = contactRequest,
_contactRequestPrivate = contactRequestPrivate, _contactRequestPrivate = contactRequestPrivate,
_contactIdentityMaster = contactIdentityMaster, _contactIdentityMaster = contactIdentityMaster,
_writer = writer; _writer = writer;
@ -140,8 +134,5 @@ class ValidContactInvitation {
final TypedKey _contactRequestInboxKey; final TypedKey _contactRequestInboxKey;
final IdentityMaster _contactIdentityMaster; final IdentityMaster _contactIdentityMaster;
final KeyPair _writer; final KeyPair _writer;
proto.SignedContactInvitation _signedContactInvitation; final proto.ContactRequestPrivate _contactRequestPrivate;
proto.ContactInvitation _contactInvitation;
proto.ContactRequest _contactRequest;
proto.ContactRequestPrivate _contactRequestPrivate;
} }

View File

@ -1 +1,2 @@
export 'models/models.dart';
export 'views/views.dart'; export 'views/views.dart';

View File

@ -2,6 +2,7 @@
// Each Contact in the ContactList has at most one Conversation between the // Each Contact in the ContactList has at most one Conversation between the
// remote contact and the local account // remote contact and the local account
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
@ -11,32 +12,86 @@ import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import 'chat.dart'; class ConversationManager {
ConversationManager(
class Conversation {
Conversation._(
{required ActiveAccountInfo activeAccountInfo, {required ActiveAccountInfo activeAccountInfo,
required TypedKey localConversationRecordKey,
required TypedKey remoteIdentityPublicKey, required TypedKey remoteIdentityPublicKey,
required TypedKey remoteConversationRecordKey}) TypedKey? localConversationRecordKey,
TypedKey? remoteConversationRecordKey})
: _activeAccountInfo = activeAccountInfo, : _activeAccountInfo = activeAccountInfo,
_localConversationRecordKey = localConversationRecordKey, _localConversationRecordKey = localConversationRecordKey,
_remoteIdentityPublicKey = remoteIdentityPublicKey, _remoteIdentityPublicKey = remoteIdentityPublicKey,
_remoteConversationRecordKey = remoteConversationRecordKey; _remoteConversationRecordKey = remoteConversationRecordKey;
Future<Conversation> open() async {} // 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;
Future<void> close() async { 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 { Future<proto.Conversation?> readRemoteConversation() async {
final accountRecordKey = final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final pool = await DHTRecordPool.instance(); final pool = DHTRecordPool.instance;
final crypto = await getConversationCrypto(); final crypto = await getConversationCrypto();
return (await pool.openRead(_remoteConversationRecordKey, return (await pool.openRead(_remoteConversationRecordKey!,
parent: accountRecordKey, crypto: crypto)) parent: accountRecordKey, crypto: crypto))
.scope((remoteConversation) async { .scope((remoteConversation) async {
// //
@ -49,10 +104,10 @@ class Conversation {
Future<proto.Conversation?> readLocalConversation() async { Future<proto.Conversation?> readLocalConversation() async {
final accountRecordKey = final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final pool = await DHTRecordPool.instance(); final pool = DHTRecordPool.instance;
final crypto = await getConversationCrypto(); final crypto = await getConversationCrypto();
return (await pool.openRead(_localConversationRecordKey, return (await pool.openRead(_localConversationRecordKey!,
parent: accountRecordKey, crypto: crypto)) parent: accountRecordKey, crypto: crypto))
.scope((localConversation) async { .scope((localConversation) async {
// //
@ -70,12 +125,12 @@ class Conversation {
}) async { }) async {
final accountRecordKey = final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final pool = await DHTRecordPool.instance(); final pool = DHTRecordPool.instance;
final crypto = await getConversationCrypto(); final crypto = await getConversationCrypto();
final writer = _activeAccountInfo.getConversationWriter(); final writer = _activeAccountInfo.conversationWriter;
return (await pool.openWrite(_localConversationRecordKey, writer, return (await pool.openWrite(_localConversationRecordKey!, writer,
parent: accountRecordKey, crypto: crypto)) parent: accountRecordKey, crypto: crypto))
.scope((localConversation) async { .scope((localConversation) async {
// //
@ -97,7 +152,7 @@ class Conversation {
final messagesRecordKey = final messagesRecordKey =
proto.TypedKeyProto.fromProto(conversation.messages); proto.TypedKeyProto.fromProto(conversation.messages);
final crypto = await getConversationCrypto(); final crypto = await getConversationCrypto();
final writer = _activeAccountInfo.getConversationWriter(); final writer = _activeAccountInfo.conversationWriter;
await (await DHTShortArray.openWrite(messagesRecordKey, writer, await (await DHTShortArray.openWrite(messagesRecordKey, writer,
parent: _localConversationRecordKey, crypto: crypto)) parent: _localConversationRecordKey, crypto: crypto))
@ -116,7 +171,7 @@ class Conversation {
final messagesRecordKey = final messagesRecordKey =
proto.TypedKeyProto.fromProto(conversation.messages); proto.TypedKeyProto.fromProto(conversation.messages);
final crypto = await getConversationCrypto(); final crypto = await getConversationCrypto();
final writer = _activeAccountInfo.getConversationWriter(); final writer = _activeAccountInfo.conversationWriter;
newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp) newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp)
.compareTo(Timestamp.fromInt64(b.timestamp))); .compareTo(Timestamp.fromInt64(b.timestamp)));
@ -195,9 +250,8 @@ class Conversation {
if (conversationCrypto != null) { if (conversationCrypto != null) {
return conversationCrypto; return conversationCrypto;
} }
final veilid = await eventualVeilid.future;
final identitySecret = _activeAccountInfo.userLogin.identitySecret; final identitySecret = _activeAccountInfo.userLogin.identitySecret;
final cs = await veilid.getCryptoSystem(identitySecret.kind); final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind);
final sharedSecret = final sharedSecret =
await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value); await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value);
@ -232,119 +286,60 @@ class Conversation {
} }
final ActiveAccountInfo _activeAccountInfo; final ActiveAccountInfo _activeAccountInfo;
final TypedKey _localConversationRecordKey;
final TypedKey _remoteIdentityPublicKey; final TypedKey _remoteIdentityPublicKey;
final TypedKey _remoteConversationRecordKey; TypedKey? _localConversationRecordKey;
TypedKey? _remoteConversationRecordKey;
// //
DHTRecordCrypto? _conversationCrypto; DHTRecordCrypto? _conversationCrypto;
} }
// Create a 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> createConversation<T>(
{required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteIdentityPublicKey,
required FutureOr<T> Function(DHTRecord) callback,
TypedKey? existingConversationRecordKey}) async {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final crypto = await getConversationCrypto( // //
activeAccountInfo: activeAccountInfo, // //
remoteIdentityPublicKey: remoteIdentityPublicKey); // //
final writer = activeAccountInfo.getConversationWriter(); // //
// Open with SMPL scheme for identity writer // @riverpod
late final DHTRecord localConversationRecord; // class ActiveConversationMessages extends _$ActiveConversationMessages {
if (existingConversationRecordKey != null) { // /// Get message for active conversation
localConversationRecord = await pool.openWrite( // @override
existingConversationRecordKey, writer, // FutureOr<IList<proto.Message>?> build() async {
parent: accountRecordKey, crypto: crypto); // await eventualVeilid.future;
} 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);
}
return 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 = activeAccountInfo.account.profile
..identityMasterJson =
jsonEncode(activeAccountInfo.localAccount.identityMaster.toJson())
..messages = messages.record.key.toProto();
// // final activeChat = ref.watch(activeChatStateProvider);
final update = await localConversation.tryWriteProtobuf( // if (activeChat == null) {
proto.Conversation.fromBuffer, conversation); // return null;
if (update != null) { // }
throw Exception('Failed to write local conversation');
}
return await callback(localConversation);
});
});
}
// // final activeAccountInfo =
// // await ref.watch(fetchActiveAccountProvider.future);
// // if (activeAccountInfo == null) {
// // return null;
// }
@riverpod // final contactList = ref.watch(fetchContactListProvider).asData?.value ??
class ActiveConversationMessages extends _$ActiveConversationMessages { // const IListConst([]);
/// Get message for active conversation
@override
FutureOr<IList<proto.Message>?> build() async {
await eventualVeilid.future;
final activeChat = ref.watch(activeChatStateProvider); // final activeChatContactIdx = contactList.indexWhere(
if (activeChat == null) { // (c) =>
return null; // 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);
final activeAccountInfo = // return await getLocalConversationMessages(
await ref.watch(fetchActiveAccountProvider.future); // activeAccountInfo: activeAccountInfo,
if (activeAccountInfo == null) { // localConversationRecordKey: localConversationRecordKey,
return null; // remoteIdentityPublicKey: remoteIdentityPublicKey,
} // );
// }
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,
);
}
}

View File

@ -0,0 +1 @@
export 'conversation.dart';

View File

@ -61,12 +61,7 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
return BlocProvider( return BlocProvider(
create: (context) => AccountRecordCubit( create: (context) => AccountRecordCubit(
record: accountInfo.activeAccountInfo!.accountRecord), record: accountInfo.activeAccountInfo!.accountRecord),
child: context.watch<AccountRecordCubit>().state.builder( child: HomeAccountReady());
(context, account) => HomeAccountReady(
localAccounts: localAccounts,
activeUserLogin: activeUserLogin,
activeAccountInfo: accountInfo.activeAccountInfo!,
account: account)));
} }
} }

View File

@ -1,13 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:equatable/equatable.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart'; import '../../../account_manager/account_manager.dart';
import '../../../contact_invitation/contact_invitation.dart'; import '../../../contact_invitation/contact_invitation.dart';
@ -19,11 +17,13 @@ import 'main_pager/main_pager.dart';
class HomeAccountReady extends StatefulWidget { class HomeAccountReady extends StatefulWidget {
const HomeAccountReady( const HomeAccountReady(
{required ActiveAccountInfo activeAccountInfo, {required ActiveAccountInfo activeAccountInfo,
required Account account, required proto.Account account,
super.key}) super.key})
: _accountReadyContext = accountReadyContext; : _activeAccountInfo = activeAccountInfo,
_account = account;
final AccountReadyContext _accountReadyContext; final ActiveAccountInfo _activeAccountInfo;
final proto.Account _account;
@override @override
HomeAccountReadyState createState() => HomeAccountReadyState(); HomeAccountReadyState createState() => HomeAccountReadyState();
@ -31,32 +31,10 @@ class HomeAccountReady extends StatefulWidget {
class HomeAccountReadyState extends State<HomeAccountReady> class HomeAccountReadyState extends State<HomeAccountReady>
with TickerProviderStateMixin { with TickerProviderStateMixin {
//
ContactInvitationRepository? _contactInvitationRepository;
// //
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Async initialize repositories for the active user
// xxx: this should not be necessary
// xxx: but RepositoryProvider doesn't call dispose()
Future.delayed(Duration.zero, () async {
//
final cir = await ContactInvitationRepository.open(
widget.activeAccountInfo, widget._accountReadyContext.account);
setState(() {
_contactInvitationRepository = cir;
});
});
}
@override
void dispose() {
super.dispose();
_contactInvitationRepository?.dispose();
} }
Widget buildUnlockAccount( Widget buildUnlockAccount(
@ -87,11 +65,11 @@ class HomeAccountReadyState extends State<HomeAccountReady>
context.go('/home/settings'); context.go('/home/settings');
}).paddingLTRB(0, 0, 8, 0), }).paddingLTRB(0, 0, 8, 0),
ProfileWidget( ProfileWidget(
name: widget._accountReadyContext.account.profile.name, name: widget._account.profile.name,
pronouns: widget._accountReadyContext.account.profile.pronouns, pronouns: widget._account.profile.pronouns,
).expanded(), ).expanded(),
]).paddingAll(8), ]).paddingAll(8),
MainPager().expanded() const MainPager().expanded()
]); ]);
} }
@ -129,18 +107,14 @@ class HomeAccountReadyState extends State<HomeAccountReady>
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => BlocProvider(
if (_contactInvitationRepository == null) { create: (context) => ContactInvitationListCubit(
return waitingPage(context); activeAccountInfo: widget._activeAccountInfo,
} account: widget._account),
child: responsiveVisibility(
return RepositoryProvider.value( context: context,
value: _contactInvitationRepository, phone: false,
child: responsiveVisibility( )
context: context, ? buildTablet(context)
phone: false, : buildPhone(context));
)
? buildTablet(context)
: buildPhone(context));
}
} }

View File

@ -2,11 +2,11 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../../../account_manager/account_manager.dart'; import '../../../../contact_invitation/contact_invitation.dart';
import '../../../../proto/proto.dart' as proto;
import '../../../../theme/theme.dart'; import '../../../../theme/theme.dart';
class AccountPage extends StatefulWidget { class AccountPage extends StatefulWidget {
@ -39,19 +39,17 @@ class AccountPageState extends State<AccountPage> {
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
final records = widget.account.contactInvitationRecords;
final contactInvitationRecordList = final contactInvitationRecordList =
ref.watch(fetchContactInvitationRecordsProvider).asData?.value ?? context.watch<ContactInvitationListCubit>().state.data?.value ??
const IListConst([]); const IListConst([]);
final contactList = ref.watch(fetchContactListProvider).asData?.value ?? final contactList = context.watch<ContactListCubit>().state.data?.value ??
const IListConst([]); const IListConst([]);
return SizedBox( return SizedBox(
child: Column(children: <Widget>[ child: Column(children: <Widget>[
if (contactInvitationRecordList.isNotEmpty) if (contactInvitationRecordList.isNotEmpty)
ExpansionTile( ExpansionTile(
tilePadding: EdgeInsets.fromLTRB(8, 0, 8, 0), tilePadding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
backgroundColor: scale.primaryScale.border, backgroundColor: scale.primaryScale.border,
collapsedBackgroundColor: scale.primaryScale.border, collapsedBackgroundColor: scale.primaryScale.border,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(

View File

@ -1,7 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
@ -9,41 +7,22 @@ import 'package:flutter_translate/flutter_translate.dart';
import 'package:preload_page_view/preload_page_view.dart'; import 'package:preload_page_view/preload_page_view.dart';
import 'package:stylish_bottom_bar/model/bar_items.dart'; import 'package:stylish_bottom_bar/model/bar_items.dart';
import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../../proto/proto.dart' as proto; import '../../../../contact_invitation/contact_invitation.dart';
import '../../../../theme/theme.dart';
import '../../../../tools/tools.dart'; import '../../../../tools/tools.dart';
import '../../../account_manager/account_manager.dart';
import '../../../contact_invitation/contact_invitation.dart';
import '../../../theme/theme.dart';
import 'account_page.dart'; import 'account_page.dart';
import 'bottom_sheet_action_button.dart'; import 'bottom_sheet_action_button.dart';
import 'chats_page.dart'; import 'chats_page.dart';
class MainPager extends StatefulWidget { class MainPager extends StatefulWidget {
const MainPager( const MainPager({super.key});
{required this.localAccounts,
required this.activeUserLogin,
required this.account,
super.key});
final IList<LocalAccount> localAccounts;
final TypedKey activeUserLogin;
final proto.Account account;
@override @override
MainPagerState createState() => MainPagerState(); MainPagerState createState() => MainPagerState();
static MainPagerState? of(BuildContext context) => static MainPagerState? of(BuildContext context) =>
context.findAncestorStateOfType<MainPagerState>(); context.findAncestorStateOfType<MainPagerState>();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(IterableProperty<LocalAccount>('localAccounts', localAccounts))
..add(DiagnosticsProperty<TypedKey>('activeUserLogin', activeUserLogin))
..add(DiagnosticsProperty<proto.Account>('account', account));
}
} }
class MainPagerState extends State<MainPager> with TickerProviderStateMixin { class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
@ -187,12 +166,9 @@ class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
_currentPage = index; _currentPage = index;
}); });
}, },
children: [ children: const [
AccountPage( AccountPage(),
localAccounts: widget.localAccounts, ChatsPage(),
activeUserLogin: widget.activeUserLogin,
account: widget.account),
const ChatsPage(),
])), ])),
// appBar: AppBar( // appBar: AppBar(
// toolbarHeight: 24, // toolbarHeight: 24,

View File

@ -1,55 +0,0 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:circular_profile_avatar/circular_profile_avatar.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart';
import '../entities/local_account.dart';
import '../providers/logins.dart';
class AccountBubble extends ConsumerWidget {
const AccountBubble({required this.account, super.key});
final LocalAccount account;
@override
Widget build(BuildContext context, WidgetRef ref) {
windowManager.setTitleBarStyle(TitleBarStyle.normal);
final logins = ref.watch(loginsProvider);
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Expanded(
flex: 4,
child: CircularProfileAvatar('',
child: Container(color: Theme.of(context).disabledColor))),
const Expanded(child: Text('Placeholder'))
]));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<LocalAccount>('account', account));
}
}
class AddAccountBubble extends ConsumerWidget {
const AddAccountBubble({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
windowManager.setTitleBarStyle(TitleBarStyle.normal);
final logins = ref.watch(loginsProvider);
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
CircularProfileAvatar('',
borderWidth: 4,
borderColor: Theme.of(context).unselectedWidgetColor,
child: Container(
color: Colors.blue, child: const Icon(Icons.add, size: 50))),
const Text('Add Account').paddingLTRB(0, 4, 0, 0)
]);
}
}

View File

@ -1,56 +0,0 @@
import 'dart:typed_data';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../entities/local_account.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../../../packages/veilid_support/veilid_support.dart';
import 'account.dart';
import 'conversation.dart';
part 'contact_invite.g.dart';
/// Get the active account contact invitation list
@riverpod
Future<IList<proto.ContactInvitationRecord>?> fetchContactInvitationRecords(
FetchContactInvitationRecordsRef ref) async {
// See if we've logged into this account or if it is locked
final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
return null;
}
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Decode the contact invitation list from the DHT
IList<proto.ContactInvitationRecord> out = const IListConst([]);
try {
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactInvitationRecords),
parent: accountRecordKey))
.scope((cirList) async {
for (var i = 0; i < cirList.length; i++) {
final cir = await cirList.getItem(i);
if (cir == null) {
throw Exception('Failed to get contact invitation record');
}
out = out.add(proto.ContactInvitationRecord.fromBuffer(cir));
}
});
} on VeilidAPIExceptionTryAgain catch (_) {
// Try again later
ref.invalidateSelf();
return null;
} on Exception catch (_) {
// Try again later
ref.invalidateSelf();
rethrow;
}
return out;
}

View File

@ -2,6 +2,7 @@ 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';
@ -41,6 +42,19 @@ class DHTShortArrayCubit<T> extends Cubit<AsyncValue<IList<T>>> {
}); });
} }
Future<void> refresh({bool forceRefresh = false}) async {
var out = IList<T>();
// xxx could be parallelized but we need to watch out for rate limits
for (var i = 0; i < _shortArray.length; i++) {
final cir = await _shortArray.getItem(i, forceRefresh: forceRefresh);
if (cir == null) {
throw Exception('Failed to get short array element');
}
out = out.add(_decodeElement(cir));
}
emit(AsyncValue.data(out));
}
void _update() { void _update() {
// Run at most one background update process // Run at most one background update process
_wantsUpdate = true; _wantsUpdate = true;
@ -91,6 +105,9 @@ class DHTShortArrayCubit<T> extends Cubit<AsyncValue<IList<T>>> {
await super.close(); await super.close();
} }
@protected
DHTShortArray get shortArray => _shortArray;
late final DHTShortArray _shortArray; late final DHTShortArray _shortArray;
final T Function(List<int> data) _decodeElement; final T Function(List<int> data) _decodeElement;
StreamSubscription<void>? _subscription; StreamSubscription<void>? _subscription;