break everything

This commit is contained in:
Christien Rioux 2023-12-26 20:26:54 -05:00
parent e898074387
commit 29210c89d2
121 changed files with 2892 additions and 2608 deletions

View file

@ -0,0 +1,145 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../entities/local_account.dart';
import '../../entities/user_login.dart';
import '../../proto/proto.dart' as proto;
import '../../veilid_support/veilid_support.dart';
import '../../local_accounts/local_accounts.dart';
import 'logins.dart';
part 'account.g.dart';
enum AccountInfoStatus {
noAccount,
accountInvalid,
accountLocked,
accountReady,
}
@immutable
class AccountInfo {
const AccountInfo({
required this.status,
required this.active,
this.account,
});
final AccountInfoStatus status;
final bool active;
final proto.Account? account;
}
/// Get an account from the identity key and if it is logged in and we
/// have its secret available, return the account record contents
@riverpod
Future<AccountInfo> fetchAccountInfo(FetchAccountInfoRef ref,
{required TypedKey accountMasterRecordKey}) async {
// Get which local account we want to fetch the profile for
final localAccount = await ref.watch(
fetchLocalAccountProvider(accountMasterRecordKey: accountMasterRecordKey)
.future);
if (localAccount == null) {
// Local account does not exist
return const AccountInfo(
status: AccountInfoStatus.noAccount, active: false);
}
// See if we've logged into this account or if it is locked
final activeUserLogin = await ref.watch(loginsProvider.future
.select((value) async => (await value).activeUserLogin));
final active = activeUserLogin == accountMasterRecordKey;
final login = await ref.watch(
fetchLoginProvider(accountMasterRecordKey: accountMasterRecordKey)
.future);
if (login == null) {
// Account was locked
return AccountInfo(status: AccountInfoStatus.accountLocked, active: active);
}
// Pull the account DHT key, decode it and return it
final pool = await DHTRecordPool.instance();
final account = await (await pool.openOwned(
login.accountRecordInfo.accountRecord,
parent: localAccount.identityMaster.identityRecordKey))
.scope((accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer));
if (account == null) {
// Account could not be read or decrypted from DHT
ref.invalidateSelf();
return AccountInfo(
status: AccountInfoStatus.accountInvalid, active: active);
}
// Got account, decrypted and decoded
return AccountInfo(
status: AccountInfoStatus.accountReady, active: active, account: account);
}
@immutable
class ActiveAccountInfo {
const ActiveAccountInfo({
required this.localAccount,
required this.userLogin,
required this.account,
});
//
KeyPair getConversationWriter() {
final identityKey = localAccount.identityMaster.identityPublicKey;
final identitySecret = userLogin.identitySecret;
return KeyPair(key: identityKey, secret: identitySecret.value);
}
//
final LocalAccount localAccount;
final UserLogin userLogin;
final proto.Account account;
}
/// Get the active account info
@riverpod
Future<ActiveAccountInfo?> fetchActiveAccountInfo(
FetchActiveAccountInfoRef ref) async {
// See if we've logged into this account or if it is locked
final activeUserLogin = await ref.watch(loginsProvider.future
.select((value) async => (await value).activeUserLogin));
if (activeUserLogin == null) {
return null;
}
// Get the user login
final userLogin = await ref.watch(
fetchLoginProvider(accountMasterRecordKey: activeUserLogin).future);
if (userLogin == null) {
// Account was locked
return null;
}
// Get which local account we want to fetch the profile for
final localAccount = await ref.watch(
fetchLocalAccountProvider(accountMasterRecordKey: activeUserLogin)
.future);
if (localAccount == null) {
// Local account does not exist
return null;
}
// Pull the account DHT key, decode it and return it
final pool = await DHTRecordPool.instance();
final account = await (await pool.openOwned(
userLogin.accountRecordInfo.accountRecord,
parent: localAccount.identityMaster.identityRecordKey))
.scope((accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer));
if (account == null) {
ref.invalidateSelf();
return null;
}
// Got account, decrypted and decoded
return ActiveAccountInfo(
localAccount: localAccount,
userLogin: userLogin,
account: account,
);
}

View file

@ -0,0 +1,118 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../proto/proto.dart' as proto;
import '../../veilid_support/veilid_support.dart';
import 'account.dart';
part 'chat.g.dart';
/// Create a new chat (singleton for single contact chats)
Future<void> getOrCreateChatSingleContact({
required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteConversationRecordKey,
}) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Create conversation type Chat
final chat = proto.Chat()
..type = proto.ChatType.SINGLE_CONTACT
..remoteConversationKey = remoteConversationRecordKey.toProto();
// Add Chat to account's list
// if this fails, don't keep retrying, user can try again later
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.chatList),
parent: accountRecordKey))
.scope((chatList) async {
for (var i = 0; i < chatList.length; i++) {
final cbuf = await chatList.getItem(i);
if (cbuf == null) {
throw Exception('Failed to get chat');
}
final c = proto.Chat.fromBuffer(cbuf);
if (c == chat) {
return;
}
}
if (await chatList.tryAddItem(chat.writeToBuffer()) == false) {
throw Exception('Failed to add chat');
}
});
}
/// Delete a chat
Future<void> deleteChat(
{required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteConversationRecordKey}) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Create conversation type Chat
final remoteConversationKey = remoteConversationRecordKey.toProto();
// Add Chat to account's list
// if this fails, don't keep retrying, user can try again later
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.chatList),
parent: accountRecordKey))
.scope((chatList) async {
for (var i = 0; i < chatList.length; i++) {
final cbuf = await chatList.getItem(i);
if (cbuf == null) {
throw Exception('Failed to get chat');
}
final c = proto.Chat.fromBuffer(cbuf);
if (c.remoteConversationKey == remoteConversationKey) {
await chatList.tryRemoveItem(i);
if (activeChatState.state == remoteConversationRecordKey) {
activeChatState.state = null;
}
return;
}
}
});
}
/// Get the active account contact list
@riverpod
Future<IList<proto.Chat>?> fetchChatList(FetchChatListRef ref) async {
// See if we've logged into this account or if it is locked
final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
return null;
}
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Decode the chat list from the DHT
IList<proto.Chat> out = const IListConst([]);
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.chatList),
parent: accountRecordKey))
.scope((cList) async {
for (var i = 0; i < cList.length; i++) {
final cir = await cList.getItem(i);
if (cir == null) {
throw Exception('Failed to get chat');
}
out = out.add(proto.Chat.fromBuffer(cir));
}
});
return out;
}
// The selected chat
final activeChatState = StateController<TypedKey?>(null);
final activeChatStateProvider =
StateNotifierProvider<StateController<TypedKey?>, TypedKey?>(
(ref) => activeChatState);

View file

@ -0,0 +1,29 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../veilid_support/veilid_support.dart';
part 'connection_state.freezed.dart';
@freezed
class ConnectionState with _$ConnectionState {
const factory ConnectionState({
required VeilidStateAttachment attachment,
}) = _ConnectionState;
const ConnectionState._();
bool get isAttached => !(attachment.state == AttachmentState.detached ||
attachment.state == AttachmentState.detaching ||
attachment.state == AttachmentState.attaching);
bool get isPublicInternetReady => attachment.publicInternetReady;
}
final connectionState = StateController<ConnectionState>(const ConnectionState(
attachment: VeilidStateAttachment(
state: AttachmentState.detached,
publicInternetReady: false,
localNetworkReady: false)));
final connectionStateProvider =
StateNotifierProvider<StateController<ConnectionState>, ConnectionState>(
(ref) => connectionState);

View file

@ -0,0 +1,150 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'connection_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$ConnectionState {
VeilidStateAttachment get attachment => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ConnectionStateCopyWith<ConnectionState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ConnectionStateCopyWith<$Res> {
factory $ConnectionStateCopyWith(
ConnectionState value, $Res Function(ConnectionState) then) =
_$ConnectionStateCopyWithImpl<$Res, ConnectionState>;
@useResult
$Res call({VeilidStateAttachment attachment});
$VeilidStateAttachmentCopyWith<$Res> get attachment;
}
/// @nodoc
class _$ConnectionStateCopyWithImpl<$Res, $Val extends ConnectionState>
implements $ConnectionStateCopyWith<$Res> {
_$ConnectionStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? attachment = null,
}) {
return _then(_value.copyWith(
attachment: null == attachment
? _value.attachment
: attachment // ignore: cast_nullable_to_non_nullable
as VeilidStateAttachment,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$VeilidStateAttachmentCopyWith<$Res> get attachment {
return $VeilidStateAttachmentCopyWith<$Res>(_value.attachment, (value) {
return _then(_value.copyWith(attachment: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$ConnectionStateImplCopyWith<$Res>
implements $ConnectionStateCopyWith<$Res> {
factory _$$ConnectionStateImplCopyWith(_$ConnectionStateImpl value,
$Res Function(_$ConnectionStateImpl) then) =
__$$ConnectionStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({VeilidStateAttachment attachment});
@override
$VeilidStateAttachmentCopyWith<$Res> get attachment;
}
/// @nodoc
class __$$ConnectionStateImplCopyWithImpl<$Res>
extends _$ConnectionStateCopyWithImpl<$Res, _$ConnectionStateImpl>
implements _$$ConnectionStateImplCopyWith<$Res> {
__$$ConnectionStateImplCopyWithImpl(
_$ConnectionStateImpl _value, $Res Function(_$ConnectionStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? attachment = null,
}) {
return _then(_$ConnectionStateImpl(
attachment: null == attachment
? _value.attachment
: attachment // ignore: cast_nullable_to_non_nullable
as VeilidStateAttachment,
));
}
}
/// @nodoc
class _$ConnectionStateImpl extends _ConnectionState {
const _$ConnectionStateImpl({required this.attachment}) : super._();
@override
final VeilidStateAttachment attachment;
@override
String toString() {
return 'ConnectionState(attachment: $attachment)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ConnectionStateImpl &&
(identical(other.attachment, attachment) ||
other.attachment == attachment));
}
@override
int get hashCode => Object.hash(runtimeType, attachment);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ConnectionStateImplCopyWith<_$ConnectionStateImpl> get copyWith =>
__$$ConnectionStateImplCopyWithImpl<_$ConnectionStateImpl>(
this, _$identity);
}
abstract class _ConnectionState extends ConnectionState {
const factory _ConnectionState(
{required final VeilidStateAttachment attachment}) =
_$ConnectionStateImpl;
const _ConnectionState._() : super._();
@override
VeilidStateAttachment get attachment;
@override
@JsonKey(ignore: true)
_$$ConnectionStateImplCopyWith<_$ConnectionStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,132 @@
import 'dart:convert';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../proto/proto.dart' as proto;
import '../../veilid_support/veilid_support.dart';
import '../../tools/tools.dart';
import 'account.dart';
import 'chat.dart';
part 'contact.g.dart';
Future<void> createContact({
required ActiveAccountInfo activeAccountInfo,
required proto.Profile profile,
required IdentityMaster remoteIdentity,
required TypedKey remoteConversationRecordKey,
required TypedKey localConversationRecordKey,
}) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Create Contact
final contact = proto.Contact()
..editedProfile = profile
..remoteProfile = profile
..identityMasterJson = jsonEncode(remoteIdentity.toJson())
..identityPublicKey = TypedKey(
kind: remoteIdentity.identityRecordKey.kind,
value: remoteIdentity.identityPublicKey)
.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto()
..localConversationRecordKey = localConversationRecordKey.toProto()
..showAvailability = false;
// Add Contact to account's list
// if this fails, don't keep retrying, user can try again later
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactList),
parent: accountRecordKey))
.scope((contactList) async {
if (await contactList.tryAddItem(contact.writeToBuffer()) == false) {
throw Exception('Failed to add contact');
}
});
}
Future<void> deleteContact(
{required ActiveAccountInfo activeAccountInfo,
required proto.Contact contact}) async {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final localConversationKey =
proto.TypedKeyProto.fromProto(contact.localConversationRecordKey);
final remoteConversationKey =
proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey);
// Remove any chats for this contact
await deleteChat(
activeAccountInfo: activeAccountInfo,
remoteConversationRecordKey: remoteConversationKey);
// Remove Contact from account's list
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactList),
parent: accountRecordKey))
.scope((contactList) async {
for (var i = 0; i < contactList.length; i++) {
final item =
await contactList.getItemProtobuf(proto.Contact.fromBuffer, i);
if (item == null) {
throw Exception('Failed to get contact');
}
if (item.remoteConversationRecordKey ==
contact.remoteConversationRecordKey) {
await contactList.tryRemoveItem(i);
break;
}
}
try {
await (await pool.openRead(localConversationKey,
parent: accountRecordKey))
.delete();
} on Exception catch (e) {
log.debug('error removing local conversation record key: $e', e);
}
try {
if (localConversationKey != remoteConversationKey) {
await (await pool.openRead(remoteConversationKey,
parent: accountRecordKey))
.delete();
}
} on Exception catch (e) {
log.debug('error removing remote conversation record key: $e', e);
}
});
}
/// Get the active account contact list
@riverpod
Future<IList<proto.Contact>?> fetchContactList(FetchContactListRef ref) async {
// See if we've logged into this account or if it is locked
final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
return null;
}
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Decode the contact list from the DHT
IList<proto.Contact> out = const IListConst([]);
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactList),
parent: accountRecordKey))
.scope((cList) async {
for (var i = 0; i < cList.length; i++) {
final cir = await cList.getItem(i);
if (cir == null) {
throw Exception('Failed to get contact');
}
out = out.add(proto.Contact.fromBuffer(cir));
}
});
return out;
}

View file

@ -0,0 +1,583 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:mutex/mutex.dart';
import '../../entities/entities.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../../veilid_support/veilid_support.dart';
import 'account.dart';
part 'contact_invitation_list_manager.g.dart';
//////////////////////////////////////////////////
class ContactInviteInvalidKeyException implements Exception {
const ContactInviteInvalidKeyException(this.type) : super();
final EncryptionKeyType type;
}
typedef GetEncryptionKeyCallback = Future<SecretKey?> Function(
VeilidCryptoSystem cs,
EncryptionKeyType encryptionKeyType,
Uint8List encryptedSecret);
//////////////////////////////////////////////////
@immutable
class AcceptedContact {
const AcceptedContact({
required this.profile,
required this.remoteIdentity,
required this.remoteConversationRecordKey,
required this.localConversationRecordKey,
});
final proto.Profile profile;
final IdentityMaster remoteIdentity;
final TypedKey remoteConversationRecordKey;
final TypedKey localConversationRecordKey;
}
@immutable
class InvitationStatus {
const InvitationStatus({required this.acceptedContact});
final AcceptedContact? acceptedContact;
}
//////////////////////////////////////////////////
//////////////////////////////////////////////////
// Mutable state for per-account contact invitations
@riverpod
class ContactInvitationListManager extends _$ContactInvitationListManager {
ContactInvitationListManager._({
required ActiveAccountInfo activeAccountInfo,
required DHTShortArray dhtRecord,
}) : _activeAccountInfo = activeAccountInfo,
_dhtRecord = dhtRecord,
_records = IList();
@override
FutureOr<IList<proto.ContactInvitationRecord>> build(
ActiveAccountInfo activeAccountInfo) async {
// Load initial todo list from the remote repository
ref.onDispose xxxx call close and pass dhtrecord through... could use a context object
and a DHTValueChangeProvider that we watch in build that updates when dht records change
return _open(activeAccountInfo);
}
static Future<ContactInvitationListManager> _open(
ActiveAccountInfo activeAccountInfo) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final dhtRecord = await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactInvitationRecords),
parent: accountRecordKey);
return ContactInvitationListManager._(
activeAccountInfo: activeAccountInfo, dhtRecord: dhtRecord);
}
Future<void> close() async {
state = "";
await _dhtRecord.close();
}
Future<void> refresh() async {
for (var i = 0; i < _dhtRecord.length; i++) {
final cir = await _dhtRecord.getItem(i);
if (cir == null) {
throw Exception('Failed to get contact invitation record');
}
_records = _records.add(proto.ContactInvitationRecord.fromBuffer(cir));
}
}
Future<Uint8List> createInvitation(
{required EncryptionKeyType encryptionKeyType,
required String encryptionKey,
required String message,
required Timestamp? expiration}) async {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final identityKey =
_activeAccountInfo.localAccount.identityMaster.identityPublicKey;
final identitySecret = _activeAccountInfo.userLogin.identitySecret.value;
// Generate writer keypair to share with new contact
final cs = await pool.veilid.bestCryptoSystem();
final contactRequestWriter = await cs.generateKeyPair();
final conversationWriter = _activeAccountInfo.getConversationWriter();
// Encrypt the writer secret with the encryption key
final encryptedSecret = await encryptSecretToBytes(
secret: contactRequestWriter.secret,
cryptoKind: cs.kind(),
encryptionKey: encryptionKey,
encryptionKeyType: encryptionKeyType);
// Create local chat DHT record with the account record key as its parent
// Do not set the encryption of this key yet as it will not yet be written
// to and it will be eventually encrypted with the DH of the contact's
// identity key
late final Uint8List signedContactInvitationBytes;
await (await pool.create(
parent: accountRecordKey,
schema: DHTSchema.smpl(oCnt: 0, members: [
DHTSchemaMember(mKey: conversationWriter.key, mCnt: 1)
])))
.deleteScope((localConversation) async {
// dont bother reopening localConversation with writer
// Make ContactRequestPrivate and encrypt with the writer secret
final crpriv = proto.ContactRequestPrivate()
..writerKey = contactRequestWriter.key.toProto()
..profile = _activeAccountInfo.account.profile
..identityMasterRecordKey =
_activeAccountInfo.userLogin.accountMasterRecordKey.toProto()
..chatRecordKey = localConversation.key.toProto()
..expiration = expiration?.toInt64() ?? Int64.ZERO;
final crprivbytes = crpriv.writeToBuffer();
final encryptedContactRequestPrivate = await cs.encryptAeadWithNonce(
crprivbytes, contactRequestWriter.secret);
// Create ContactRequest and embed contactrequestprivate
final creq = proto.ContactRequest()
..encryptionKeyType = encryptionKeyType.toProto()
..private = encryptedContactRequestPrivate;
// Create DHT unicast inbox for ContactRequest
await (await pool.create(
parent: accountRecordKey,
schema: DHTSchema.smpl(oCnt: 1, members: [
DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key)
]),
crypto: const DHTRecordCryptoPublic()))
.deleteScope((contactRequestInbox) async {
// Store ContactRequest in owner subkey
await contactRequestInbox.eventualWriteProtobuf(creq);
// Create ContactInvitation and SignedContactInvitation
final cinv = proto.ContactInvitation()
..contactRequestInboxKey = contactRequestInbox.key.toProto()
..writerSecret = encryptedSecret;
final cinvbytes = cinv.writeToBuffer();
final scinv = proto.SignedContactInvitation()
..contactInvitation = cinvbytes
..identitySignature =
(await cs.sign(identityKey, identitySecret, cinvbytes)).toProto();
signedContactInvitationBytes = scinv.writeToBuffer();
// Create ContactInvitationRecord
final cinvrec = proto.ContactInvitationRecord()
..contactRequestInbox =
contactRequestInbox.ownedDHTRecordPointer.toProto()
..writerKey = contactRequestWriter.key.toProto()
..writerSecret = contactRequestWriter.secret.toProto()
..localConversationRecordKey = localConversation.key.toProto()
..expiration = expiration?.toInt64() ?? Int64.ZERO
..invitation = signedContactInvitationBytes
..message = message;
// Add ContactInvitationRecord to account's list
// if this fails, don't keep retrying, user can try again later
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
_activeAccountInfo.account.contactInvitationRecords),
parent: accountRecordKey))
.scope((cirList) async {
if (await cirList.tryAddItem(cinvrec.writeToBuffer()) == false) {
throw Exception('Failed to add contact invitation record');
}
});
});
});
return signedContactInvitationBytes;
}
Future<void> deleteInvitation(
{required bool accepted,
required proto.ContactInvitationRecord contactInvitationRecord}) async {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Remove ContactInvitationRecord from account's list
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
_activeAccountInfo.account.contactInvitationRecords),
parent: accountRecordKey))
.scope((cirList) async {
for (var i = 0; i < cirList.length; i++) {
final item = await cirList.getItemProtobuf(
proto.ContactInvitationRecord.fromBuffer, i);
if (item == null) {
throw Exception('Failed to get contact invitation record');
}
if (item.contactRequestInbox.recordKey ==
contactInvitationRecord.contactRequestInbox.recordKey) {
await cirList.tryRemoveItem(i);
break;
}
}
await (await pool.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
contactInvitationRecord.contactRequestInbox),
parent: accountRecordKey))
.scope((contactRequestInbox) async {
// Wipe out old invitation so it shows up as invalid
await contactRequestInbox.tryWriteBytes(Uint8List(0));
await contactRequestInbox.delete();
});
if (!accepted) {
await (await pool.openRead(
proto.TypedKeyProto.fromProto(
contactInvitationRecord.localConversationRecordKey),
parent: accountRecordKey))
.delete();
}
});
}
Future<ValidContactInvitation?> validateInvitation(
{required Uint8List inviteData,
required GetEncryptionKeyCallback getEncryptionKeyCallback}) async {
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final signedContactInvitation =
proto.SignedContactInvitation.fromBuffer(inviteData);
final contactInvitationBytes =
Uint8List.fromList(signedContactInvitation.contactInvitation);
final contactInvitation =
proto.ContactInvitation.fromBuffer(contactInvitationBytes);
final contactRequestInboxKey =
proto.TypedKeyProto.fromProto(contactInvitation.contactRequestInboxKey);
ValidContactInvitation? out;
final pool = await DHTRecordPool.instance();
final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
// See if we're chatting to ourselves, if so, don't delete it here
final isSelf = _records.indexWhere((cir) =>
proto.TypedKeyProto.fromProto(cir.contactRequestInbox.recordKey) ==
contactRequestInboxKey) !=
-1;
await (await pool.openRead(contactRequestInboxKey,
parent: accountRecordKey))
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
//
final contactRequest = await contactRequestInbox
.getProtobuf(proto.ContactRequest.fromBuffer);
// Decrypt contact request private
final encryptionKeyType =
EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType);
late final SharedSecret? writerSecret;
try {
writerSecret = await getEncryptionKeyCallback(cs, encryptionKeyType,
Uint8List.fromList(contactInvitation.writerSecret));
} on Exception catch (_) {
throw ContactInviteInvalidKeyException(encryptionKeyType);
}
if (writerSecret == null) {
return null;
}
final contactRequestPrivateBytes = await cs.decryptAeadWithNonce(
Uint8List.fromList(contactRequest.private), writerSecret);
final contactRequestPrivate =
proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes);
final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto(
contactRequestPrivate.identityMasterRecordKey);
// Fetch the account master
final contactIdentityMaster = await openIdentityMaster(
identityMasterRecordKey: contactIdentityMasterRecordKey);
// Verify
final signature = proto.SignatureProto.fromProto(
signedContactInvitation.identitySignature);
await cs.verify(contactIdentityMaster.identityPublicKey,
contactInvitationBytes, signature);
final writer = KeyPair(
key: proto.CryptoKeyProto.fromProto(contactRequestPrivate.writerKey),
secret: writerSecret);
out = ValidContactInvitation._(
contactInvitationManager: this,
signedContactInvitation: signedContactInvitation,
contactInvitation: contactInvitation,
contactRequestInboxKey: contactRequestInboxKey,
contactRequest: contactRequest,
contactRequestPrivate: contactRequestPrivate,
contactIdentityMaster: contactIdentityMaster,
writer: writer);
});
return out;
}
Future<InvitationStatus?> checkInvitationStatus(
{required ActiveAccountInfo activeAccountInfo,
required proto.ContactInvitationRecord contactInvitationRecord}) async {
// Open the contact request inbox
try {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final writerKey =
proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerKey);
final writerSecret =
proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerSecret);
final recordKey = proto.TypedKeyProto.fromProto(
contactInvitationRecord.contactRequestInbox.recordKey);
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 = proto.TypedKeyProto.fromProto(
contactResponse.identityMasterRecordKey);
final cs = await pool.veilid.getCryptoSystem(recordKey.kind);
// Fetch the remote contact's account master
final contactIdentityMaster = await openIdentityMaster(
identityMasterRecordKey: contactIdentityMasterRecordKey);
// Verify
final signature = proto.SignatureProto.fromProto(
signedContactResponse.identitySignature);
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 = proto.TypedKeyProto.fromProto(
contactResponse.remoteConversationRecordKey);
final remoteConversation = await readRemoteConversation(
activeAccountInfo: activeAccountInfo,
remoteIdentityPublicKey:
contactIdentityMaster.identityPublicTypedKey(),
remoteConversationRecordKey: remoteConversationRecordKey);
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 = proto.TypedKeyProto.fromProto(
contactInvitationRecord.localConversationRecordKey);
return createConversation(
activeAccountInfo: activeAccountInfo,
remoteIdentityPublicKey:
contactIdentityMaster.identityPublicTypedKey(),
existingConversationRecordKey: localConversationRecordKey,
// ignore: prefer_expression_function_bodies
callback: (localConversation) async {
return InvitationStatus(
acceptedContact: AcceptedContact(
profile: 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 checkAcceptRejectContact: $e', e);
// Attempt to clean up. All this needs better lifetime management
await deleteInvitation(
accepted: false, contactInvitationRecord: contactInvitationRecord);
rethrow;
}
}
//
final ActiveAccountInfo _activeAccountInfo;
final DHTShortArray _dhtRecord;
IList<proto.ContactInvitationRecord> _records;
}
//////////////////////////////////////////////////
///
class ValidContactInvitation {
ValidContactInvitation._(
{required ContactInvitationListManager contactInvitationManager,
required proto.SignedContactInvitation signedContactInvitation,
required proto.ContactInvitation contactInvitation,
required TypedKey contactRequestInboxKey,
required proto.ContactRequest contactRequest,
required proto.ContactRequestPrivate contactRequestPrivate,
required IdentityMaster contactIdentityMaster,
required KeyPair writer})
: _contactInvitationManager = contactInvitationManager,
_signedContactInvitation = signedContactInvitation,
_contactInvitation = contactInvitation,
_contactRequestInboxKey = contactRequestInboxKey,
_contactRequest = contactRequest,
_contactRequestPrivate = contactRequestPrivate,
_contactIdentityMaster = contactIdentityMaster,
_writer = writer;
Future<AcceptedContact?> accept() async {
final pool = await DHTRecordPool.instance();
final activeAccountInfo = _contactInvitationManager._activeAccountInfo;
try {
// Ensure we don't delete this if we're trying to chat to self
final isSelf = _contactIdentityMaster.identityPublicKey ==
activeAccountInfo.localAccount.identityMaster.identityPublicKey;
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
return (await pool.openWrite(_contactRequestInboxKey, _writer,
parent: accountRecordKey))
// ignore: prefer_expression_function_bodies
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
// Create local conversation key for this
// contact and send via contact response
return createConversation(
activeAccountInfo: activeAccountInfo,
remoteIdentityPublicKey:
_contactIdentityMaster.identityPublicTypedKey(),
callback: (localConversation) async {
final contactResponse = proto.ContactResponse()
..accept = true
..remoteConversationRecordKey = localConversation.key.toProto()
..identityMasterRecordKey = activeAccountInfo
.localAccount.identityMaster.masterRecordKey
.toProto();
final contactResponseBytes = contactResponse.writeToBuffer();
final cs = await pool.veilid
.getCryptoSystem(_contactRequestInboxKey.kind);
final identitySignature = await cs.sign(
activeAccountInfo
.localAccount.identityMaster.identityPublicKey,
activeAccountInfo.userLogin.identitySecret.value,
contactResponseBytes);
final signedContactResponse = proto.SignedContactResponse()
..contactResponse = contactResponseBytes
..identitySignature = identitySignature.toProto();
// Write the acceptance to the inbox
if (await contactRequestInbox.tryWriteProtobuf(
proto.SignedContactResponse.fromBuffer,
signedContactResponse,
subkey: 1) !=
null) {
throw Exception('failed to accept contact invitation');
}
return AcceptedContact(
profile: _contactRequestPrivate.profile,
remoteIdentity: _contactIdentityMaster,
remoteConversationRecordKey: proto.TypedKeyProto.fromProto(
_contactRequestPrivate.chatRecordKey),
localConversationRecordKey: localConversation.key,
);
});
});
} on Exception catch (e) {
log.debug('exception: $e', e);
return null;
}
}
Future<bool> reject() async {
final pool = await DHTRecordPool.instance();
final activeAccountInfo = _contactInvitationManager._activeAccountInfo;
// Ensure we don't delete this if we're trying to chat to self
final isSelf = _contactIdentityMaster.identityPublicKey ==
activeAccountInfo.localAccount.identityMaster.identityPublicKey;
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
return (await pool.openWrite(_contactRequestInboxKey, _writer,
parent: accountRecordKey))
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
final cs =
await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind);
final contactResponse = proto.ContactResponse()
..accept = false
..identityMasterRecordKey = activeAccountInfo
.localAccount.identityMaster.masterRecordKey
.toProto();
final contactResponseBytes = contactResponse.writeToBuffer();
final identitySignature = await cs.sign(
activeAccountInfo.localAccount.identityMaster.identityPublicKey,
activeAccountInfo.userLogin.identitySecret.value,
contactResponseBytes);
final signedContactResponse = proto.SignedContactResponse()
..contactResponse = contactResponseBytes
..identitySignature = identitySignature.toProto();
// Write the rejection to the inbox
if (await contactRequestInbox.tryWriteProtobuf(
proto.SignedContactResponse.fromBuffer, signedContactResponse,
subkey: 1) !=
null) {
log.error('failed to reject contact invitation');
return false;
}
return true;
});
}
//
ContactInvitationListManager _contactInvitationManager;
proto.SignedContactInvitation _signedContactInvitation;
proto.ContactInvitation _contactInvitation;
TypedKey _contactRequestInboxKey;
proto.ContactRequest _contactRequest;
proto.ContactRequestPrivate _contactRequestPrivate;
IdentityMaster _contactIdentityMaster;
KeyPair _writer;
}

View file

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

View file

@ -0,0 +1,355 @@
// 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:convert';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../../veilid_init.dart';
import '../../veilid_support/veilid_support.dart';
import 'account.dart';
import 'chat.dart';
import 'contact.dart';
part 'conversation.g.dart';
class Conversation {
Conversation._(
{required ActiveAccountInfo activeAccountInfo,
required TypedKey localConversationRecordKey,
required TypedKey remoteIdentityPublicKey,
required TypedKey remoteConversationRecordKey})
: _activeAccountInfo = activeAccountInfo,
_localConversationRecordKey = localConversationRecordKey,
_remoteIdentityPublicKey = remoteIdentityPublicKey,
_remoteConversationRecordKey = remoteConversationRecordKey;
Future<Conversation> open() async {}
Future<void> close() async {
//
}
Future<proto.Conversation?> readRemoteConversation() async {
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final pool = await DHTRecordPool.instance();
final crypto = await getConversationCrypto();
return (await pool.openRead(_remoteConversationRecordKey,
parent: accountRecordKey, crypto: crypto))
.scope((remoteConversation) async {
//
final conversation =
await remoteConversation.getProtobuf(proto.Conversation.fromBuffer);
return conversation;
});
}
Future<proto.Conversation?> readLocalConversation() async {
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final pool = await DHTRecordPool.instance();
final crypto = await getConversationCrypto();
return (await pool.openRead(_localConversationRecordKey,
parent: accountRecordKey, crypto: crypto))
.scope((localConversation) async {
//
final update =
await localConversation.getProtobuf(proto.Conversation.fromBuffer);
if (update != null) {
return update;
}
return null;
});
}
Future<proto.Conversation?> writeLocalConversation({
required proto.Conversation conversation,
}) async {
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final pool = await DHTRecordPool.instance();
final crypto = await getConversationCrypto();
final writer = _activeAccountInfo.getConversationWriter();
return (await pool.openWrite(_localConversationRecordKey, writer,
parent: accountRecordKey, crypto: crypto))
.scope((localConversation) async {
//
final update = await localConversation.tryWriteProtobuf(
proto.Conversation.fromBuffer, conversation);
if (update != null) {
return update;
}
return null;
});
}
Future<void> addLocalConversationMessage(
{required proto.Message message}) async {
final conversation = await readLocalConversation();
if (conversation == null) {
return;
}
final messagesRecordKey =
proto.TypedKeyProto.fromProto(conversation.messages);
final crypto = await getConversationCrypto();
final writer = _activeAccountInfo.getConversationWriter();
await (await DHTShortArray.openWrite(messagesRecordKey, writer,
parent: _localConversationRecordKey, crypto: crypto))
.scope((messages) async {
await messages.tryAddItem(message.writeToBuffer());
});
}
Future<bool> mergeLocalConversationMessages(
{required IList<proto.Message> newMessages}) async {
final conversation = await readLocalConversation();
if (conversation == null) {
return false;
}
var changed = false;
final messagesRecordKey =
proto.TypedKeyProto.fromProto(conversation.messages);
final crypto = await getConversationCrypto();
final writer = _activeAccountInfo.getConversationWriter();
newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp)
.compareTo(Timestamp.fromInt64(b.timestamp)));
await (await DHTShortArray.openWrite(messagesRecordKey, writer,
parent: _localConversationRecordKey, crypto: crypto))
.scope((messages) async {
// Ensure newMessages is sorted by timestamp
newMessages =
newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
// Existing messages will always be sorted by timestamp so merging is easy
var pos = 0;
outer:
for (final newMessage in newMessages) {
var skip = false;
while (pos < messages.length) {
final m =
await messages.getItemProtobuf(proto.Message.fromBuffer, pos);
if (m == null) {
log.error('unable to get message #$pos');
break outer;
}
// If timestamp to insert is less than
// the current position, insert it here
final newTs = Timestamp.fromInt64(newMessage.timestamp);
final curTs = Timestamp.fromInt64(m.timestamp);
final cmp = newTs.compareTo(curTs);
if (cmp < 0) {
break;
} else if (cmp == 0) {
skip = true;
break;
}
pos++;
}
// Insert at this position
if (!skip) {
await messages.tryInsertItem(pos, newMessage.writeToBuffer());
changed = true;
}
}
});
return changed;
}
Future<IList<proto.Message>?> getRemoteConversationMessages() async {
final conversation = await readRemoteConversation();
if (conversation == null) {
return null;
}
final messagesRecordKey =
proto.TypedKeyProto.fromProto(conversation.messages);
final crypto = await getConversationCrypto();
return (await DHTShortArray.openRead(messagesRecordKey,
parent: _remoteConversationRecordKey, crypto: crypto))
.scope((messages) async {
var out = IList<proto.Message>();
for (var i = 0; i < messages.length; i++) {
final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i);
if (msg == null) {
throw Exception('Failed to get message');
}
out = out.add(msg);
}
return out;
});
}
//
Future<DHTRecordCrypto> getConversationCrypto() async {
var conversationCrypto = _conversationCrypto;
if (conversationCrypto != null) {
return conversationCrypto;
}
final veilid = await eventualVeilid.future;
final identitySecret = _activeAccountInfo.userLogin.identitySecret;
final cs = await veilid.getCryptoSystem(identitySecret.kind);
final sharedSecret =
await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value);
conversationCrypto = await DHTRecordCryptoPrivate.fromSecret(
identitySecret.kind, sharedSecret);
_conversationCrypto = conversationCrypto;
return conversationCrypto;
}
Future<IList<proto.Message>?> getLocalConversationMessages() async {
final conversation = await readLocalConversation();
if (conversation == null) {
return null;
}
final messagesRecordKey =
proto.TypedKeyProto.fromProto(conversation.messages);
final crypto = await getConversationCrypto();
return (await DHTShortArray.openRead(messagesRecordKey,
parent: _localConversationRecordKey, crypto: crypto))
.scope((messages) async {
var out = IList<proto.Message>();
for (var i = 0; i < messages.length; i++) {
final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i);
if (msg == null) {
throw Exception('Failed to get message');
}
out = out.add(msg);
}
return out;
});
}
final ActiveAccountInfo _activeAccountInfo;
final TypedKey _localConversationRecordKey;
final TypedKey _remoteIdentityPublicKey;
final TypedKey _remoteConversationRecordKey;
//
DHTRecordCrypto? _conversationCrypto;
}
// Create a conversation
// If we were the initiator of the conversation there may be an
// incomplete 'existingConversationRecord' that we need to fill
// in now that we have the remote identity key
Future<T> createConversation<T>(
{required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteIdentityPublicKey,
required FutureOr<T> Function(DHTRecord) callback,
TypedKey? existingConversationRecordKey}) async {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final crypto = await getConversationCrypto(
activeAccountInfo: activeAccountInfo,
remoteIdentityPublicKey: remoteIdentityPublicKey);
final writer = activeAccountInfo.getConversationWriter();
// Open with SMPL scheme for identity writer
late final DHTRecord localConversationRecord;
if (existingConversationRecordKey != null) {
localConversationRecord = await pool.openWrite(
existingConversationRecordKey, writer,
parent: accountRecordKey, crypto: crypto);
} else {
final localConversationRecordCreate = await pool.create(
parent: accountRecordKey,
crypto: crypto,
schema: DHTSchema.smpl(
oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)]));
await localConversationRecordCreate.close();
localConversationRecord = await pool.openWrite(
localConversationRecordCreate.key, writer,
parent: accountRecordKey, crypto: crypto);
}
return localConversationRecord
// ignore: prefer_expression_function_bodies
.deleteScope((localConversation) async {
// Make messages log
return (await DHTShortArray.create(
parent: localConversation.key, crypto: crypto, smplWriter: writer))
.deleteScope((messages) async {
// Write local conversation key
final conversation = proto.Conversation()
..profile = activeAccountInfo.account.profile
..identityMasterJson =
jsonEncode(activeAccountInfo.localAccount.identityMaster.toJson())
..messages = messages.record.key.toProto();
//
final update = await localConversation.tryWriteProtobuf(
proto.Conversation.fromBuffer, conversation);
if (update != null) {
throw Exception('Failed to write local conversation');
}
return await callback(localConversation);
});
});
}
//
//
//
//
@riverpod
class ActiveConversationMessages extends _$ActiveConversationMessages {
/// Get message for active conversation
@override
FutureOr<IList<proto.Message>?> build() async {
await eventualVeilid.future;
final activeChat = ref.watch(activeChatStateProvider);
if (activeChat == null) {
return null;
}
final activeAccountInfo =
await ref.watch(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
return null;
}
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
const IListConst([]);
final activeChatContactIdx = contactList.indexWhere(
(c) =>
proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) ==
activeChat,
);
if (activeChatContactIdx == -1) {
return null;
}
final activeChatContact = contactList[activeChatContactIdx];
final remoteIdentityPublicKey =
proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey);
// final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(
// activeChatContact.remoteConversationRecordKey);
final localConversationRecordKey = proto.TypedKeyProto.fromProto(
activeChatContact.localConversationRecordKey);
return await getLocalConversationMessages(
activeAccountInfo: activeAccountInfo,
localConversationRecordKey: localConversationRecordKey,
remoteIdentityPublicKey: remoteIdentityPublicKey,
);
}
}

View file

@ -0,0 +1,83 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:window_manager/window_manager.dart';
import '../../tools/responsive.dart';
export 'package:window_manager/window_manager.dart' show TitleBarStyle;
part 'window_control.g.dart';
enum OrientationCapability {
normal,
portraitOnly,
landscapeOnly,
}
// Window Control
@riverpod
class WindowControl extends _$WindowControl {
/// Change window control
@override
FutureOr<bool> build() async {
await _doWindowSetup(TitleBarStyle.hidden, OrientationCapability.normal);
return true;
}
static Future<void> initialize() async {
if (isDesktop) {
await windowManager.ensureInitialized();
const windowOptions = WindowOptions(
size: Size(768, 1024),
//minimumSize: Size(480, 480),
center: true,
backgroundColor: Colors.transparent,
skipTaskbar: false,
);
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
}
}
Future<void> _doWindowSetup(TitleBarStyle titleBarStyle,
OrientationCapability orientationCapability) async {
if (isDesktop) {
await windowManager.setTitleBarStyle(titleBarStyle);
} else {
switch (orientationCapability) {
case OrientationCapability.normal:
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
case OrientationCapability.portraitOnly:
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
case OrientationCapability.landscapeOnly:
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
}
}
//////////////////////////////////////////////////////////////
/// Mutators and Selectors
/// Reorder accounts
Future<void> changeWindowSetup(TitleBarStyle titleBarStyle,
OrientationCapability orientationCapability) async {
state = const AsyncValue.loading();
await _doWindowSetup(titleBarStyle, orientationCapability);
state = const AsyncValue.data(true);
}
}