mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-10-01 06:55:46 -04:00
state follower
This commit is contained in:
parent
f936cb069e
commit
450bdf9c7c
@ -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;
|
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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> {
|
||||||
|
@ -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
|
@ -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';
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
151
lib/contact_invitation/cubits/contact_request_inbox_cubit.dart
Normal file
151
lib/contact_invitation/cubits/contact_request_inbox_cubit.dart
Normal 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;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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';
|
||||||
|
221
lib/contact_invitation/cubits/waiting_invitation_cubit.dart
Normal file
221
lib/contact_invitation/cubits/waiting_invitation_cubit.dart
Normal 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;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
@ -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
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
@ -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))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
62
lib/tools/async_transformer_cubit.dart
Normal file
62
lib/tools/async_transformer_cubit.dart
Normal 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;
|
||||||
|
}
|
@ -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
12
lib/tools/bloc_tools.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
78
lib/tools/state_follower.dart
Normal file
78
lib/tools/state_follower.dart
Normal 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;
|
||||||
|
}
|
@ -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';
|
||||||
|
72
packages/veilid_support/lib/src/memory_tools.dart
Normal file
72
packages/veilid_support/lib/src/memory_tools.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user