state follower

This commit is contained in:
Christien Rioux 2024-02-20 17:57:05 -05:00
parent f936cb069e
commit 450bdf9c7c
20 changed files with 787 additions and 128 deletions

View File

@ -1,13 +1,12 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
class ActiveChatCubit extends Cubit<TypedKey?> { import '../../tools/tools.dart';
ActiveChatCubit(super.initialState, this.setHasActiveChat);
class ActiveChatCubit extends Cubit<TypedKey?> with BlocTools {
ActiveChatCubit(super.initialState);
void setActiveChat(TypedKey? activeChatRemoteConversationRecordKey) { void setActiveChat(TypedKey? activeChatRemoteConversationRecordKey) {
setHasActiveChat(activeChatRemoteConversationRecordKey != null);
emit(activeChatRemoteConversationRecordKey); emit(activeChatRemoteConversationRecordKey);
} }
void Function(bool) setHasActiveChat;
} }

View File

@ -52,7 +52,7 @@ class ChatComponent extends StatelessWidget {
if (contactList == null) { if (contactList == null) {
return debugPage('should always have a contact list here'); return debugPage('should always have a contact list here');
} }
final avconversation = context.select<ActiveConversationsCubit, final avconversation = context.select<ActiveConversationsBlocMapCubit,
AsyncValue<ActiveConversationState>?>( AsyncValue<ActiveConversationState>?>(
(x) => x.state[remoteConversationRecordKey]); (x) => x.state[remoteConversationRecordKey]);
if (avconversation == null) { if (avconversation == null) {

View File

@ -8,7 +8,7 @@ import '../../account_manager/account_manager.dart';
import '../../chat/chat.dart'; import '../../chat/chat.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import 'active_conversations_cubit.dart'; import 'active_conversations_bloc_map_cubit.dart';
class ActiveConversationMessagesCubit extends BlocMapCubit<TypedKey, class ActiveConversationMessagesCubit extends BlocMapCubit<TypedKey,
AsyncValue<IList<proto.Message>>, MessagesCubit> { AsyncValue<IList<proto.Message>>, MessagesCubit> {

View File

@ -32,9 +32,10 @@ typedef ActiveConversationsBlocMapState
// Map of remoteConversationRecordKey to ActiveConversationCubit // Map of remoteConversationRecordKey to ActiveConversationCubit
// Wraps a conversation cubit to only expose completely built conversations // Wraps a conversation cubit to only expose completely built conversations
class ActiveConversationsCubit extends BlocMapCubit<TypedKey, class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<ActiveConversationState>, ActiveConversationCubit> { AsyncValue<ActiveConversationState>, ActiveConversationCubit> {
ActiveConversationsCubit({required ActiveAccountInfo activeAccountInfo}) ActiveConversationsBlocMapCubit(
{required ActiveAccountInfo activeAccountInfo})
: _activeAccountInfo = activeAccountInfo; : _activeAccountInfo = activeAccountInfo;
// Add an active conversation to be tracked for changes // Add an active conversation to be tracked for changes

View File

@ -1,3 +1,3 @@
export 'active_conversation_messages_cubit.dart'; export 'active_conversation_messages_cubit.dart';
export 'active_conversations_cubit.dart'; export 'active_conversations_bloc_map_cubit.dart';
export 'chat_list_cubit.dart'; export 'chat_list_cubit.dart';

View File

@ -69,7 +69,7 @@ class ChatSingleContactItemWidget extends StatelessWidget {
child: ListTile( child: ListTile(
onTap: () { onTap: () {
final activeConversationsCubit = final activeConversationsCubit =
context.read<ActiveConversationsCubit>(); context.read<ActiveConversationsBlocMapCubit>();
singleFuture(activeChatCubit, () async { singleFuture(activeChatCubit, () async {
await activeConversationsCubit.addConversation( await activeConversationsCubit.addConversation(
contact: _contact); contact: _contact);

View File

@ -5,9 +5,7 @@ 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 '../models/models.dart'; import '../models/models.dart';
////////////////////////////////////////////////// //////////////////////////////////////////////////
@ -22,12 +20,6 @@ typedef GetEncryptionKeyCallback = Future<SecretKey?> Function(
EncryptionKeyType encryptionKeyType, EncryptionKeyType encryptionKeyType,
Uint8List encryptedSecret); Uint8List encryptedSecret);
@immutable
class InvitationStatus {
const InvitationStatus({required this.acceptedContact});
final AcceptedContact? acceptedContact;
}
////////////////////////////////////////////////// //////////////////////////////////////////////////
////////////////////////////////////////////////// //////////////////////////////////////////////////
@ -271,109 +263,109 @@ class ContactInvitationListCubit
return out; return out;
} }
Future<InvitationStatus?> checkInvitationStatus( // Future<InvitationStatus?> checkInvitationStatus(
{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 = _activeAccountInfo // final accountRecordKey = _activeAccountInfo
.userLogin.accountRecordInfo.accountRecord.recordKey; // .userLogin.accountRecordInfo.accountRecord.recordKey;
final writerKey = contactInvitationRecord.writerKey.toVeilid(); // final writerKey = contactInvitationRecord.writerKey.toVeilid();
final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); // final writerSecret = contactInvitationRecord.writerSecret.toVeilid();
final recordKey = // final recordKey =
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); // contactInvitationRecord.contactRequestInbox.recordKey.toVeilid();
final writer = TypedKeyPair( // final writer = TypedKeyPair(
kind: recordKey.kind, key: writerKey, secret: writerSecret); // kind: recordKey.kind, key: writerKey, secret: writerSecret);
final acceptReject = await (await pool.openRead(recordKey, // final acceptReject = await (await pool.openRead(recordKey,
crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), // crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer),
parent: accountRecordKey, // parent: accountRecordKey,
defaultSubkey: 1)) // defaultSubkey: 1))
.scope((contactRequestInbox) async { // .scope((contactRequestInbox) async {
// // //
final signedContactResponse = await contactRequestInbox.getProtobuf( // final signedContactResponse = await contactRequestInbox.getProtobuf(
proto.SignedContactResponse.fromBuffer, // proto.SignedContactResponse.fromBuffer,
forceRefresh: true); // forceRefresh: true);
if (signedContactResponse == null) { // if (signedContactResponse == null) {
return null; // return null;
} // }
final contactResponseBytes = // final contactResponseBytes =
Uint8List.fromList(signedContactResponse.contactResponse); // Uint8List.fromList(signedContactResponse.contactResponse);
final contactResponse = // final contactResponse =
proto.ContactResponse.fromBuffer(contactResponseBytes); // proto.ContactResponse.fromBuffer(contactResponseBytes);
final contactIdentityMasterRecordKey = // final contactIdentityMasterRecordKey =
contactResponse.identityMasterRecordKey.toVeilid(); // contactResponse.identityMasterRecordKey.toVeilid();
final cs = await pool.veilid.getCryptoSystem(recordKey.kind); // final cs = await pool.veilid.getCryptoSystem(recordKey.kind);
// Fetch the remote contact's account master // // Fetch the remote contact's account master
final contactIdentityMaster = await openIdentityMaster( // final contactIdentityMaster = await openIdentityMaster(
identityMasterRecordKey: contactIdentityMasterRecordKey); // identityMasterRecordKey: contactIdentityMasterRecordKey);
// Verify // // Verify
final signature = signedContactResponse.identitySignature.toVeilid(); // final signature = signedContactResponse.identitySignature.toVeilid();
await cs.verify(contactIdentityMaster.identityPublicKey, // await cs.verify(contactIdentityMaster.identityPublicKey,
contactResponseBytes, signature); // contactResponseBytes, signature);
// Check for rejection // // Check for rejection
if (!contactResponse.accept) { // if (!contactResponse.accept) {
return const InvitationStatus(acceptedContact: null); // return const InvitationStatus(acceptedContact: null);
} // }
// Pull profile from remote conversation key // // Pull profile from remote conversation key
final remoteConversationRecordKey = // final remoteConversationRecordKey =
contactResponse.remoteConversationRecordKey.toVeilid(); // contactResponse.remoteConversationRecordKey.toVeilid();
final conversation = ConversationCubit( // final conversation = ConversationCubit(
activeAccountInfo: _activeAccountInfo, // activeAccountInfo: _activeAccountInfo,
remoteIdentityPublicKey: // remoteIdentityPublicKey:
contactIdentityMaster.identityPublicTypedKey(), // contactIdentityMaster.identityPublicTypedKey(),
remoteConversationRecordKey: remoteConversationRecordKey); // remoteConversationRecordKey: remoteConversationRecordKey);
await conversation.refresh(); // await conversation.refresh();
final remoteConversation = // final remoteConversation =
conversation.state.data?.value.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 = // final localConversationRecordKey =
contactInvitationRecord.localConversationRecordKey.toVeilid(); // contactInvitationRecord.localConversationRecordKey.toVeilid();
return conversation.initLocalConversation( // return conversation.initLocalConversation(
existingConversationRecordKey: localConversationRecordKey, // existingConversationRecordKey: localConversationRecordKey,
profile: _account.profile, // profile: _account.profile,
// ignore: prefer_expression_function_bodies // // ignore: prefer_expression_function_bodies
callback: (localConversation) async { // callback: (localConversation) async {
return InvitationStatus( // return InvitationStatus(
acceptedContact: AcceptedContact( // acceptedContact: AcceptedContact(
remoteProfile: remoteConversation.profile, // remoteProfile: remoteConversation.profile,
remoteIdentity: contactIdentityMaster, // remoteIdentity: contactIdentityMaster,
remoteConversationRecordKey: remoteConversationRecordKey, // remoteConversationRecordKey: remoteConversationRecordKey,
localConversationRecordKey: localConversationRecordKey)); // localConversationRecordKey: localConversationRecordKey));
}); // });
}); // });
if (acceptReject == null) { // if (acceptReject == null) {
return null; // return null;
} // }
// Delete invitation and return the accepted or rejected contact // // Delete invitation and return the accepted or rejected contact
await deleteInvitation( // await deleteInvitation(
accepted: acceptReject.acceptedContact != null, // accepted: acceptReject.acceptedContact != null,
contactInvitationRecord: contactInvitationRecord); // contactInvitationRecord: contactInvitationRecord);
return acceptReject; // return acceptReject;
} on Exception catch (e) { // } on Exception catch (e) {
log.error('Exception in checkAcceptRejectContact: $e', e); // log.error('Exception in checkInvitationStatus: $e', e);
// Attempt to clean up. All this needs better lifetime management // // Attempt to clean up. All this needs better lifetime management
await deleteInvitation( // await deleteInvitation(
accepted: false, contactInvitationRecord: contactInvitationRecord); // accepted: false, contactInvitationRecord: contactInvitationRecord);
rethrow; // rethrow;
} // }
} // }
// //
final ActiveAccountInfo _activeAccountInfo; final ActiveAccountInfo _activeAccountInfo;

View File

@ -0,0 +1,151 @@
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto;
class ContactRequestInboxCubit
extends DefaultDHTRecordCubit<proto.SignedContactResponse> {
ContactRequestInboxCubit(
{required this.activeAccountInfo, required this.contactInvitationRecord})
: super(
open: () => _open(
activeAccountInfo: activeAccountInfo,
contactInvitationRecord: contactInvitationRecord),
decodeState: proto.SignedContactResponse.fromBuffer);
ContactRequestInboxCubit.value(
{required super.record,
required this.activeAccountInfo,
required this.contactInvitationRecord})
: super.value(decodeState: proto.SignedContactResponse.fromBuffer);
static Future<DHTRecord> _open(
{required ActiveAccountInfo activeAccountInfo,
required proto.ContactInvitationRecord contactInvitationRecord}) async {
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);
return pool.openRead(recordKey,
crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer),
parent: accountRecordKey,
defaultSubkey: 1);
}
final ActiveAccountInfo activeAccountInfo;
final proto.ContactInvitationRecord contactInvitationRecord;
}
// Future<InvitationStatus?> checkInvitationStatus(
// {}) async {
// // Open the contact request inbox
// try {
// 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 acceptReject = await (await pool.openRead(recordKey,
// crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer),
// parent: accountRecordKey,
// defaultSubkey: 1))
// .scope((contactRequestInbox) async {
// //
// final signedContactResponse = await contactRequestInbox.getProtobuf(
// proto.SignedContactResponse.fromBuffer,
// forceRefresh: true);
// if (signedContactResponse == null) {
// return null;
// }
// final contactResponseBytes =
// Uint8List.fromList(signedContactResponse.contactResponse);
// final contactResponse =
// proto.ContactResponse.fromBuffer(contactResponseBytes);
// final contactIdentityMasterRecordKey =
// contactResponse.identityMasterRecordKey.toVeilid();
// final cs = await pool.veilid.getCryptoSystem(recordKey.kind);
// // Fetch the remote contact's account master
// final contactIdentityMaster = await openIdentityMaster(
// identityMasterRecordKey: contactIdentityMasterRecordKey);
// // Verify
// final signature = signedContactResponse.identitySignature.toVeilid();
// await cs.verify(contactIdentityMaster.identityPublicKey,
// contactResponseBytes, signature);
// // Check for rejection
// if (!contactResponse.accept) {
// return const InvitationStatus(acceptedContact: null);
// }
// // Pull profile from remote conversation key
// final remoteConversationRecordKey =
// contactResponse.remoteConversationRecordKey.toVeilid();
// final conversation = ConversationCubit(
// activeAccountInfo: _activeAccountInfo,
// remoteIdentityPublicKey:
// contactIdentityMaster.identityPublicTypedKey(),
// remoteConversationRecordKey: remoteConversationRecordKey);
// await conversation.refresh();
// final remoteConversation =
// conversation.state.data?.value.remoteConversation;
// if (remoteConversation == null) {
// log.info('Remote conversation could not be read. Waiting...');
// return null;
// }
// // Complete the local conversation now that we have the remote profile
// final localConversationRecordKey =
// contactInvitationRecord.localConversationRecordKey.toVeilid();
// return conversation.initLocalConversation(
// existingConversationRecordKey: localConversationRecordKey,
// profile: _account.profile,
// // ignore: prefer_expression_function_bodies
// callback: (localConversation) async {
// return InvitationStatus(
// acceptedContact: AcceptedContact(
// remoteProfile: remoteConversation.profile,
// remoteIdentity: contactIdentityMaster,
// remoteConversationRecordKey: remoteConversationRecordKey,
// localConversationRecordKey: localConversationRecordKey));
// });
// });
// if (acceptReject == null) {
// return null;
// }
// // Delete invitation and return the accepted or rejected contact
// await deleteInvitation(
// accepted: acceptReject.acceptedContact != null,
// contactInvitationRecord: contactInvitationRecord);
// return acceptReject;
// } on Exception catch (e) {
// log.error('Exception in checkInvitationStatus: $e', e);
// // Attempt to clean up. All this needs better lifetime management
// await deleteInvitation(
// accepted: false, contactInvitationRecord: contactInvitationRecord);
// rethrow;
// }

View File

@ -1 +1,4 @@
export 'contact_invitation_list_cubit.dart'; export 'contact_invitation_list_cubit.dart';
export 'contact_request_inbox_cubit.dart';
export 'waiting_invitation_cubit.dart';
export 'waiting_invitations_bloc_map_cubit.dart';

View File

@ -0,0 +1,221 @@
import 'dart:typed_data';
import 'package:async_tools/async_tools.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../models/accepted_contact.dart';
import 'contact_request_inbox_cubit.dart';
@immutable
class InvitationStatus extends Equatable {
const InvitationStatus({required this.acceptedContact});
final AcceptedContact? acceptedContact;
@override
List<Object?> get props => [acceptedContact];
}
class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
proto.SignedContactResponse> {
WaitingInvitationCubit(ContactRequestInboxCubit super.input,
{required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
required proto.ContactInvitationRecord contactInvitationRecord})
: super(
transform: (signedContactResponse) => _transform(
signedContactResponse,
activeAccountInfo: activeAccountInfo,
account: account,
contactInvitationRecord: contactInvitationRecord));
static Future<AsyncValue<InvitationStatus>> _transform(
proto.SignedContactResponse signedContactResponse,
{required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
required proto.ContactInvitationRecord contactInvitationRecord}) async {
final pool = DHTRecordPool.instance;
final contactResponseBytes =
Uint8List.fromList(signedContactResponse.contactResponse);
final contactResponse =
proto.ContactResponse.fromBuffer(contactResponseBytes);
final contactIdentityMasterRecordKey =
contactResponse.identityMasterRecordKey.toVeilid();
final cs =
await pool.veilid.getCryptoSystem(contactIdentityMasterRecordKey.kind);
// Fetch the remote contact's account master
final contactIdentityMaster = await openIdentityMaster(
identityMasterRecordKey: contactIdentityMasterRecordKey);
// Verify
final signature = signedContactResponse.identitySignature.toVeilid();
await cs.verify(contactIdentityMaster.identityPublicKey,
contactResponseBytes, signature);
// Check for rejection
if (!contactResponse.accept) {
// Rejection
return const AsyncValue.data(InvitationStatus(acceptedContact: null));
}
// Pull profile from remote conversation key
final remoteConversationRecordKey =
contactResponse.remoteConversationRecordKey.toVeilid();
final conversation = ConversationCubit(
activeAccountInfo: activeAccountInfo,
remoteIdentityPublicKey: contactIdentityMaster.identityPublicTypedKey(),
remoteConversationRecordKey: remoteConversationRecordKey);
// wait for remote conversation for up to 20 seconds
proto.Conversation? remoteConversation;
var retryCount = 20;
do {
await conversation.refresh();
remoteConversation = conversation.state.data?.value.remoteConversation;
if (remoteConversation != null) {
break;
}
log.info('Remote conversation could not be read. Waiting...');
await Future<void>.delayed(const Duration(seconds: 1));
retryCount--;
} while (retryCount > 0);
if (remoteConversation == null) {
return AsyncValue.error('Invitation accept timed out.');
}
// Complete the local conversation now that we have the remote profile
final remoteProfile = remoteConversation.profile;
final localConversationRecordKey =
contactInvitationRecord.localConversationRecordKey.toVeilid();
return conversation.initLocalConversation(
existingConversationRecordKey: localConversationRecordKey,
profile: account.profile,
// ignore: prefer_expression_function_bodies
callback: (localConversation) async {
return AsyncValue.data(InvitationStatus(
acceptedContact: AcceptedContact(
remoteProfile: remoteProfile,
remoteIdentity: contactIdentityMaster,
remoteConversationRecordKey: remoteConversationRecordKey,
localConversationRecordKey: localConversationRecordKey)));
});
}
}
// Future<InvitationStatus?> checkInvitationStatus(
// {}) async {
// // Open the contact request inbox
// try {
// 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 acceptReject = await (await pool.openRead(recordKey,
// crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer),
// parent: accountRecordKey,
// defaultSubkey: 1))
// .scope((contactRequestInbox) async {
// //
// final signedContactResponse = await contactRequestInbox.getProtobuf(
// proto.SignedContactResponse.fromBuffer,
// forceRefresh: true);
// if (signedContactResponse == null) {
// return null;
// }
// final contactResponseBytes =
// Uint8List.fromList(signedContactResponse.contactResponse);
// final contactResponse =
// proto.ContactResponse.fromBuffer(contactResponseBytes);
// final contactIdentityMasterRecordKey =
// contactResponse.identityMasterRecordKey.toVeilid();
// final cs = await pool.veilid.getCryptoSystem(recordKey.kind);
// // Fetch the remote contact's account master
// final contactIdentityMaster = await openIdentityMaster(
// identityMasterRecordKey: contactIdentityMasterRecordKey);
// // Verify
// final signature = signedContactResponse.identitySignature.toVeilid();
// await cs.verify(contactIdentityMaster.identityPublicKey,
// contactResponseBytes, signature);
// // Check for rejection
// if (!contactResponse.accept) {
// return const InvitationStatus(acceptedContact: null);
// }
// // Pull profile from remote conversation key
// final remoteConversationRecordKey =
// contactResponse.remoteConversationRecordKey.toVeilid();
// final conversation = ConversationCubit(
// activeAccountInfo: _activeAccountInfo,
// remoteIdentityPublicKey:
// contactIdentityMaster.identityPublicTypedKey(),
// remoteConversationRecordKey: remoteConversationRecordKey);
// await conversation.refresh();
// final remoteConversation =
// conversation.state.data?.value.remoteConversation;
// if (remoteConversation == null) {
// log.info('Remote conversation could not be read. Waiting...');
// return null;
// }
// // Complete the local conversation now that we have the remote profile
// final localConversationRecordKey =
// contactInvitationRecord.localConversationRecordKey.toVeilid();
// return conversation.initLocalConversation(
// existingConversationRecordKey: localConversationRecordKey,
// profile: _account.profile,
// // ignore: prefer_expression_function_bodies
// callback: (localConversation) async {
// return InvitationStatus(
// acceptedContact: AcceptedContact(
// remoteProfile: remoteConversation.profile,
// remoteIdentity: contactIdentityMaster,
// remoteConversationRecordKey: remoteConversationRecordKey,
// localConversationRecordKey: localConversationRecordKey));
// });
// });
// if (acceptReject == null) {
// return null;
// }
// // Delete invitation and return the accepted or rejected contact
// await deleteInvitation(
// accepted: acceptReject.acceptedContact != null,
// contactInvitationRecord: contactInvitationRecord);
// return acceptReject;
// } on Exception catch (e) {
// log.error('Exception in checkInvitationStatus: $e', e);
// // Attempt to clean up. All this needs better lifetime management
// await deleteInvitation(
// accepted: false, contactInvitationRecord: contactInvitationRecord);
// rethrow;
// }

View File

@ -0,0 +1,58 @@
import 'package:async_tools/async_tools.dart';
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';
import 'cubits.dart';
typedef WaitingInvitationsBlocMapState
= BlocMapState<TypedKey, AsyncValue<InvitationStatus>>;
// Map of contactInvitationListRecordKey to WaitingInvitationCubit
// Wraps a contact invitation cubit to watch for accept/reject
// Automatically follows the state of a ContactInvitiationListCubit.
class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<InvitationStatus>, WaitingInvitationCubit>
with
StateFollower<AsyncValue<IList<proto.ContactInvitationRecord>>,
TypedKey, proto.ContactInvitationRecord> {
WaitingInvitationsBlocMapCubit(
{required this.activeAccountInfo, required this.account});
Future<void> addWaitingInvitation(
{required proto.ContactInvitationRecord
contactInvitationRecord}) async =>
add(() => MapEntry(
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(),
WaitingInvitationCubit(
ContactRequestInboxCubit(
activeAccountInfo: activeAccountInfo,
contactInvitationRecord: contactInvitationRecord),
activeAccountInfo: activeAccountInfo,
account: account,
contactInvitationRecord: contactInvitationRecord)));
final ActiveAccountInfo activeAccountInfo;
final proto.Account account;
/// StateFollower /////////////////////////
@override
IMap<TypedKey, proto.ContactInvitationRecord> getStateMap(
AsyncValue<IList<proto.ContactInvitationRecord>> avstate) {
final state = avstate.data?.value;
if (state == null) {
return IMap();
}
return IMap.fromIterable(state,
keyMapper: (e) => e.contactRequestInbox.recordKey.toVeilid(),
valueMapper: (e) => e);
}
@override
Future<void> removeFromState(TypedKey key) => remove(key);
@override
Future<void> updateState(TypedKey key, proto.ContactInvitationRecord value) =>
addWaitingInvitation(contactInvitationRecord: value);
}

View File

@ -1,10 +1,11 @@
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
@immutable @immutable
class AcceptedContact { class AcceptedContact extends Equatable {
const AcceptedContact({ const AcceptedContact({
required this.remoteProfile, required this.remoteProfile,
required this.remoteIdentity, required this.remoteIdentity,
@ -16,4 +17,12 @@ class AcceptedContact {
final IdentityMaster remoteIdentity; final IdentityMaster remoteIdentity;
final TypedKey remoteConversationRecordKey; final TypedKey remoteConversationRecordKey;
final TypedKey localConversationRecordKey; final TypedKey localConversationRecordKey;
@override
List<Object?> get props => [
remoteProfile,
remoteIdentity,
remoteConversationRecordKey,
localConversationRecordKey
];
} }

View File

@ -80,20 +80,6 @@ class ContactItemWidget extends StatelessWidget {
await MainPager.of(context)?.pageController.animateToPage(1, await MainPager.of(context)?.pageController.animateToPage(1,
duration: 250.ms, curve: Curves.easeInOut); duration: 250.ms, curve: Curves.easeInOut);
} }
// // ignore: use_build_context_synchronously
// if (!context.mounted) {
// return;
// }
// await showDialog<void>(
// context: context,
// builder: (context) => ContactInvitationDisplayDialog(
// name: activeAccountInfo.localAccount.name,
// message: contactInvitationRecord.message,
// generator: Uint8List.fromList(
// contactInvitationRecord.invitation),
// ));
// }
}, },
title: Text(contact.editedProfile.name), title: Text(contact.editedProfile.name),
subtitle: (contact.editedProfile.pronouns.isNotEmpty) subtitle: (contact.editedProfile.pronouns.isNotEmpty)
@ -101,7 +87,6 @@ class ContactItemWidget extends StatelessWidget {
: null, : null,
iconColor: scale.tertiaryScale.background, iconColor: scale.tertiaryScale.background,
textColor: scale.tertiaryScale.text, textColor: scale.tertiaryScale.text,
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
leading: const Icon(Icons.person)))); leading: const Icon(Icons.person))));
} }

View File

@ -61,11 +61,16 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
activeAccountInfo: activeAccountInfo, activeAccountInfo: activeAccountInfo,
account: account)), account: account)),
BlocProvider( BlocProvider(
create: (context) => ActiveConversationsCubit( create: (context) => ActiveConversationsBlocMapCubit(
activeAccountInfo: activeAccountInfo)), activeAccountInfo: activeAccountInfo)),
BlocProvider( BlocProvider(
create: (context) => create: (context) => ActiveChatCubit(null)
ActiveChatCubit(null, routerCubit.setHasActiveChat)) ..withStateListen((event) {
routerCubit.setHasActiveChat(event != null);
})),
BlocProvider(
create: (context) => WaitingInvitationsBlocMapCubit(
activeAccountInfo: activeAccountInfo, account: account))
], child: widget.child); ], child: widget.child);
}))); })));
} }

View File

@ -0,0 +1,62 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:bloc/bloc.dart';
class AsyncTransformerCubit<T, S> extends Cubit<AsyncValue<T>> {
AsyncTransformerCubit(this.input, {required this.transform})
: super(const AsyncValue.loading()) {
_asyncTransform(input.state);
_subscription = input.stream.listen(_asyncTransform);
}
void _asyncTransform(AsyncValue<S> newInputState) {
// Use a singlefuture here to ensure we get dont lose any updates
// If the input stream gives us an update while we are
// still processing the last update, the most recent input state will
// be saved and processed eventually.
singleFuture(this, () async {
var newState = newInputState;
var done = false;
while (!done) {
// Emit the transformed state
try {
if (newState is AsyncLoading) {
return AsyncValue<T>.loading();
}
if (newState is AsyncError) {
final newStateError = newState as AsyncError<S>;
return AsyncValue<T>.error(
newStateError.error, newStateError.stackTrace);
}
final transformedState = await transform(newState.data!.value);
emit(transformedState);
} on Exception catch (e, st) {
emit(AsyncValue.error(e, st));
}
// See if there's another state change to process
final next = _nextInputState;
_nextInputState = null;
if (next != null) {
newState = next;
} else {
done = true;
}
}
}, onBusy: () {
// Keep this state until we process again
_nextInputState = newInputState;
});
}
@override
Future<void> close() async {
await _subscription.cancel();
await input.close();
await super.close();
}
Cubit<AsyncValue<S>> input;
AsyncValue<S>? _nextInputState;
Future<AsyncValue<T>> Function(S) transform;
late final StreamSubscription<AsyncValue<S>> _subscription;
}

View File

@ -12,6 +12,14 @@ class _ItemEntry<S, B> {
final StreamSubscription<S> subscription; final StreamSubscription<S> subscription;
} }
// Streaming container cubit that is a map from some immutable key
// to a some other cubit's output state. Output state for this container
// cubit is an immutable map of the key to the output state of the contained
// cubits.
//
// K = Key type for the bloc map, used to look up some mapped cubit
// S = State type for the value, keys will look up values of this type
// B = Bloc/cubit type for the value, output states of type S
abstract class BlocMapCubit<K, S, B extends BlocBase<S>> abstract class BlocMapCubit<K, S, B extends BlocBase<S>>
extends Cubit<BlocMapState<K, S>> { extends Cubit<BlocMapState<K, S>> {
BlocMapCubit() BlocMapCubit()

12
lib/tools/bloc_tools.dart Normal file
View File

@ -0,0 +1,12 @@
import 'package:bloc/bloc.dart';
mixin BlocTools<State> on BlocBase<State> {
void withStateListen(void Function(State event)? onData,
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
if (onData != null) {
onData(state);
}
stream.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
}

View File

@ -0,0 +1,78 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
// Mixin that automatically keeps two blocs/cubits in sync with each other
// Useful for having a BlocMapCubit 'follow' the state of another input cubit.
// As the state of the input cubit changes, the BlocMapCubit can add/remove
// mapped Cubits that automatically process the input state reactively.
//
// S = Input state type
// K = Key derived from elements of input state
// V = Value derived from elements of input state
abstract mixin class StateFollower<S extends Object, K, V> {
void follow({
required S initialInputState,
required Stream<S> stream,
}) {
//
_lastInputStateMap = getStateMap(initialInputState);
_subscription = stream.listen(_updateFollow);
}
Future<void> close() async {
await _subscription.cancel();
}
IMap<K, V> getStateMap(S state);
Future<void> removeFromState(K key);
Future<void> updateState(K key, V value);
void _updateFollow(S newInputState) {
// Use a singlefuture here to ensure we get dont lose any updates
// If the input stream gives us an update while we are
// still processing the last update, the most recent input state will
// be saved and processed eventually.
final newInputStateMap = getStateMap(newInputState);
singleFuture(this, () async {
var newStateMap = newInputStateMap;
var done = false;
while (!done) {
for (final k in _lastInputStateMap.keys) {
if (!newStateMap.containsKey(k)) {
// deleted
await removeFromState(k);
}
}
for (final newEntry in newStateMap.entries) {
final v = _lastInputStateMap.get(newEntry.key);
if (v == null || v != newEntry.value) {
// added or changed
await updateState(newEntry.key, newEntry.value);
}
}
// Keep this state map for the next time
_lastInputStateMap = newStateMap;
// See if there's another state change to process
final next = _nextInputStateMap;
_nextInputStateMap = null;
if (next != null) {
newStateMap = next;
} else {
done = true;
}
}
}, onBusy: () {
// Keep this state until we process again
_nextInputStateMap = newInputStateMap;
});
}
late IMap<K, V> _lastInputStateMap;
IMap<K, V>? _nextInputStateMap;
late final StreamSubscription<S> _subscription;
}

View File

@ -1,5 +1,7 @@
export 'animations.dart'; export 'animations.dart';
export 'async_transformer_cubit.dart';
export 'bloc_map_cubit.dart'; export 'bloc_map_cubit.dart';
export 'bloc_tools.dart';
export 'enter_password.dart'; export 'enter_password.dart';
export 'enter_pin.dart'; export 'enter_pin.dart';
export 'future_cubit.dart'; export 'future_cubit.dart';
@ -8,6 +10,7 @@ export 'phono_byte.dart';
export 'responsive.dart'; export 'responsive.dart';
export 'scanner_error_widget.dart'; export 'scanner_error_widget.dart';
export 'shared_preferences.dart'; export 'shared_preferences.dart';
export 'state_follower.dart';
export 'state_logger.dart'; export 'state_logger.dart';
export 'stream_listenable.dart'; export 'stream_listenable.dart';
export 'stream_wrapper_cubit.dart'; export 'stream_wrapper_cubit.dart';

View File

@ -0,0 +1,72 @@
import 'dart:math';
import 'dart:typed_data';
/// Compares two [Uint8List] contents for equality by comparing words at a time.
/// Returns true if this == other
extension Uint8ListCompare on Uint8List {
bool equals(Uint8List other) {
if (identical(this, other)) {
return true;
}
if (length != other.length) {
return false;
}
final words = buffer.asUint32List();
final otherwords = other.buffer.asUint32List();
final wordLen = words.length;
var i = 0;
for (; i < wordLen; i++) {
if (words[i] != otherwords[i]) {
break;
}
}
i <<= 2;
for (; i < length; i++) {
if (this[i] != other[i]) {
return false;
}
}
return true;
}
/// Compares two [Uint8List] contents for
/// numeric ordering by comparing words at a time.
/// Returns -1 for this < other, 1 for this > other, and 0 for this == other.
int compare(Uint8List other) {
if (identical(this, other)) {
return 0;
}
final words = buffer.asUint32List();
final otherwords = other.buffer.asUint32List();
final minWordLen = min(words.length, otherwords.length);
var i = 0;
for (; i < minWordLen; i++) {
if (words[i] != otherwords[i]) {
break;
}
}
i <<= 2;
final minLen = min(length, other.length);
for (; i < minLen; i++) {
final a = this[i];
final b = other[i];
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
}
if (length < other.length) {
return -1;
}
if (length > other.length) {
return 1;
}
return 0;
}
}