veilidchat/lib/conversation/cubits/conversation_cubit.dart

334 lines
12 KiB
Dart
Raw Normal View History

2024-01-31 22:29:06 -05:00
// A Conversation is a type of Chat that is 1:1 between two Contacts only
// Each Contact in the ContactList has at most one Conversation between the
// remote contact and the local account
import 'dart:async';
import 'dart:convert';
2024-02-11 00:29:58 -05:00
import 'package:async_tools/async_tools.dart';
2024-01-31 22:29:06 -05:00
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:meta/meta.dart';
2024-06-15 00:01:08 -04:00
import 'package:protobuf/protobuf.dart';
2024-01-31 22:29:06 -05:00
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto;
2024-06-15 00:01:08 -04:00
const _sfUpdateAccountChange = 'updateAccountChange';
2024-01-31 22:29:06 -05:00
@immutable
class ConversationState extends Equatable {
const ConversationState(
{required this.localConversation, required this.remoteConversation});
final proto.Conversation? localConversation;
final proto.Conversation? remoteConversation;
@override
List<Object?> get props => [localConversation, remoteConversation];
}
2024-06-15 00:01:08 -04:00
/// Represents the control channel between two contacts
/// Used to pass profile, identity and status changes, and the messages key for
/// 1-1 chats
2024-01-31 22:29:06 -05:00
class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
ConversationCubit(
2024-06-18 21:20:06 -04:00
{required AccountInfo accountInfo,
2024-01-31 22:29:06 -05:00
required TypedKey remoteIdentityPublicKey,
TypedKey? localConversationRecordKey,
TypedKey? remoteConversationRecordKey})
2024-06-18 21:20:06 -04:00
: _accountInfo = accountInfo,
2024-01-31 22:29:06 -05:00
_localConversationRecordKey = localConversationRecordKey,
_remoteIdentityPublicKey = remoteIdentityPublicKey,
_remoteConversationRecordKey = remoteConversationRecordKey,
super(const AsyncValue.loading()) {
2024-06-18 21:20:06 -04:00
_identityWriter = _accountInfo.identityWriter;
2024-06-15 23:29:15 -04:00
2024-01-31 22:29:06 -05:00
if (_localConversationRecordKey != null) {
2024-07-17 16:53:47 -04:00
_initWait.add((_) async {
2024-04-01 09:04:54 -04:00
await _setLocalConversation(() async {
// Open local record key if it is specified
final pool = DHTRecordPool.instance;
final crypto = await _cachedConversationCrypto();
2024-06-15 23:29:15 -04:00
final writer = _identityWriter;
2024-07-17 16:53:47 -04:00
final record = await pool.openRecordWrite(
2024-04-01 09:04:54 -04:00
_localConversationRecordKey!, writer,
debugName: 'ConversationCubit::LocalConversation',
2024-06-18 21:20:06 -04:00
parent: accountInfo.accountRecordKey,
crypto: crypto);
2024-06-15 00:01:08 -04:00
2024-04-01 09:04:54 -04:00
return record;
});
2024-01-31 22:29:06 -05:00
});
}
if (_remoteConversationRecordKey != null) {
2024-07-17 16:53:47 -04:00
_initWait.add((cancel) async {
2024-04-01 09:04:54 -04:00
await _setRemoteConversation(() async {
// Open remote record key if it is specified
final pool = DHTRecordPool.instance;
final crypto = await _cachedConversationCrypto();
2024-07-17 16:53:47 -04:00
final record = await pool.openRecordRead(_remoteConversationRecordKey,
debugName: 'ConversationCubit::RemoteConversation',
2024-06-16 22:12:24 -04:00
parent: pool.getParentRecordKey(_remoteConversationRecordKey) ??
2024-06-18 21:20:06 -04:00
accountInfo.accountRecordKey,
crypto: crypto);
2024-07-17 16:53:47 -04:00
2024-04-01 09:04:54 -04:00
return record;
});
2024-01-31 22:29:06 -05:00
});
}
}
@override
Future<void> close() async {
await _initWait();
2024-06-15 00:01:08 -04:00
await _accountSubscription?.cancel();
2024-02-27 21:37:39 -05:00
await _localSubscription?.cancel();
await _remoteSubscription?.cancel();
2024-04-01 09:04:54 -04:00
await _localConversationCubit?.close();
await _remoteConversationCubit?.close();
2024-01-31 22:29:06 -05:00
await super.close();
}
2024-06-15 00:01:08 -04:00
////////////////////////////////////////////////////////////////////////////
// Public Interface
2024-06-15 00:01:08 -04:00
/// 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
/// The ConversationCubit must not already have a local conversation
/// The callback allows for more initialization to occur and for
/// cleanup to delete records upon failure of the callback
2024-01-31 22:29:06 -05:00
Future<T> initLocalConversation<T>(
2024-06-18 21:20:06 -04:00
{required proto.Profile profile,
required FutureOr<T> Function(DHTRecord) callback,
2024-01-31 22:29:06 -05:00
TypedKey? existingConversationRecordKey}) async {
assert(_localConversationRecordKey == null,
'must not have a local conversation yet');
final pool = DHTRecordPool.instance;
2024-03-24 12:13:27 -04:00
final crypto = await _cachedConversationCrypto();
2024-06-18 21:20:06 -04:00
final accountRecordKey = _accountInfo.accountRecordKey;
final writer = _accountInfo.identityWriter;
2024-01-31 22:29:06 -05:00
2024-06-21 22:44:35 -04:00
// Open with SMPL schema for identity writer
2024-01-31 22:29:06 -05:00
late final DHTRecord localConversationRecord;
if (existingConversationRecordKey != null) {
localConversationRecord = await pool.openRecordWrite(
2024-01-31 22:29:06 -05:00
existingConversationRecordKey, writer,
debugName:
'ConversationCubit::initLocalConversation::LocalConversation',
parent: accountRecordKey,
crypto: crypto);
2024-01-31 22:29:06 -05:00
} else {
localConversationRecord = await pool.createRecord(
debugName:
'ConversationCubit::initLocalConversation::LocalConversation',
2024-01-31 22:29:06 -05:00
parent: accountRecordKey,
crypto: crypto,
writer: writer,
2024-01-31 22:29:06 -05:00
schema: DHTSchema.smpl(
oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)]));
}
final out = localConversationRecord
// ignore: prefer_expression_function_bodies
.deleteScope((localConversation) async {
// Make messages log
return _initLocalMessages(
2024-02-08 20:35:59 -05:00
localConversationKey: localConversation.key,
callback: (messages) async {
// Create initial local conversation key contents
final conversation = proto.Conversation()
2024-06-18 21:20:06 -04:00
..profile = profile
..superIdentityJson =
jsonEncode(_accountInfo.localAccount.superIdentity.toJson())
2024-03-24 12:13:27 -04:00
..messages = messages.recordKey.toProto();
2024-02-08 20:35:59 -05:00
// Write initial conversation to record
final update = await localConversation.tryWriteProtobuf(
proto.Conversation.fromBuffer, conversation);
if (update != null) {
throw Exception('Failed to write local conversation');
}
final out = await callback(localConversation);
// Upon success emit the local conversation record to the state
_updateLocalConversationState(AsyncValue.data(conversation));
2024-02-08 20:35:59 -05:00
return out;
});
2024-01-31 22:29:06 -05:00
});
// If success, save the new local conversation record key in this object
_localConversationRecordKey = localConversationRecord.key;
2024-04-01 09:04:54 -04:00
await _setLocalConversation(() async => localConversationRecord);
2024-01-31 22:29:06 -05:00
return out;
}
2024-06-15 00:01:08 -04:00
/// Force refresh of conversation keys
2024-01-31 22:29:06 -05:00
Future<void> refresh() async {
await _initWait();
2024-02-07 19:13:32 -05:00
final lcc = _localConversationCubit;
final rcc = _remoteConversationCubit;
if (lcc != null) {
await lcc.refreshDefault();
}
if (rcc != null) {
await rcc.refreshDefault();
2024-01-31 22:29:06 -05:00
}
}
2024-06-15 00:01:08 -04:00
/// Watch for account record changes and update the conversation
void watchAccountChanges(Stream<AsyncValue<proto.Account>> accountStream,
AsyncValue<proto.Account> currentState) {
assert(_accountSubscription == null, 'only watch account once');
_accountSubscription = accountStream.listen(_updateAccountChange);
_updateAccountChange(currentState);
}
////////////////////////////////////////////////////////////////////////////
// Private Implementation
2024-01-31 22:29:06 -05:00
2024-06-15 00:01:08 -04:00
void _updateAccountChange(AsyncValue<proto.Account> avaccount) {
final account = avaccount.asData?.value;
if (account == null) {
return;
2024-02-07 19:13:32 -05:00
}
2024-06-15 00:01:08 -04:00
final cubit = _localConversationCubit;
if (cubit == null) {
return;
}
serialFuture((this, _sfUpdateAccountChange), () async {
await cubit.record.eventualUpdateProtobuf(proto.Conversation.fromBuffer,
(old) async {
if (old == null || old.profile == account.profile) {
return null;
}
return old.deepCopy()..profile = account.profile;
});
});
}
void _updateLocalConversationState(AsyncValue<proto.Conversation> avconv) {
final newState = avconv.when(
data: (conv) {
_incrementalState = ConversationState(
localConversation: conv,
remoteConversation: _incrementalState.remoteConversation);
// return loading still if state isn't complete
if ((_localConversationRecordKey != null &&
_incrementalState.localConversation == null) ||
(_remoteConversationRecordKey != null &&
_incrementalState.remoteConversation == null)) {
return const AsyncValue<ConversationState>.loading();
}
// state is complete, all required keys are open
return AsyncValue.data(_incrementalState);
},
loading: AsyncValue<ConversationState>.loading,
error: AsyncValue<ConversationState>.error,
);
emit(newState);
}
2024-02-07 19:13:32 -05:00
2024-06-15 00:01:08 -04:00
void _updateRemoteConversationState(AsyncValue<proto.Conversation> avconv) {
final newState = avconv.when(
data: (conv) {
_incrementalState = ConversationState(
localConversation: _incrementalState.localConversation,
remoteConversation: conv);
// return loading still if state isn't complete
if ((_localConversationRecordKey != null &&
_incrementalState.localConversation == null) ||
(_remoteConversationRecordKey != null &&
_incrementalState.remoteConversation == null)) {
return const AsyncValue<ConversationState>.loading();
}
// state is complete, all required keys are open
return AsyncValue.data(_incrementalState);
},
loading: AsyncValue<ConversationState>.loading,
error: AsyncValue<ConversationState>.error,
);
emit(newState);
}
// Open local converation key
Future<void> _setLocalConversation(Future<DHTRecord> Function() open) async {
assert(_localConversationCubit == null,
'shoud not set local conversation twice');
_localConversationCubit = DefaultDHTRecordCubit(
open: open, decodeState: proto.Conversation.fromBuffer);
_localSubscription =
_localConversationCubit!.stream.listen(_updateLocalConversationState);
2024-06-20 21:38:59 -04:00
_updateLocalConversationState(_localConversationCubit!.state);
2024-06-15 00:01:08 -04:00
}
// Open remote converation key
Future<void> _setRemoteConversation(Future<DHTRecord> Function() open) async {
assert(_remoteConversationCubit == null,
'shoud not set remote conversation twice');
_remoteConversationCubit = DefaultDHTRecordCubit(
open: open, decodeState: proto.Conversation.fromBuffer);
_remoteSubscription =
_remoteConversationCubit!.stream.listen(_updateRemoteConversationState);
2024-06-20 21:38:59 -04:00
_updateRemoteConversationState(_remoteConversationCubit!.state);
2024-06-15 00:01:08 -04:00
}
// Initialize local messages
Future<T> _initLocalMessages<T>({
required TypedKey localConversationKey,
required FutureOr<T> Function(DHTLog) callback,
}) async {
2024-06-15 23:29:15 -04:00
final crypto = await _cachedConversationCrypto();
final writer = _identityWriter;
2024-06-15 00:01:08 -04:00
return (await DHTLog.create(
debugName: 'ConversationCubit::initLocalMessages::LocalMessages',
parent: localConversationKey,
crypto: crypto,
writer: writer))
.deleteScope((messages) async => await callback(messages));
2024-02-07 19:13:32 -05:00
}
2024-01-31 22:29:06 -05:00
2024-05-25 22:46:43 -04:00
Future<VeilidCrypto> _cachedConversationCrypto() async {
2024-01-31 22:29:06 -05:00
var conversationCrypto = _conversationCrypto;
if (conversationCrypto != null) {
return conversationCrypto;
}
2024-06-18 21:20:06 -04:00
conversationCrypto =
await _accountInfo.makeConversationCrypto(_remoteIdentityPublicKey);
2024-01-31 22:29:06 -05:00
_conversationCrypto = conversationCrypto;
return conversationCrypto;
}
2024-06-15 00:01:08 -04:00
////////////////////////////////////////////////////////////////////////////
// Fields
2024-06-16 22:12:24 -04:00
TypedKey get remoteIdentityPublicKey => _remoteIdentityPublicKey;
2024-06-18 21:20:06 -04:00
final AccountInfo _accountInfo;
2024-06-15 23:29:15 -04:00
late final KeyPair _identityWriter;
2024-01-31 22:29:06 -05:00
final TypedKey _remoteIdentityPublicKey;
TypedKey? _localConversationRecordKey;
2024-02-07 19:13:32 -05:00
final TypedKey? _remoteConversationRecordKey;
2024-01-31 22:29:06 -05:00
DefaultDHTRecordCubit<proto.Conversation>? _localConversationCubit;
DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit;
2024-02-27 21:37:39 -05:00
StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription;
StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription;
2024-06-15 00:01:08 -04:00
StreamSubscription<AsyncValue<proto.Account>>? _accountSubscription;
ConversationState _incrementalState = const ConversationState(
localConversation: null, remoteConversation: null);
2024-05-25 22:46:43 -04:00
VeilidCrypto? _conversationCrypto;
2024-07-17 16:53:47 -04:00
final WaitSet<void, void> _initWait = WaitSet();
2024-01-31 22:29:06 -05:00
}