Merge branch 'recovery-key-ui' into 'main'

Multiple accounts support

See merge request veilid/veilidchat!30
This commit is contained in:
Christien Rioux 2024-06-22 03:01:01 +00:00
commit 00fe682e0c
99 changed files with 5003 additions and 2538 deletions

View file

@ -2,8 +2,9 @@
"app": { "app": {
"title": "VeilidChat" "title": "VeilidChat"
}, },
"app_bar": { "menu": {
"settings_tooltip": "Settings" "settings_tooltip": "Settings",
"add_account_tooltip": "Add Account"
}, },
"pager": { "pager": {
"chats": "Chats", "chats": "Chats",
@ -18,7 +19,7 @@
"lock_type_password": "password" "lock_type_password": "password"
}, },
"new_account_page": { "new_account_page": {
"titlebar": "Create a new account", "titlebar": "Create A New Account",
"header": "Account Profile", "header": "Account Profile",
"create": "Create", "create": "Create",
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.", "instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.",
@ -26,12 +27,37 @@
"name": "Name", "name": "Name",
"pronouns": "Pronouns" "pronouns": "Pronouns"
}, },
"edit_account_page": {
"titlebar": "Edit Account",
"header": "Account Profile",
"update": "Update",
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.",
"error": "Account modification error",
"name": "Name",
"pronouns": "Pronouns",
"remove_account": "Remove Account",
"delete_identity": "Delete Identity",
"remove_account_confirm": "Confirm Account Removal?",
"remove_account_description": "Remove account from this device only",
"delete_identity_description": "Delete identity and all messages completely",
"delete_identity_confirm_message": "This action is PERMANENT, and your identity will no longer be recoverable with the recovery key. This will not remove your messages you have sent from other people's devices.",
"confirm_are_you_sure": "Are you sure you want to do this?"
},
"show_recovery_key_page": {
"titlebar": "Save Recovery Key",
"instructions": "You must save this recovery key somewhere safe. This key is the ONLY way to recover your VeilidChat account in the event of a forgotton password or a lost, stolen, or compromised device.",
"instructions_options": "Here are some options for your recovery key:",
"instructions_print": "Print the recovery key and keep it somewhere safe",
"instructions_write": "View the recovery key and write it down on paper",
"instructions_send": "Send the recovery key to another app to save it"
},
"button": { "button": {
"ok": "Ok", "ok": "Ok",
"cancel": "Cancel", "cancel": "Cancel",
"delete": "Delete", "delete": "Delete",
"accept": "Accept", "accept": "Accept",
"reject": "Reject", "reject": "Reject",
"finish": "Finish",
"waiting_for_network": "Waiting For Network" "waiting_for_network": "Waiting For Network"
}, },
"toast": { "toast": {

View file

@ -0,0 +1,40 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:veilid_support/veilid_support.dart';
import '../models/models.dart';
import '../repository/account_repository.dart';
class AccountInfoCubit extends Cubit<AccountInfo> {
AccountInfoCubit(
{required AccountRepository accountRepository,
required TypedKey superIdentityRecordKey})
: _accountRepository = accountRepository,
super(accountRepository.getAccountInfo(superIdentityRecordKey)!) {
// Subscribe to streams
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
switch (change) {
case AccountRepositoryChange.activeLocalAccount:
case AccountRepositoryChange.localAccounts:
case AccountRepositoryChange.userLogins:
final acctInfo =
accountRepository.getAccountInfo(superIdentityRecordKey);
if (acctInfo != null) {
emit(acctInfo);
}
break;
}
});
}
@override
Future<void> close() async {
await super.close();
await _accountRepositorySubscription.cancel();
}
final AccountRepository _accountRepository;
late final StreamSubscription<AccountRepositoryChange>
_accountRepositorySubscription;
}

View file

@ -1,16 +1,51 @@
import 'dart:async'; import 'dart:async';
import 'package:protobuf/protobuf.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;
import '../account_manager.dart';
class AccountRecordCubit extends DefaultDHTRecordCubit<proto.Account> { typedef AccountRecordState = proto.Account;
AccountRecordCubit({
required super.open, /// The saved state of a VeilidChat Account on the DHT
}) : super(decodeState: proto.Account.fromBuffer); /// Used to synchronize status, profile, and options for a specific account
/// across multiple clients. This DHT record is the 'source of truth' for an
/// account and is privately encrypted with an owned record from the 'userLogin'
/// tabledb-local storage, encrypted by the unlock code for the account.
class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
AccountRecordCubit(
{required LocalAccount localAccount, required UserLogin userLogin})
: super(
decodeState: proto.Account.fromBuffer,
open: () => _open(localAccount, userLogin));
static Future<DHTRecord> _open(
LocalAccount localAccount, UserLogin userLogin) async {
// Record not yet open, do it
final pool = DHTRecordPool.instance;
final record = await pool.openRecordOwned(
userLogin.accountRecordInfo.accountRecord,
debugName: 'AccountRecordCubit::_open::AccountRecord',
parent: localAccount.superIdentity.currentInstance.recordKey);
return record;
}
@override @override
Future<void> close() async { Future<void> close() async {
await super.close(); await super.close();
} }
////////////////////////////////////////////////////////////////////////////
// Public Interface
Future<void> updateProfile(proto.Profile profile) async {
await record.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async {
if (old == null || old.profile == profile) {
return null;
}
return old.deepCopy()..profile = profile;
});
}
} }

View file

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../repository/account_repository/account_repository.dart'; import '../repository/account_repository.dart';
class ActiveLocalAccountCubit extends Cubit<TypedKey?> { class ActiveLocalAccountCubit extends Cubit<TypedKey?> {
ActiveLocalAccountCubit(AccountRepository accountRepository) ActiveLocalAccountCubit(AccountRepository accountRepository)

View file

@ -1,4 +1,7 @@
export 'account_info_cubit.dart';
export 'account_record_cubit.dart'; export 'account_record_cubit.dart';
export 'active_local_account_cubit.dart'; export 'active_local_account_cubit.dart';
export 'local_accounts_cubit.dart'; export 'local_accounts_cubit.dart';
export 'per_account_collection_bloc_map_cubit.dart';
export 'per_account_collection_cubit.dart';
export 'user_logins_cubit.dart'; export 'user_logins_cubit.dart';

View file

@ -1,12 +1,17 @@
import 'dart:async'; import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:veilid_support/veilid_support.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../repository/account_repository/account_repository.dart'; import '../repository/account_repository.dart';
class LocalAccountsCubit extends Cubit<IList<LocalAccount>> { typedef LocalAccountsState = IList<LocalAccount>;
class LocalAccountsCubit extends Cubit<LocalAccountsState>
with StateMapFollowable<LocalAccountsState, TypedKey, LocalAccount> {
LocalAccountsCubit(AccountRepository accountRepository) LocalAccountsCubit(AccountRepository accountRepository)
: _accountRepository = accountRepository, : _accountRepository = accountRepository,
super(accountRepository.getLocalAccounts()) { super(accountRepository.getLocalAccounts()) {
@ -30,6 +35,14 @@ class LocalAccountsCubit extends Cubit<IList<LocalAccount>> {
await _accountRepositorySubscription.cancel(); await _accountRepositorySubscription.cancel();
} }
/// StateMapFollowable /////////////////////////
@override
IMap<TypedKey, LocalAccount> getStateMap(LocalAccountsState state) {
final stateValue = state;
return IMap.fromIterable(stateValue,
keyMapper: (e) => e.superIdentity.recordKey, valueMapper: (e) => e);
}
final AccountRepository _accountRepository; final AccountRepository _accountRepository;
late final StreamSubscription<AccountRepositoryChange> late final StreamSubscription<AccountRepositoryChange>
_accountRepositorySubscription; _accountRepositorySubscription;

View file

@ -0,0 +1,63 @@
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
typedef PerAccountCollectionBlocMapState
= BlocMapState<TypedKey, PerAccountCollectionState>;
/// Map of the logged in user accounts to their PerAccountCollectionCubit
/// Ensures there is an single account record cubit for each logged in account
class PerAccountCollectionBlocMapCubit extends BlocMapCubit<TypedKey,
PerAccountCollectionState, PerAccountCollectionCubit>
with StateMapFollower<LocalAccountsState, TypedKey, LocalAccount> {
PerAccountCollectionBlocMapCubit({
required Locator locator,
required AccountRepository accountRepository,
}) : _locator = locator,
_accountRepository = accountRepository {
// Follow the local accounts cubit
follow(locator<LocalAccountsCubit>());
}
// Add account record cubit
Future<void> _addPerAccountCollectionCubit(
{required TypedKey superIdentityRecordKey}) async =>
add(() => MapEntry(
superIdentityRecordKey,
PerAccountCollectionCubit(
locator: _locator,
accountInfoCubit: AccountInfoCubit(
accountRepository: _accountRepository,
superIdentityRecordKey: superIdentityRecordKey))));
/// StateFollower /////////////////////////
@override
Future<void> removeFromState(TypedKey key) => remove(key);
@override
Future<void> updateState(
TypedKey key, LocalAccount? oldValue, LocalAccount newValue) async {
// Don't replace unless this is a totally different account
// The sub-cubit's subscription will update our state later
if (oldValue != null) {
if (oldValue.superIdentity.recordKey !=
newValue.superIdentity.recordKey) {
throw StateError(
'should remove LocalAccount and make a new one, not change it, if '
'the superidentity record key has changed');
}
// This never changes anything that should result in rebuildin the
// sub-cubit
return;
}
await _addPerAccountCollectionCubit(
superIdentityRecordKey: newValue.superIdentity.recordKey);
}
////////////////////////////////////////////////////////////////////////////
final AccountRepository _accountRepository;
final Locator _locator;
}

View file

@ -0,0 +1,299 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../chat/chat.dart';
import '../../chat_list/chat_list.dart';
import '../../contact_invitation/contact_invitation.dart';
import '../../contacts/contacts.dart';
import '../../conversation/conversation.dart';
import '../../proto/proto.dart' as proto;
import '../account_manager.dart';
class PerAccountCollectionCubit extends Cubit<PerAccountCollectionState> {
PerAccountCollectionCubit({
required Locator locator,
required this.accountInfoCubit,
}) : _locator = locator,
super(_initialState(accountInfoCubit)) {
// Async Init
_initWait.add(_init);
}
@override
Future<void> close() async {
await _initWait();
await _processor.close();
await accountInfoCubit.close();
await _accountRecordSubscription?.cancel();
await accountRecordCubit?.close();
await activeSingleContactChatBlocMapCubitUpdater.close();
await activeConversationsBlocMapCubitUpdater.close();
await activeChatCubitUpdater.close();
await waitingInvitationsBlocMapCubitUpdater.close();
await chatListCubitUpdater.close();
await contactListCubitUpdater.close();
await contactInvitationListCubitUpdater.close();
await super.close();
}
Future<void> _init() async {
// subscribe to accountInfo changes
_processor.follow(accountInfoCubit.stream, accountInfoCubit.state,
_followAccountInfoState);
}
static PerAccountCollectionState _initialState(
AccountInfoCubit accountInfoCubit) =>
PerAccountCollectionState(
accountInfo: accountInfoCubit.state,
avAccountRecordState: const AsyncValue.loading(),
contactInvitationListCubit: null,
accountInfoCubit: null,
accountRecordCubit: null,
contactListCubit: null,
waitingInvitationsBlocMapCubit: null,
activeChatCubit: null,
chatListCubit: null,
activeConversationsBlocMapCubit: null,
activeSingleContactChatBlocMapCubit: null);
Future<void> _followAccountInfoState(AccountInfo accountInfo) async {
// Get the next state
var nextState = state.copyWith(accountInfo: accountInfo);
// Update AccountRecordCubit
if (accountInfo.userLogin == null) {
/////////////// Not logged in /////////////////
// Unsubscribe AccountRecordCubit
await _accountRecordSubscription?.cancel();
_accountRecordSubscription = null;
// Update state to 'loading'
nextState = _updateAccountRecordState(nextState, null);
emit(nextState);
// Close AccountRecordCubit
await accountRecordCubit?.close();
accountRecordCubit = null;
} else {
///////////////// Logged in ///////////////////
// Create AccountRecordCubit
accountRecordCubit ??= AccountRecordCubit(
localAccount: accountInfo.localAccount,
userLogin: accountInfo.userLogin!);
// Update state to value
nextState =
_updateAccountRecordState(nextState, accountRecordCubit!.state);
emit(nextState);
// Subscribe AccountRecordCubit
_accountRecordSubscription ??=
accountRecordCubit!.stream.listen((avAccountRecordState) {
emit(_updateAccountRecordState(state, avAccountRecordState));
});
}
}
PerAccountCollectionState _updateAccountRecordState(
PerAccountCollectionState prevState,
AsyncValue<AccountRecordState>? avAccountRecordState) {
// Get next state
final nextState =
prevState.copyWith(avAccountRecordState: avAccountRecordState);
// Get bloc parameters
final accountInfo = nextState.accountInfo;
// ContactInvitationListCubit
final contactInvitationListRecordPointer = nextState
.avAccountRecordState?.asData?.value.contactInvitationRecords
.toVeilid();
final contactInvitationListCubit = contactInvitationListCubitUpdater.update(
accountInfo.userLogin == null ||
contactInvitationListRecordPointer == null
? null
: (accountInfo, contactInvitationListRecordPointer));
// ContactListCubit
final contactListRecordPointer =
nextState.avAccountRecordState?.asData?.value.contactList.toVeilid();
final contactListCubit = contactListCubitUpdater.update(
accountInfo.userLogin == null || contactListRecordPointer == null
? null
: (accountInfo, contactListRecordPointer));
// WaitingInvitationsBlocMapCubit
final waitingInvitationsBlocMapCubit = waitingInvitationsBlocMapCubitUpdater
.update(accountInfo.userLogin == null ||
contactInvitationListCubit == null ||
contactListCubit == null
? null
: (
accountInfo,
accountRecordCubit!,
contactInvitationListCubit,
contactListCubit,
));
// ActiveChatCubit
final activeChatCubit = activeChatCubitUpdater
.update((accountInfo.userLogin == null) ? null : true);
// ChatListCubit
final chatListRecordPointer =
nextState.avAccountRecordState?.asData?.value.chatList.toVeilid();
final chatListCubit = chatListCubitUpdater.update(
accountInfo.userLogin == null ||
chatListRecordPointer == null ||
activeChatCubit == null
? null
: (accountInfo, chatListRecordPointer, activeChatCubit));
// ActiveConversationsBlocMapCubit
final activeConversationsBlocMapCubit =
activeConversationsBlocMapCubitUpdater.update(
accountRecordCubit == null ||
chatListCubit == null ||
contactListCubit == null
? null
: (
accountInfo,
accountRecordCubit!,
chatListCubit,
contactListCubit
));
// ActiveSingleContactChatBlocMapCubit
final activeSingleContactChatBlocMapCubit =
activeSingleContactChatBlocMapCubitUpdater.update(
accountInfo.userLogin == null ||
activeConversationsBlocMapCubit == null
? null
: (
accountInfo,
activeConversationsBlocMapCubit,
));
// Update available blocs in our state
return nextState.copyWith(
contactInvitationListCubit: contactInvitationListCubit,
accountInfoCubit: accountInfoCubit,
accountRecordCubit: accountRecordCubit,
contactListCubit: contactListCubit,
waitingInvitationsBlocMapCubit: waitingInvitationsBlocMapCubit,
activeChatCubit: activeChatCubit,
chatListCubit: chatListCubit,
activeConversationsBlocMapCubit: activeConversationsBlocMapCubit,
activeSingleContactChatBlocMapCubit:
activeSingleContactChatBlocMapCubit);
}
T collectionLocator<T>() {
if (T is AccountInfoCubit) {
return accountInfoCubit as T;
}
if (T is AccountRecordCubit) {
return accountRecordCubit! as T;
}
if (T is ContactInvitationListCubit) {
return contactInvitationListCubitUpdater.bloc! as T;
}
if (T is ContactListCubit) {
return contactListCubitUpdater.bloc! as T;
}
if (T is WaitingInvitationsBlocMapCubit) {
return waitingInvitationsBlocMapCubitUpdater.bloc! as T;
}
if (T is ActiveChatCubit) {
return activeChatCubitUpdater.bloc! as T;
}
if (T is ChatListCubit) {
return chatListCubitUpdater.bloc! as T;
}
if (T is ActiveConversationsBlocMapCubit) {
return activeConversationsBlocMapCubitUpdater.bloc! as T;
}
if (T is ActiveSingleContactChatBlocMapCubit) {
return activeSingleContactChatBlocMapCubitUpdater.bloc! as T;
}
return _locator<T>();
}
final Locator _locator;
final _processor = SingleStateProcessor<AccountInfo>();
final _initWait = WaitSet<void>();
// Per-account cubits regardless of login state
final AccountInfoCubit accountInfoCubit;
// Per logged-in account cubits
AccountRecordCubit? accountRecordCubit;
StreamSubscription<AsyncValue<AccountRecordState>>?
_accountRecordSubscription;
final contactInvitationListCubitUpdater = BlocUpdater<
ContactInvitationListCubit, (AccountInfo, OwnedDHTRecordPointer)>(
create: (params) => ContactInvitationListCubit(
accountInfo: params.$1,
contactInvitationListRecordPointer: params.$2,
));
final contactListCubitUpdater =
BlocUpdater<ContactListCubit, (AccountInfo, OwnedDHTRecordPointer)>(
create: (params) => ContactListCubit(
accountInfo: params.$1,
contactListRecordPointer: params.$2,
));
final waitingInvitationsBlocMapCubitUpdater = BlocUpdater<
WaitingInvitationsBlocMapCubit,
(
AccountInfo,
AccountRecordCubit,
ContactInvitationListCubit,
ContactListCubit
)>(
create: (params) => WaitingInvitationsBlocMapCubit(
accountInfo: params.$1,
accountRecordCubit: params.$2,
contactInvitationListCubit: params.$3,
contactListCubit: params.$4,
));
final activeChatCubitUpdater =
BlocUpdater<ActiveChatCubit, bool>(create: (_) => ActiveChatCubit(null));
final chatListCubitUpdater = BlocUpdater<ChatListCubit,
(AccountInfo, OwnedDHTRecordPointer, ActiveChatCubit)>(
create: (params) => ChatListCubit(
accountInfo: params.$1,
chatListRecordPointer: params.$2,
activeChatCubit: params.$3));
final activeConversationsBlocMapCubitUpdater = BlocUpdater<
ActiveConversationsBlocMapCubit,
(AccountInfo, AccountRecordCubit, ChatListCubit, ContactListCubit)>(
create: (params) => ActiveConversationsBlocMapCubit(
accountInfo: params.$1,
accountRecordCubit: params.$2,
chatListCubit: params.$3,
contactListCubit: params.$4));
final activeSingleContactChatBlocMapCubitUpdater = BlocUpdater<
ActiveSingleContactChatBlocMapCubit,
(
AccountInfo,
ActiveConversationsBlocMapCubit,
)>(
create: (params) => ActiveSingleContactChatBlocMapCubit(
accountInfo: params.$1,
activeConversationsBlocMapCubit: params.$2,
));
}

View file

@ -4,9 +4,11 @@ import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../repository/account_repository/account_repository.dart'; import '../repository/account_repository.dart';
class UserLoginsCubit extends Cubit<IList<UserLogin>> { typedef UserLoginsState = IList<UserLogin>;
class UserLoginsCubit extends Cubit<UserLoginsState> {
UserLoginsCubit(AccountRepository accountRepository) UserLoginsCubit(AccountRepository accountRepository)
: _accountRepository = accountRepository, : _accountRepository = accountRepository,
super(accountRepository.getUserLogins()) { super(accountRepository.getUserLogins()) {
@ -29,6 +31,7 @@ class UserLoginsCubit extends Cubit<IList<UserLogin>> {
await super.close(); await super.close();
await _accountRepositorySubscription.cancel(); await _accountRepositorySubscription.cancel();
} }
////////////////////////////////////////////////////////////////////////////
final AccountRepository _accountRepository; final AccountRepository _accountRepository;
late final StreamSubscription<AccountRepositoryChange> late final StreamSubscription<AccountRepositoryChange>

View file

@ -1,23 +1,62 @@
import 'package:meta/meta.dart'; import 'dart:convert';
import 'active_account_info.dart'; import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart';
import '../account_manager.dart';
enum AccountInfoStatus { enum AccountInfoStatus {
noAccount,
accountInvalid, accountInvalid,
accountLocked, accountLocked,
accountReady, accountUnlocked,
} }
@immutable @immutable
class AccountInfo { class AccountInfo extends Equatable {
const AccountInfo({ const AccountInfo({
required this.status, required this.status,
required this.active, required this.localAccount,
required this.activeAccountInfo, required this.userLogin,
}); });
final AccountInfoStatus status; final AccountInfoStatus status;
final bool active; final LocalAccount localAccount;
final ActiveAccountInfo? activeAccountInfo; final UserLogin? userLogin;
@override
List<Object?> get props => [
status,
localAccount,
userLogin,
];
}
extension AccountInfoExt on AccountInfo {
TypedKey get superIdentityRecordKey => localAccount.superIdentity.recordKey;
TypedKey get accountRecordKey =>
userLogin!.accountRecordInfo.accountRecord.recordKey;
TypedKey get identityTypedPublicKey =>
localAccount.superIdentity.currentInstance.typedPublicKey;
PublicKey get identityPublicKey =>
localAccount.superIdentity.currentInstance.publicKey;
SecretKey get identitySecretKey => userLogin!.identitySecret.value;
KeyPair get identityWriter =>
KeyPair(key: identityPublicKey, secret: identitySecretKey);
Future<VeilidCryptoSystem> get identityCryptoSystem =>
localAccount.superIdentity.currentInstance.cryptoSystem;
Future<VeilidCrypto> makeConversationCrypto(
TypedKey remoteIdentityPublicKey) async {
final identitySecret = userLogin!.identitySecret;
final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind);
final sharedSecret = await cs.generateSharedSecret(
remoteIdentityPublicKey.value,
identitySecret.value,
utf8.encode('VeilidChat Conversation'));
final messagesCrypto = await VeilidCryptoPrivate.fromSharedSecret(
identitySecret.kind, sharedSecret);
return messagesCrypto;
}
} }

View file

@ -1,47 +0,0 @@
import 'dart:convert';
import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart';
import 'local_account/local_account.dart';
import 'user_login/user_login.dart';
@immutable
class ActiveAccountInfo {
const ActiveAccountInfo({
required this.localAccount,
required this.userLogin,
});
//
TypedKey get superIdentityRecordKey => localAccount.superIdentity.recordKey;
TypedKey get accountRecordKey =>
userLogin.accountRecordInfo.accountRecord.recordKey;
TypedKey get identityTypedPublicKey =>
localAccount.superIdentity.currentInstance.typedPublicKey;
PublicKey get identityPublicKey =>
localAccount.superIdentity.currentInstance.publicKey;
SecretKey get identitySecretKey => userLogin.identitySecret.value;
KeyPair get identityWriter =>
KeyPair(key: identityPublicKey, secret: identitySecretKey);
Future<VeilidCryptoSystem> get identityCryptoSystem =>
localAccount.superIdentity.currentInstance.cryptoSystem;
Future<VeilidCrypto> makeConversationCrypto(
TypedKey remoteIdentityPublicKey) async {
final identitySecret = userLogin.identitySecret;
final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind);
final sharedSecret = await cs.generateSharedSecret(
remoteIdentityPublicKey.value,
identitySecret.value,
utf8.encode('VeilidChat Conversation'));
final messagesCrypto = await VeilidCryptoPrivate.fromSharedSecret(
identitySecret.kind, sharedSecret);
return messagesCrypto;
}
//
final LocalAccount localAccount;
final UserLogin userLogin;
}

View file

@ -1,6 +1,6 @@
export 'account_info.dart'; export 'account_info.dart';
export 'active_account_info.dart';
export 'encryption_key_type.dart'; export 'encryption_key_type.dart';
export 'local_account/local_account.dart'; export 'local_account/local_account.dart';
export 'new_profile_spec.dart'; export 'new_profile_spec.dart';
export 'per_account_collection_state/per_account_collection_state.dart';
export 'user_login/user_login.dart'; export 'user_login/user_login.dart';

View file

@ -0,0 +1,59 @@
import 'package:async_tools/async_tools.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../chat/chat.dart';
import '../../../chat_list/chat_list.dart';
import '../../../contact_invitation/contact_invitation.dart';
import '../../../contacts/contacts.dart';
import '../../../conversation/conversation.dart';
import '../../../proto/proto.dart' show Account;
import '../../account_manager.dart';
part 'per_account_collection_state.freezed.dart';
@freezed
class PerAccountCollectionState with _$PerAccountCollectionState {
const factory PerAccountCollectionState({
required AccountInfo accountInfo,
required AsyncValue<AccountRecordState>? avAccountRecordState,
required AccountInfoCubit? accountInfoCubit,
required AccountRecordCubit? accountRecordCubit,
required ContactInvitationListCubit? contactInvitationListCubit,
required ContactListCubit? contactListCubit,
required WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit,
required ActiveChatCubit? activeChatCubit,
required ChatListCubit? chatListCubit,
required ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit,
required ActiveSingleContactChatBlocMapCubit?
activeSingleContactChatBlocMapCubit,
}) = _PerAccountCollectionState;
}
extension PerAccountCollectionStateExt on PerAccountCollectionState {
bool get isReady =>
avAccountRecordState != null &&
avAccountRecordState!.isData &&
accountInfoCubit != null &&
accountRecordCubit != null &&
contactInvitationListCubit != null &&
contactListCubit != null &&
waitingInvitationsBlocMapCubit != null &&
activeChatCubit != null &&
chatListCubit != null &&
activeConversationsBlocMapCubit != null &&
activeSingleContactChatBlocMapCubit != null;
Widget provide({required Widget child}) => MultiBlocProvider(providers: [
BlocProvider.value(value: accountInfoCubit!),
BlocProvider.value(value: accountRecordCubit!),
BlocProvider.value(value: contactInvitationListCubit!),
BlocProvider.value(value: contactListCubit!),
BlocProvider.value(value: waitingInvitationsBlocMapCubit!),
BlocProvider.value(value: activeChatCubit!),
BlocProvider.value(value: chatListCubit!),
BlocProvider.value(value: activeConversationsBlocMapCubit!),
BlocProvider.value(value: activeSingleContactChatBlocMapCubit!),
], child: child);
}

View file

@ -0,0 +1,408 @@
// 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 'per_account_collection_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#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$PerAccountCollectionState {
AccountInfo get accountInfo => throw _privateConstructorUsedError;
AsyncValue<Account>? get avAccountRecordState =>
throw _privateConstructorUsedError;
AccountInfoCubit? get accountInfoCubit => throw _privateConstructorUsedError;
AccountRecordCubit? get accountRecordCubit =>
throw _privateConstructorUsedError;
ContactInvitationListCubit? get contactInvitationListCubit =>
throw _privateConstructorUsedError;
ContactListCubit? get contactListCubit => throw _privateConstructorUsedError;
WaitingInvitationsBlocMapCubit? get waitingInvitationsBlocMapCubit =>
throw _privateConstructorUsedError;
ActiveChatCubit? get activeChatCubit => throw _privateConstructorUsedError;
ChatListCubit? get chatListCubit => throw _privateConstructorUsedError;
ActiveConversationsBlocMapCubit? get activeConversationsBlocMapCubit =>
throw _privateConstructorUsedError;
ActiveSingleContactChatBlocMapCubit?
get activeSingleContactChatBlocMapCubit =>
throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$PerAccountCollectionStateCopyWith<PerAccountCollectionState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $PerAccountCollectionStateCopyWith<$Res> {
factory $PerAccountCollectionStateCopyWith(PerAccountCollectionState value,
$Res Function(PerAccountCollectionState) then) =
_$PerAccountCollectionStateCopyWithImpl<$Res, PerAccountCollectionState>;
@useResult
$Res call(
{AccountInfo accountInfo,
AsyncValue<Account>? avAccountRecordState,
AccountInfoCubit? accountInfoCubit,
AccountRecordCubit? accountRecordCubit,
ContactInvitationListCubit? contactInvitationListCubit,
ContactListCubit? contactListCubit,
WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit,
ActiveChatCubit? activeChatCubit,
ChatListCubit? chatListCubit,
ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit,
ActiveSingleContactChatBlocMapCubit?
activeSingleContactChatBlocMapCubit});
$AsyncValueCopyWith<Account, $Res>? get avAccountRecordState;
}
/// @nodoc
class _$PerAccountCollectionStateCopyWithImpl<$Res,
$Val extends PerAccountCollectionState>
implements $PerAccountCollectionStateCopyWith<$Res> {
_$PerAccountCollectionStateCopyWithImpl(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? accountInfo = null,
Object? avAccountRecordState = freezed,
Object? accountInfoCubit = freezed,
Object? accountRecordCubit = freezed,
Object? contactInvitationListCubit = freezed,
Object? contactListCubit = freezed,
Object? waitingInvitationsBlocMapCubit = freezed,
Object? activeChatCubit = freezed,
Object? chatListCubit = freezed,
Object? activeConversationsBlocMapCubit = freezed,
Object? activeSingleContactChatBlocMapCubit = freezed,
}) {
return _then(_value.copyWith(
accountInfo: null == accountInfo
? _value.accountInfo
: accountInfo // ignore: cast_nullable_to_non_nullable
as AccountInfo,
avAccountRecordState: freezed == avAccountRecordState
? _value.avAccountRecordState
: avAccountRecordState // ignore: cast_nullable_to_non_nullable
as AsyncValue<Account>?,
accountInfoCubit: freezed == accountInfoCubit
? _value.accountInfoCubit
: accountInfoCubit // ignore: cast_nullable_to_non_nullable
as AccountInfoCubit?,
accountRecordCubit: freezed == accountRecordCubit
? _value.accountRecordCubit
: accountRecordCubit // ignore: cast_nullable_to_non_nullable
as AccountRecordCubit?,
contactInvitationListCubit: freezed == contactInvitationListCubit
? _value.contactInvitationListCubit
: contactInvitationListCubit // ignore: cast_nullable_to_non_nullable
as ContactInvitationListCubit?,
contactListCubit: freezed == contactListCubit
? _value.contactListCubit
: contactListCubit // ignore: cast_nullable_to_non_nullable
as ContactListCubit?,
waitingInvitationsBlocMapCubit: freezed == waitingInvitationsBlocMapCubit
? _value.waitingInvitationsBlocMapCubit
: waitingInvitationsBlocMapCubit // ignore: cast_nullable_to_non_nullable
as WaitingInvitationsBlocMapCubit?,
activeChatCubit: freezed == activeChatCubit
? _value.activeChatCubit
: activeChatCubit // ignore: cast_nullable_to_non_nullable
as ActiveChatCubit?,
chatListCubit: freezed == chatListCubit
? _value.chatListCubit
: chatListCubit // ignore: cast_nullable_to_non_nullable
as ChatListCubit?,
activeConversationsBlocMapCubit: freezed ==
activeConversationsBlocMapCubit
? _value.activeConversationsBlocMapCubit
: activeConversationsBlocMapCubit // ignore: cast_nullable_to_non_nullable
as ActiveConversationsBlocMapCubit?,
activeSingleContactChatBlocMapCubit: freezed ==
activeSingleContactChatBlocMapCubit
? _value.activeSingleContactChatBlocMapCubit
: activeSingleContactChatBlocMapCubit // ignore: cast_nullable_to_non_nullable
as ActiveSingleContactChatBlocMapCubit?,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$AsyncValueCopyWith<Account, $Res>? get avAccountRecordState {
if (_value.avAccountRecordState == null) {
return null;
}
return $AsyncValueCopyWith<Account, $Res>(_value.avAccountRecordState!,
(value) {
return _then(_value.copyWith(avAccountRecordState: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$PerAccountCollectionStateImplCopyWith<$Res>
implements $PerAccountCollectionStateCopyWith<$Res> {
factory _$$PerAccountCollectionStateImplCopyWith(
_$PerAccountCollectionStateImpl value,
$Res Function(_$PerAccountCollectionStateImpl) then) =
__$$PerAccountCollectionStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{AccountInfo accountInfo,
AsyncValue<Account>? avAccountRecordState,
AccountInfoCubit? accountInfoCubit,
AccountRecordCubit? accountRecordCubit,
ContactInvitationListCubit? contactInvitationListCubit,
ContactListCubit? contactListCubit,
WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit,
ActiveChatCubit? activeChatCubit,
ChatListCubit? chatListCubit,
ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit,
ActiveSingleContactChatBlocMapCubit?
activeSingleContactChatBlocMapCubit});
@override
$AsyncValueCopyWith<Account, $Res>? get avAccountRecordState;
}
/// @nodoc
class __$$PerAccountCollectionStateImplCopyWithImpl<$Res>
extends _$PerAccountCollectionStateCopyWithImpl<$Res,
_$PerAccountCollectionStateImpl>
implements _$$PerAccountCollectionStateImplCopyWith<$Res> {
__$$PerAccountCollectionStateImplCopyWithImpl(
_$PerAccountCollectionStateImpl _value,
$Res Function(_$PerAccountCollectionStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountInfo = null,
Object? avAccountRecordState = freezed,
Object? accountInfoCubit = freezed,
Object? accountRecordCubit = freezed,
Object? contactInvitationListCubit = freezed,
Object? contactListCubit = freezed,
Object? waitingInvitationsBlocMapCubit = freezed,
Object? activeChatCubit = freezed,
Object? chatListCubit = freezed,
Object? activeConversationsBlocMapCubit = freezed,
Object? activeSingleContactChatBlocMapCubit = freezed,
}) {
return _then(_$PerAccountCollectionStateImpl(
accountInfo: null == accountInfo
? _value.accountInfo
: accountInfo // ignore: cast_nullable_to_non_nullable
as AccountInfo,
avAccountRecordState: freezed == avAccountRecordState
? _value.avAccountRecordState
: avAccountRecordState // ignore: cast_nullable_to_non_nullable
as AsyncValue<Account>?,
accountInfoCubit: freezed == accountInfoCubit
? _value.accountInfoCubit
: accountInfoCubit // ignore: cast_nullable_to_non_nullable
as AccountInfoCubit?,
accountRecordCubit: freezed == accountRecordCubit
? _value.accountRecordCubit
: accountRecordCubit // ignore: cast_nullable_to_non_nullable
as AccountRecordCubit?,
contactInvitationListCubit: freezed == contactInvitationListCubit
? _value.contactInvitationListCubit
: contactInvitationListCubit // ignore: cast_nullable_to_non_nullable
as ContactInvitationListCubit?,
contactListCubit: freezed == contactListCubit
? _value.contactListCubit
: contactListCubit // ignore: cast_nullable_to_non_nullable
as ContactListCubit?,
waitingInvitationsBlocMapCubit: freezed == waitingInvitationsBlocMapCubit
? _value.waitingInvitationsBlocMapCubit
: waitingInvitationsBlocMapCubit // ignore: cast_nullable_to_non_nullable
as WaitingInvitationsBlocMapCubit?,
activeChatCubit: freezed == activeChatCubit
? _value.activeChatCubit
: activeChatCubit // ignore: cast_nullable_to_non_nullable
as ActiveChatCubit?,
chatListCubit: freezed == chatListCubit
? _value.chatListCubit
: chatListCubit // ignore: cast_nullable_to_non_nullable
as ChatListCubit?,
activeConversationsBlocMapCubit: freezed ==
activeConversationsBlocMapCubit
? _value.activeConversationsBlocMapCubit
: activeConversationsBlocMapCubit // ignore: cast_nullable_to_non_nullable
as ActiveConversationsBlocMapCubit?,
activeSingleContactChatBlocMapCubit: freezed ==
activeSingleContactChatBlocMapCubit
? _value.activeSingleContactChatBlocMapCubit
: activeSingleContactChatBlocMapCubit // ignore: cast_nullable_to_non_nullable
as ActiveSingleContactChatBlocMapCubit?,
));
}
}
/// @nodoc
class _$PerAccountCollectionStateImpl implements _PerAccountCollectionState {
const _$PerAccountCollectionStateImpl(
{required this.accountInfo,
required this.avAccountRecordState,
required this.accountInfoCubit,
required this.accountRecordCubit,
required this.contactInvitationListCubit,
required this.contactListCubit,
required this.waitingInvitationsBlocMapCubit,
required this.activeChatCubit,
required this.chatListCubit,
required this.activeConversationsBlocMapCubit,
required this.activeSingleContactChatBlocMapCubit});
@override
final AccountInfo accountInfo;
@override
final AsyncValue<Account>? avAccountRecordState;
@override
final AccountInfoCubit? accountInfoCubit;
@override
final AccountRecordCubit? accountRecordCubit;
@override
final ContactInvitationListCubit? contactInvitationListCubit;
@override
final ContactListCubit? contactListCubit;
@override
final WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit;
@override
final ActiveChatCubit? activeChatCubit;
@override
final ChatListCubit? chatListCubit;
@override
final ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit;
@override
final ActiveSingleContactChatBlocMapCubit?
activeSingleContactChatBlocMapCubit;
@override
String toString() {
return 'PerAccountCollectionState(accountInfo: $accountInfo, avAccountRecordState: $avAccountRecordState, accountInfoCubit: $accountInfoCubit, accountRecordCubit: $accountRecordCubit, contactInvitationListCubit: $contactInvitationListCubit, contactListCubit: $contactListCubit, waitingInvitationsBlocMapCubit: $waitingInvitationsBlocMapCubit, activeChatCubit: $activeChatCubit, chatListCubit: $chatListCubit, activeConversationsBlocMapCubit: $activeConversationsBlocMapCubit, activeSingleContactChatBlocMapCubit: $activeSingleContactChatBlocMapCubit)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PerAccountCollectionStateImpl &&
(identical(other.accountInfo, accountInfo) ||
other.accountInfo == accountInfo) &&
(identical(other.avAccountRecordState, avAccountRecordState) ||
other.avAccountRecordState == avAccountRecordState) &&
(identical(other.accountInfoCubit, accountInfoCubit) ||
other.accountInfoCubit == accountInfoCubit) &&
(identical(other.accountRecordCubit, accountRecordCubit) ||
other.accountRecordCubit == accountRecordCubit) &&
(identical(other.contactInvitationListCubit,
contactInvitationListCubit) ||
other.contactInvitationListCubit ==
contactInvitationListCubit) &&
(identical(other.contactListCubit, contactListCubit) ||
other.contactListCubit == contactListCubit) &&
(identical(other.waitingInvitationsBlocMapCubit,
waitingInvitationsBlocMapCubit) ||
other.waitingInvitationsBlocMapCubit ==
waitingInvitationsBlocMapCubit) &&
(identical(other.activeChatCubit, activeChatCubit) ||
other.activeChatCubit == activeChatCubit) &&
(identical(other.chatListCubit, chatListCubit) ||
other.chatListCubit == chatListCubit) &&
(identical(other.activeConversationsBlocMapCubit,
activeConversationsBlocMapCubit) ||
other.activeConversationsBlocMapCubit ==
activeConversationsBlocMapCubit) &&
(identical(other.activeSingleContactChatBlocMapCubit,
activeSingleContactChatBlocMapCubit) ||
other.activeSingleContactChatBlocMapCubit ==
activeSingleContactChatBlocMapCubit));
}
@override
int get hashCode => Object.hash(
runtimeType,
accountInfo,
avAccountRecordState,
accountInfoCubit,
accountRecordCubit,
contactInvitationListCubit,
contactListCubit,
waitingInvitationsBlocMapCubit,
activeChatCubit,
chatListCubit,
activeConversationsBlocMapCubit,
activeSingleContactChatBlocMapCubit);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$PerAccountCollectionStateImplCopyWith<_$PerAccountCollectionStateImpl>
get copyWith => __$$PerAccountCollectionStateImplCopyWithImpl<
_$PerAccountCollectionStateImpl>(this, _$identity);
}
abstract class _PerAccountCollectionState implements PerAccountCollectionState {
const factory _PerAccountCollectionState(
{required final AccountInfo accountInfo,
required final AsyncValue<Account>? avAccountRecordState,
required final AccountInfoCubit? accountInfoCubit,
required final AccountRecordCubit? accountRecordCubit,
required final ContactInvitationListCubit? contactInvitationListCubit,
required final ContactListCubit? contactListCubit,
required final WaitingInvitationsBlocMapCubit?
waitingInvitationsBlocMapCubit,
required final ActiveChatCubit? activeChatCubit,
required final ChatListCubit? chatListCubit,
required final ActiveConversationsBlocMapCubit?
activeConversationsBlocMapCubit,
required final ActiveSingleContactChatBlocMapCubit?
activeSingleContactChatBlocMapCubit}) =
_$PerAccountCollectionStateImpl;
@override
AccountInfo get accountInfo;
@override
AsyncValue<Account>? get avAccountRecordState;
@override
AccountInfoCubit? get accountInfoCubit;
@override
AccountRecordCubit? get accountRecordCubit;
@override
ContactInvitationListCubit? get contactInvitationListCubit;
@override
ContactListCubit? get contactListCubit;
@override
WaitingInvitationsBlocMapCubit? get waitingInvitationsBlocMapCubit;
@override
ActiveChatCubit? get activeChatCubit;
@override
ChatListCubit? get chatListCubit;
@override
ActiveConversationsBlocMapCubit? get activeConversationsBlocMapCubit;
@override
ActiveSingleContactChatBlocMapCubit? get activeSingleContactChatBlocMapCubit;
@override
@JsonKey(ignore: true)
_$$PerAccountCollectionStateImplCopyWith<_$PerAccountCollectionStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

View file

@ -3,9 +3,9 @@ import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.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;
import '../../../tools/tools.dart'; import '../../tools/tools.dart';
import '../../models/models.dart'; import '../models/models.dart';
const String veilidChatAccountKey = 'com.veilid.veilidchat'; const String veilidChatAccountKey = 'com.veilid.veilidchat';
@ -45,19 +45,6 @@ class AccountRepository {
valueToJson: (val) => val?.toJson(), valueToJson: (val) => val?.toJson(),
makeInitialValue: () => null); makeInitialValue: () => null);
//////////////////////////////////////////////////////////////
/// Fields
final TableDBValue<IList<LocalAccount>> _localAccounts;
final TableDBValue<IList<UserLogin>> _userLogins;
final TableDBValue<TypedKey?> _activeLocalAccount;
final StreamController<AccountRepositoryChange> _streamController;
//////////////////////////////////////////////////////////////
/// Singleton initialization
static AccountRepository instance = AccountRepository._();
Future<void> init() async { Future<void> init() async {
await _localAccounts.get(); await _localAccounts.get();
await _userLogins.get(); await _userLogins.get();
@ -71,12 +58,10 @@ class AccountRepository {
} }
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
/// Streams /// Public Interface
///
Stream<AccountRepositoryChange> get stream => _streamController.stream; Stream<AccountRepositoryChange> get stream => _streamController.stream;
//////////////////////////////////////////////////////////////
/// Selectors
IList<LocalAccount> getLocalAccounts() => _localAccounts.value; IList<LocalAccount> getLocalAccounts() => _localAccounts.value;
TypedKey? getActiveLocalAccount() => _activeLocalAccount.value; TypedKey? getActiveLocalAccount() => _activeLocalAccount.value;
IList<UserLogin> getUserLogins() => _userLogins.value; IList<UserLogin> getUserLogins() => _userLogins.value;
@ -107,29 +92,11 @@ class AccountRepository {
return userLogins[idx]; return userLogins[idx];
} }
AccountInfo getAccountInfo(TypedKey? superIdentityRecordKey) { AccountInfo? getAccountInfo(TypedKey superIdentityRecordKey) {
// Get active account if we have one
final activeLocalAccount = getActiveLocalAccount();
if (superIdentityRecordKey == null) {
if (activeLocalAccount == null) {
// No user logged in
return const AccountInfo(
status: AccountInfoStatus.noAccount,
active: false,
activeAccountInfo: null);
}
superIdentityRecordKey = activeLocalAccount;
}
final active = superIdentityRecordKey == activeLocalAccount;
// Get which local account we want to fetch the profile for // Get which local account we want to fetch the profile for
final localAccount = fetchLocalAccount(superIdentityRecordKey); final localAccount = fetchLocalAccount(superIdentityRecordKey);
if (localAccount == null) { if (localAccount == null) {
// account does not exist return null;
return AccountInfo(
status: AccountInfoStatus.noAccount,
active: active,
activeAccountInfo: null);
} }
// See if we've logged into this account or if it is locked // See if we've logged into this account or if it is locked
@ -138,22 +105,19 @@ class AccountRepository {
// Account was locked // Account was locked
return AccountInfo( return AccountInfo(
status: AccountInfoStatus.accountLocked, status: AccountInfoStatus.accountLocked,
active: active, localAccount: localAccount,
activeAccountInfo: null); userLogin: null,
);
} }
// Got account, decrypted and decoded // Got account, decrypted and decoded
return AccountInfo( return AccountInfo(
status: AccountInfoStatus.accountReady, status: AccountInfoStatus.accountUnlocked,
active: active, localAccount: localAccount,
activeAccountInfo: userLogin: userLogin,
ActiveAccountInfo(localAccount: localAccount, userLogin: userLogin),
); );
} }
//////////////////////////////////////////////////////////////
/// Mutators
/// Reorder accounts /// Reorder accounts
Future<void> reorderAccount(int oldIndex, int newIndex) async { Future<void> reorderAccount(int oldIndex, int newIndex) async {
final localAccounts = await _localAccounts.get(); final localAccounts = await _localAccounts.get();
@ -168,104 +132,39 @@ class AccountRepository {
/// Creates a new super identity, an identity instance, an account associated /// Creates a new super identity, an identity instance, an account associated
/// with the identity instance, stores the account in the identity key and /// with the identity instance, stores the account in the identity key and
/// then logs into that account with no password set at this time /// then logs into that account with no password set at this time
Future<void> createWithNewSuperIdentity(NewProfileSpec newProfileSpec) async { Future<SecretKey> createWithNewSuperIdentity(proto.Profile newProfile) async {
log.debug('Creating super identity'); log.debug('Creating super identity');
final wsi = await WritableSuperIdentity.create(); final wsi = await WritableSuperIdentity.create();
try { try {
final localAccount = await _newLocalAccount( final localAccount = await _newLocalAccount(
superIdentity: wsi.superIdentity, superIdentity: wsi.superIdentity,
identitySecret: wsi.identitySecret, identitySecret: wsi.identitySecret,
newProfileSpec: newProfileSpec); newProfile: newProfile);
// Log in the new account by default with no pin // Log in the new account by default with no pin
final ok = await login( final ok = await login(
localAccount.superIdentity.recordKey, EncryptionKeyType.none, ''); localAccount.superIdentity.recordKey, EncryptionKeyType.none, '');
assert(ok, 'login with none should never fail'); assert(ok, 'login with none should never fail');
return wsi.superSecret;
} on Exception catch (_) { } on Exception catch (_) {
await wsi.delete(); await wsi.delete();
rethrow; rethrow;
} }
} }
/// Creates a new Account associated with the current instance of the identity Future<void> editAccountProfile(
/// Adds a logged-out LocalAccount to track its existence on this device TypedKey superIdentityRecordKey, proto.Profile newProfile) async {
Future<LocalAccount> _newLocalAccount( log.debug('Editing profile for $superIdentityRecordKey');
{required SuperIdentity superIdentity,
required SecretKey identitySecret,
required NewProfileSpec newProfileSpec,
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
String encryptionKey = ''}) async {
log.debug('Creating new local account');
final localAccounts = await _localAccounts.get(); final localAccounts = await _localAccounts.get();
// Add account with profile to DHT final newLocalAccounts = localAccounts.replaceFirstWhere(
await superIdentity.currentInstance.addAccount( (x) => x.superIdentity.recordKey == superIdentityRecordKey,
superRecordKey: superIdentity.recordKey, (localAccount) => localAccount!.copyWith(name: newProfile.name));
secretKey: identitySecret,
accountKey: veilidChatAccountKey,
createAccountCallback: (parent) async {
// Make empty contact list
log.debug('Creating contacts list');
final contactList = await (await DHTShortArray.create(
debugName: 'AccountRepository::_newLocalAccount::Contacts',
parent: parent))
.scope((r) async => r.recordPointer);
// Make empty contact invitation record list
log.debug('Creating contact invitation records list');
final contactInvitationRecords = await (await DHTShortArray.create(
debugName:
'AccountRepository::_newLocalAccount::ContactInvitations',
parent: parent))
.scope((r) async => r.recordPointer);
// Make empty chat record list
log.debug('Creating chat records list');
final chatRecords = await (await DHTShortArray.create(
debugName: 'AccountRepository::_newLocalAccount::Chats',
parent: parent))
.scope((r) async => r.recordPointer);
// Make account object
final account = proto.Account()
..profile = (proto.Profile()
..name = newProfileSpec.name
..pronouns = newProfileSpec.pronouns)
..contactList = contactList.toProto()
..contactInvitationRecords = contactInvitationRecords.toProto()
..chatList = chatRecords.toProto();
return account.writeToBuffer();
});
// Encrypt identitySecret with key
final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes(
secret: identitySecret,
cryptoKind: superIdentity.currentInstance.recordKey.kind,
encryptionKey: encryptionKey,
);
// Create local account object
// Does not contain the account key or its secret
// as that is not to be persisted, and only pulled from the identity key
// and optionally decrypted with the unlock password
final localAccount = LocalAccount(
superIdentity: superIdentity,
identitySecretBytes: identitySecretBytes,
encryptionKeyType: encryptionKeyType,
biometricsEnabled: false,
hiddenAccount: false,
name: newProfileSpec.name,
);
// Add local account object to internal store
final newLocalAccounts = localAccounts.add(localAccount);
await _localAccounts.set(newLocalAccounts); await _localAccounts.set(newLocalAccounts);
_streamController.add(AccountRepositoryChange.localAccounts); _streamController.add(AccountRepositoryChange.localAccounts);
// Return local account object
return localAccount;
} }
/// Remove an account and wipe the messages for this account from this device /// Remove an account and wipe the messages for this account from this device
@ -307,6 +206,88 @@ class AccountRepository {
_streamController.add(AccountRepositoryChange.activeLocalAccount); _streamController.add(AccountRepositoryChange.activeLocalAccount);
} }
//////////////////////////////////////////////////////////////
/// Internal Implementation
/// Creates a new Account associated with the current instance of the identity
/// Adds a logged-out LocalAccount to track its existence on this device
Future<LocalAccount> _newLocalAccount(
{required SuperIdentity superIdentity,
required SecretKey identitySecret,
required proto.Profile newProfile,
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
String encryptionKey = ''}) async {
log.debug('Creating new local account');
final localAccounts = await _localAccounts.get();
// Add account with profile to DHT
await superIdentity.currentInstance.addAccount(
superRecordKey: superIdentity.recordKey,
secretKey: identitySecret,
accountKey: veilidChatAccountKey,
createAccountCallback: (parent) async {
// Make empty contact list
log.debug('Creating contacts list');
final contactList = await (await DHTShortArray.create(
debugName: 'AccountRepository::_newLocalAccount::Contacts',
parent: parent))
.scope((r) async => r.recordPointer);
// Make empty contact invitation record list
log.debug('Creating contact invitation records list');
final contactInvitationRecords = await (await DHTShortArray.create(
debugName:
'AccountRepository::_newLocalAccount::ContactInvitations',
parent: parent))
.scope((r) async => r.recordPointer);
// Make empty chat record list
log.debug('Creating chat records list');
final chatRecords = await (await DHTShortArray.create(
debugName: 'AccountRepository::_newLocalAccount::Chats',
parent: parent))
.scope((r) async => r.recordPointer);
// Make account object
final account = proto.Account()
..profile = newProfile
..contactList = contactList.toProto()
..contactInvitationRecords = contactInvitationRecords.toProto()
..chatList = chatRecords.toProto();
return account.writeToBuffer();
});
// Encrypt identitySecret with key
final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes(
secret: identitySecret,
cryptoKind: superIdentity.currentInstance.recordKey.kind,
encryptionKey: encryptionKey,
);
// Create local account object
// Does not contain the account key or its secret
// as that is not to be persisted, and only pulled from the identity key
// and optionally decrypted with the unlock password
final localAccount = LocalAccount(
superIdentity: superIdentity,
identitySecretBytes: identitySecretBytes,
encryptionKeyType: encryptionKeyType,
biometricsEnabled: false,
hiddenAccount: false,
name: newProfile.name,
);
// Add local account object to internal store
final newLocalAccounts = localAccounts.add(localAccount);
await _localAccounts.set(newLocalAccounts);
_streamController.add(AccountRepositoryChange.localAccounts);
// Return local account object
return localAccount;
}
Future<bool> _decryptedLogin( Future<bool> _decryptedLogin(
SuperIdentity superIdentity, SecretKey identitySecret) async { SuperIdentity superIdentity, SecretKey identitySecret) async {
// Verify identity secret works and return the valid cryptosystem // Verify identity secret works and return the valid cryptosystem
@ -399,16 +380,13 @@ class AccountRepository {
_streamController.add(AccountRepositoryChange.userLogins); _streamController.add(AccountRepositoryChange.userLogins);
} }
Future<DHTRecord> openAccountRecord(UserLogin userLogin) async { //////////////////////////////////////////////////////////////
final localAccount = fetchLocalAccount(userLogin.superIdentityRecordKey)!; /// Fields
// Record not yet open, do it static AccountRepository instance = AccountRepository._();
final pool = DHTRecordPool.instance;
final record = await pool.openRecordOwned(
userLogin.accountRecordInfo.accountRecord,
debugName: 'AccountRepository::openAccountRecord::AccountRecord',
parent: localAccount.superIdentity.currentInstance.recordKey);
return record; final TableDBValue<IList<LocalAccount>> _localAccounts;
} final TableDBValue<IList<UserLogin>> _userLogins;
final TableDBValue<TypedKey?> _activeLocalAccount;
final StreamController<AccountRepositoryChange> _streamController;
} }

View file

@ -1 +1 @@
export 'account_repository/account_repository.dart'; export 'account_repository.dart';

View file

@ -0,0 +1,158 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
import 'package:protobuf/protobuf.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../layout/default_app_bar.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import '../../tools/tools.dart';
import '../../veilid_processor/veilid_processor.dart';
import '../account_manager.dart';
import 'profile_edit_form.dart';
class EditAccountPage extends StatefulWidget {
const EditAccountPage(
{required this.superIdentityRecordKey,
required this.existingProfile,
super.key});
@override
State createState() => _EditAccountPageState();
final TypedKey superIdentityRecordKey;
final proto.Profile existingProfile;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<TypedKey>(
'superIdentityRecordKey', superIdentityRecordKey))
..add(DiagnosticsProperty<proto.Profile>(
'existingProfile', existingProfile));
}
}
class _EditAccountPageState extends State<EditAccountPage> {
bool _isInAsyncCall = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.portraitOnly);
});
}
Widget _editAccountForm(BuildContext context,
{required Future<void> Function(GlobalKey<FormBuilderState>)
onSubmit}) =>
EditProfileForm(
header: translate('edit_account_page.header'),
instructions: translate('edit_account_page.instructions'),
submitText: translate('edit_account_page.update'),
submitDisabledText: translate('button.waiting_for_network'),
onSubmit: onSubmit,
initialValueCallback: (key) => switch (key) {
EditProfileForm.formFieldName => widget.existingProfile.name,
EditProfileForm.formFieldPronouns => widget.existingProfile.pronouns,
String() => throw UnimplementedError(),
},
);
@override
Widget build(BuildContext context) {
final displayModalHUD = _isInAsyncCall;
return Scaffold(
// resizeToAvoidBottomInset: false,
appBar: DefaultAppBar(
title: Text(translate('edit_account_page.titlebar')),
leading: Navigator.canPop(context)
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
)
: null,
actions: [
const SignalStrengthMeterWidget(),
IconButton(
icon: const Icon(Icons.settings),
tooltip: translate('menu.settings_tooltip'),
onPressed: () async {
await GoRouterHelper(context).push('/settings');
})
]),
body: _editAccountForm(
context,
onSubmit: (formKey) async {
// dismiss the keyboard by unfocusing the textfield
FocusScope.of(context).unfocus();
try {
final name = formKey.currentState!
.fields[EditProfileForm.formFieldName]!.value as String;
final pronouns = formKey
.currentState!
.fields[EditProfileForm.formFieldPronouns]!
.value as String? ??
'';
final newProfile = widget.existingProfile.deepCopy()
..name = name
..pronouns = pronouns
..timestamp = Veilid.instance.now().toInt64();
setState(() {
_isInAsyncCall = true;
});
try {
// Look up account cubit for this specific account
final perAccountCollectionBlocMapCubit =
context.read<PerAccountCollectionBlocMapCubit>();
final accountRecordCubit = await perAccountCollectionBlocMapCubit
.operate(widget.superIdentityRecordKey,
closure: (c) async => c.accountRecordCubit);
if (accountRecordCubit == null) {
return;
}
// Update account profile DHT record
// This triggers ConversationCubits to update
await accountRecordCubit.updateProfile(newProfile);
// Update local account profile
await AccountRepository.instance.editAccountProfile(
widget.superIdentityRecordKey, newProfile);
if (context.mounted) {
Navigator.canPop(context)
? GoRouterHelper(context).pop()
: GoRouterHelper(context).go('/');
}
} finally {
if (mounted) {
setState(() {
_isInAsyncCall = false;
});
}
}
} on Exception catch (e) {
if (context.mounted) {
await showErrorModal(context,
translate('edit_account_page.error'), 'Exception: $e');
}
}
},
).paddingSymmetric(horizontal: 24, vertical: 8),
).withModalHUD(context, displayModalHUD);
}
}

View file

@ -1,30 +1,27 @@
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../layout/default_app_bar.dart'; import '../../layout/default_app_bar.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../../veilid_processor/veilid_processor.dart'; import '../../veilid_processor/veilid_processor.dart';
import '../account_manager.dart'; import '../account_manager.dart';
import 'profile_edit_form.dart';
class NewAccountPage extends StatefulWidget { class NewAccountPage extends StatefulWidget {
const NewAccountPage({super.key}); const NewAccountPage({super.key});
@override @override
NewAccountPageState createState() => NewAccountPageState(); State createState() => _NewAccountPageState();
} }
class NewAccountPageState extends State<NewAccountPage> { class _NewAccountPageState extends State<NewAccountPage> {
final _formKey = GlobalKey<FormBuilderState>(); bool _isInAsyncCall = false;
late bool isInAsyncCall = false;
static const String formFieldName = 'name';
static const String formFieldPronouns = 'pronouns';
@override @override
void initState() { void initState() {
@ -47,80 +44,35 @@ class NewAccountPageState extends State<NewAccountPage> {
false; false;
final canSubmit = networkReady; final canSubmit = networkReady;
return FormBuilder( return EditProfileForm(
key: _formKey, header: translate('new_account_page.header'),
child: ListView( instructions: translate('new_account_page.instructions'),
children: [ submitText: translate('new_account_page.create'),
Text(translate('new_account_page.header')) submitDisabledText: translate('button.waiting_for_network'),
.textStyle(context.headlineSmall) onSubmit: !canSubmit ? null : onSubmit);
.paddingSymmetric(vertical: 16),
FormBuilderTextField(
autofocus: true,
name: formFieldName,
decoration:
InputDecoration(labelText: translate('account.form_name')),
maxLength: 64,
// The validator receives the text that the user has entered.
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
]),
textInputAction: TextInputAction.next,
),
FormBuilderTextField(
name: formFieldPronouns,
maxLength: 64,
decoration:
InputDecoration(labelText: translate('account.form_pronouns')),
textInputAction: TextInputAction.next,
),
Row(children: [
const Spacer(),
Text(translate('new_account_page.instructions'))
.toCenter()
.flexible(flex: 6),
const Spacer(),
]).paddingSymmetric(vertical: 4),
ElevatedButton(
onPressed: !canSubmit
? null
: () async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
setState(() {
isInAsyncCall = true;
});
try {
await onSubmit(_formKey);
} finally {
if (mounted) {
setState(() {
isInAsyncCall = false;
});
}
}
}
},
child: Text(translate(!networkReady
? 'button.waiting_for_network'
: 'new_account_page.create')),
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
],
),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final displayModalHUD = isInAsyncCall; final displayModalHUD = _isInAsyncCall;
return Scaffold( return Scaffold(
// resizeToAvoidBottomInset: false, // resizeToAvoidBottomInset: false,
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: Text(translate('new_account_page.titlebar')), title: Text(translate('new_account_page.titlebar')),
leading: Navigator.canPop(context)
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
)
: null,
actions: [ actions: [
const SignalStrengthMeterWidget(), const SignalStrengthMeterWidget(),
IconButton( IconButton(
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
tooltip: translate('app_bar.settings_tooltip'), tooltip: translate('menu.settings_tooltip'),
onPressed: () async { onPressed: () async {
await GoRouterHelper(context).push('/settings'); await GoRouterHelper(context).push('/settings');
}) })
@ -132,16 +84,33 @@ class NewAccountPageState extends State<NewAccountPage> {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
try { try {
final name = final name = formKey.currentState!
_formKey.currentState!.fields[formFieldName]!.value as String; .fields[EditProfileForm.formFieldName]!.value as String;
final pronouns = _formKey.currentState!.fields[formFieldPronouns]! final pronouns = formKey
.currentState!
.fields[EditProfileForm.formFieldPronouns]!
.value as String? ?? .value as String? ??
''; '';
final newProfileSpec = final newProfile = proto.Profile()
NewProfileSpec(name: name, pronouns: pronouns); ..name = name
..pronouns = pronouns;
await AccountRepository.instance setState(() {
.createWithNewSuperIdentity(newProfileSpec); _isInAsyncCall = true;
});
try {
final superSecret = await AccountRepository.instance
.createWithNewSuperIdentity(newProfile);
GoRouterHelper(context).pushReplacement(
'/new_account/recovery_key',
extra: superSecret);
} finally {
if (mounted) {
setState(() {
_isInAsyncCall = false;
});
}
}
} on Exception catch (e) { } on Exception catch (e) {
if (context.mounted) { if (context.mounted) {
await showErrorModal(context, translate('new_account_page.error'), await showErrorModal(context, translate('new_account_page.error'),
@ -152,10 +121,4 @@ class NewAccountPageState extends State<NewAccountPage> {
).paddingSymmetric(horizontal: 24, vertical: 8), ).paddingSymmetric(horizontal: 24, vertical: 8),
).withModalHUD(context, displayModalHUD); ).withModalHUD(context, displayModalHUD);
} }
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('isInAsyncCall', isInAsyncCall));
}
} }

View file

@ -0,0 +1,114 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
class EditProfileForm extends StatefulWidget {
const EditProfileForm({
required this.header,
required this.instructions,
required this.submitText,
required this.submitDisabledText,
super.key,
this.onSubmit,
this.initialValueCallback,
});
@override
State createState() => _EditProfileFormState();
final String header;
final String instructions;
final Future<void> Function(GlobalKey<FormBuilderState>)? onSubmit;
final String submitText;
final String submitDisabledText;
final Object? Function(String key)? initialValueCallback;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(StringProperty('header', header))
..add(StringProperty('instructions', instructions))
..add(ObjectFlagProperty<
Future<void> Function(
GlobalKey<FormBuilderState> p1)?>.has('onSubmit', onSubmit))
..add(StringProperty('submitText', submitText))
..add(StringProperty('submitDisabledText', submitDisabledText))
..add(ObjectFlagProperty<Object? Function(String key)?>.has(
'initialValueCallback', initialValueCallback));
}
static const String formFieldName = 'name';
static const String formFieldPronouns = 'pronouns';
}
class _EditProfileFormState extends State<EditProfileForm> {
final _formKey = GlobalKey<FormBuilderState>();
@override
void initState() {
super.initState();
}
Widget _editProfileForm(
BuildContext context,
) =>
FormBuilder(
key: _formKey,
child: ListView(
children: [
Text(widget.header)
.textStyle(context.headlineSmall)
.paddingSymmetric(vertical: 16),
FormBuilderTextField(
autofocus: true,
name: EditProfileForm.formFieldName,
initialValue: widget.initialValueCallback
?.call(EditProfileForm.formFieldName) as String?,
decoration:
InputDecoration(labelText: translate('account.form_name')),
maxLength: 64,
// The validator receives the text that the user has entered.
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
]),
textInputAction: TextInputAction.next,
),
FormBuilderTextField(
name: EditProfileForm.formFieldPronouns,
initialValue: widget.initialValueCallback
?.call(EditProfileForm.formFieldPronouns) as String?,
maxLength: 64,
decoration: InputDecoration(
labelText: translate('account.form_pronouns')),
textInputAction: TextInputAction.next,
),
Row(children: [
const Spacer(),
Text(widget.instructions).toCenter().flexible(flex: 6),
const Spacer(),
]).paddingSymmetric(vertical: 4),
ElevatedButton(
onPressed: widget.onSubmit == null
? null
: () async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
await widget.onSubmit!(_formKey);
}
},
child: Text((widget.onSubmit == null)
? widget.submitDisabledText
: widget.submitText),
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
],
),
);
@override
Widget build(BuildContext context) => _editProfileForm(
context,
);
}

View file

@ -0,0 +1,63 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../layout/default_app_bar.dart';
import '../../tools/tools.dart';
import '../../veilid_processor/veilid_processor.dart';
class ShowRecoveryKeyPage extends StatefulWidget {
const ShowRecoveryKeyPage({required SecretKey secretKey, super.key})
: _secretKey = secretKey;
@override
ShowRecoveryKeyPageState createState() => ShowRecoveryKeyPageState();
final SecretKey _secretKey;
}
class ShowRecoveryKeyPageState extends State<ShowRecoveryKeyPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.portraitOnly);
});
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final secretKey = widget._secretKey;
return Scaffold(
// resizeToAvoidBottomInset: false,
appBar: DefaultAppBar(
title: Text(translate('show_recovery_key_page.titlebar')),
actions: [
const SignalStrengthMeterWidget(),
IconButton(
icon: const Icon(Icons.settings),
tooltip: translate('menu.settings_tooltip'),
onPressed: () async {
await GoRouterHelper(context).push('/settings');
})
]),
body: Column(children: [
Text('ASS: $secretKey'),
ElevatedButton(
onPressed: () {
if (context.mounted) {
Navigator.canPop(context)
? GoRouterHelper(context).pop()
: GoRouterHelper(context).go('/');
}
},
child: Text(translate('button.finish')))
]).paddingSymmetric(horizontal: 24, vertical: 8));
}
}

View file

@ -1,2 +1,4 @@
export 'edit_account_page.dart';
export 'new_account_page.dart'; export 'new_account_page.dart';
export 'profile_widget.dart'; export 'profile_widget.dart';
export 'show_recovery_key_page.dart';

View file

@ -11,6 +11,7 @@ import 'package:provider/provider.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 'account_manager/cubits/active_local_account_cubit.dart';
import 'init.dart'; import 'init.dart';
import 'layout/splash.dart'; import 'layout/splash.dart';
import 'router/router.dart'; import 'router/router.dart';
@ -129,7 +130,11 @@ class VeilidChatApp extends StatelessWidget {
BlocProvider<PreferencesCubit>( BlocProvider<PreferencesCubit>(
create: (context) => create: (context) =>
PreferencesCubit(PreferencesRepository.instance), PreferencesCubit(PreferencesRepository.instance),
) ),
BlocProvider<PerAccountCollectionBlocMapCubit>(
create: (context) => PerAccountCollectionBlocMapCubit(
accountRepository: AccountRepository.instance,
locator: context.read)),
], ],
child: BackgroundTicker( child: BackgroundTicker(
child: _buildShortcuts( child: _buildShortcuts(
@ -137,7 +142,7 @@ class VeilidChatApp extends StatelessWidget {
builder: (context) => MaterialApp.router( builder: (context) => MaterialApp.router(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
routerConfig: routerConfig:
context.watch<RouterCubit>().router(), context.read<RouterCubit>().router(),
title: translate('app.title'), title: translate('app.title'),
theme: theme, theme: theme,
localizationsDelegates: [ localizationsDelegates: [

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -12,7 +13,8 @@ import 'package:scroll_to_index/scroll_to_index.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 '../../chat_list/chat_list.dart'; import '../../contacts/contacts.dart';
import '../../conversation/conversation.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../models/chat_component_state.dart'; import '../models/chat_component_state.dart';
import '../models/message_state.dart'; import '../models/message_state.dart';
@ -23,18 +25,27 @@ const metadataKeyIdentityPublicKey = 'identityPublicKey';
const metadataKeyExpirationDuration = 'expiration'; const metadataKeyExpirationDuration = 'expiration';
const metadataKeyViewLimit = 'view_limit'; const metadataKeyViewLimit = 'view_limit';
const metadataKeyAttachments = 'attachments'; const metadataKeyAttachments = 'attachments';
const _sfChangedContacts = 'changedContacts';
class ChatComponentCubit extends Cubit<ChatComponentState> { class ChatComponentCubit extends Cubit<ChatComponentState> {
ChatComponentCubit._({ ChatComponentCubit._({
required AccountInfo accountInfo,
required AccountRecordCubit accountRecordCubit,
required ContactListCubit contactListCubit,
required List<ActiveConversationCubit> conversationCubits,
required SingleContactMessagesCubit messagesCubit, required SingleContactMessagesCubit messagesCubit,
required types.User localUser, }) : _accountInfo = accountInfo,
required IMap<TypedKey, types.User> remoteUsers, _accountRecordCubit = accountRecordCubit,
}) : _messagesCubit = messagesCubit, _contactListCubit = contactListCubit,
_conversationCubits = conversationCubits,
_messagesCubit = messagesCubit,
super(ChatComponentState( super(ChatComponentState(
chatKey: GlobalKey<ChatState>(), chatKey: GlobalKey<ChatState>(),
scrollController: AutoScrollController(), scrollController: AutoScrollController(),
localUser: localUser, localUser: null,
remoteUsers: remoteUsers, remoteUsers: const IMap.empty(),
historicalRemoteUsers: const IMap.empty(),
unknownUsers: const IMap.empty(),
messageWindow: const AsyncLoading(), messageWindow: const AsyncLoading(),
title: '', title: '',
)) { )) {
@ -42,54 +53,49 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
_initWait.add(_init); _initWait.add(_init);
} }
// ignore: prefer_constructors_over_static_methods factory ChatComponentCubit.singleContact(
static ChatComponentCubit singleContact( {required AccountInfo accountInfo,
{required ActiveAccountInfo activeAccountInfo, required AccountRecordCubit accountRecordCubit,
required proto.Account accountRecordInfo, required ContactListCubit contactListCubit,
required ActiveConversationState activeConversationState, required ActiveConversationCubit activeConversationCubit,
required SingleContactMessagesCubit messagesCubit}) { required SingleContactMessagesCubit messagesCubit}) =>
// Make local 'User' ChatComponentCubit._(
final localUserIdentityKey = activeAccountInfo.identityTypedPublicKey; accountInfo: accountInfo,
final localUser = types.User( accountRecordCubit: accountRecordCubit,
id: localUserIdentityKey.toString(), contactListCubit: contactListCubit,
firstName: accountRecordInfo.profile.name, conversationCubits: [activeConversationCubit],
metadata: {metadataKeyIdentityPublicKey: localUserIdentityKey});
// Make remote 'User's
final remoteUsers = {
activeConversationState.contact.identityPublicKey.toVeilid(): types.User(
id: activeConversationState.contact.identityPublicKey
.toVeilid()
.toString(),
firstName: activeConversationState.contact.editedProfile.name,
metadata: {
metadataKeyIdentityPublicKey:
activeConversationState.contact.identityPublicKey.toVeilid()
})
}.toIMap();
return ChatComponentCubit._(
messagesCubit: messagesCubit, messagesCubit: messagesCubit,
localUser: localUser,
remoteUsers: remoteUsers,
); );
}
Future<void> _init() async { Future<void> _init() async {
_messagesSubscription = _messagesCubit.stream.listen((messagesState) { // Get local user info and account record cubit
emit(state.copyWith( _localUserIdentityKey = _accountInfo.identityTypedPublicKey;
messageWindow: _convertMessages(messagesState),
)); // Subscribe to local user info
}); _accountRecordSubscription =
emit(state.copyWith( _accountRecordCubit.stream.listen(_onChangedAccountRecord);
messageWindow: _convertMessages(_messagesCubit.state), _onChangedAccountRecord(_accountRecordCubit.state);
title: _getTitle(),
)); // Subscribe to remote user info
await _updateConversationSubscriptions();
// Subscribe to messages
_messagesSubscription = _messagesCubit.stream.listen(_onChangedMessages);
_onChangedMessages(_messagesCubit.state);
// Subscribe to contact list changes
_contactListSubscription =
_contactListCubit.stream.listen(_onChangedContacts);
_onChangedContacts(_contactListCubit.state);
} }
@override @override
Future<void> close() async { Future<void> close() async {
await _initWait(); await _initWait();
await _contactListSubscription.cancel();
await _accountRecordSubscription.cancel();
await _messagesSubscription.cancel(); await _messagesSubscription.cancel();
await _conversationSubscriptions.values.map((v) => v.cancel()).wait;
await super.close(); await super.close();
} }
@ -153,23 +159,162 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Private Implementation // Private Implementation
String _getTitle() { void _onChangedAccountRecord(AsyncValue<proto.Account> avAccount) {
if (state.remoteUsers.length == 1) { // Update local 'User'
final remoteUser = state.remoteUsers.values.first; final account = avAccount.asData?.value;
return remoteUser.firstName ?? '<unnamed>'; if (account == null) {
} else { emit(state.copyWith(localUser: null));
return '<group chat with ${state.remoteUsers.length} users>'; return;
}
final localUser = types.User(
id: _localUserIdentityKey.toString(),
firstName: account.profile.name,
metadata: {metadataKeyIdentityPublicKey: _localUserIdentityKey});
emit(state.copyWith(localUser: localUser));
}
void _onChangedMessages(
AsyncValue<WindowState<MessageState>> avMessagesState) {
emit(_convertMessages(state, avMessagesState));
}
void _onChangedContacts(
BlocBusyState<AsyncValue<IList<DHTShortArrayElementState<proto.Contact>>>>
bavContacts) {
// Rewrite users when contacts change
singleFuture((this, _sfChangedContacts), _updateConversationSubscriptions);
}
void _onChangedConversation(
TypedKey remoteIdentityPublicKey,
AsyncValue<ActiveConversationState> avConversationState,
) {
// Update remote 'User'
final activeConversationState = avConversationState.asData?.value;
if (activeConversationState == null) {
// Don't change user information on loading state
return;
}
emit(_updateTitle(state.copyWith(
remoteUsers: state.remoteUsers.add(
remoteIdentityPublicKey,
_convertRemoteUser(
remoteIdentityPublicKey, activeConversationState)))));
}
static ChatComponentState _updateTitle(ChatComponentState currentState) {
if (currentState.remoteUsers.length == 0) {
return currentState.copyWith(title: 'Empty Chat');
}
if (currentState.remoteUsers.length == 1) {
final remoteUser = currentState.remoteUsers.values.first;
return currentState.copyWith(title: remoteUser.firstName ?? '<unnamed>');
}
return currentState.copyWith(
title: '<group chat with ${currentState.remoteUsers.length} users>');
}
types.User _convertRemoteUser(TypedKey remoteIdentityPublicKey,
ActiveConversationState activeConversationState) {
// See if we have a contact for this remote user
final contacts = _contactListCubit.state.state.asData?.value;
if (contacts != null) {
final contactIdx = contacts.indexWhere((x) =>
x.value.identityPublicKey.toVeilid() == remoteIdentityPublicKey);
if (contactIdx != -1) {
final contact = contacts[contactIdx].value;
return types.User(
id: remoteIdentityPublicKey.toString(),
firstName: contact.displayName,
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
} }
} }
types.Message? _messageStateToChatMessage(MessageState message) { return types.User(
id: remoteIdentityPublicKey.toString(),
firstName: activeConversationState.remoteConversation.profile.name,
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
}
types.User _convertUnknownUser(TypedKey remoteIdentityPublicKey) =>
types.User(
id: remoteIdentityPublicKey.toString(),
firstName: '<$remoteIdentityPublicKey>',
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
Future<void> _updateConversationSubscriptions() async {
// Get existing subscription keys and state
final existing = _conversationSubscriptions.keys.toList();
var currentRemoteUsersState = state.remoteUsers;
// Process cubit list
for (final cc in _conversationCubits) {
// Get the remote identity key
final remoteIdentityPublicKey = cc.input.remoteIdentityPublicKey;
// If the cubit is already being listened to we have nothing to do
if (existing.remove(remoteIdentityPublicKey)) {
// If the cubit is not already being listened to we should do that
_conversationSubscriptions[remoteIdentityPublicKey] = cc.stream.listen(
(avConv) =>
_onChangedConversation(remoteIdentityPublicKey, avConv));
}
final activeConversationState = cc.state.asData?.value;
if (activeConversationState != null) {
currentRemoteUsersState = currentRemoteUsersState.add(
remoteIdentityPublicKey,
_convertRemoteUser(
remoteIdentityPublicKey, activeConversationState));
}
}
// Purge remote users we didn't see in the cubit list any more
final cancels = <Future<void>>[];
for (final deadUser in existing) {
currentRemoteUsersState = currentRemoteUsersState.remove(deadUser);
cancels.add(_conversationSubscriptions.remove(deadUser)!.cancel());
}
await cancels.wait;
// Emit change to remote users state
emit(_updateTitle(state.copyWith(remoteUsers: currentRemoteUsersState)));
}
(ChatComponentState, types.Message?) _messageStateToChatMessage(
ChatComponentState currentState, MessageState message) {
final authorIdentityPublicKey = message.content.author.toVeilid(); final authorIdentityPublicKey = message.content.author.toVeilid();
final author = late final types.User author;
state.remoteUsers[authorIdentityPublicKey] ?? state.localUser; if (authorIdentityPublicKey == _localUserIdentityKey &&
currentState.localUser != null) {
author = currentState.localUser!;
} else {
final remoteUser = currentState.remoteUsers[authorIdentityPublicKey];
if (remoteUser != null) {
author = remoteUser;
} else {
final historicalRemoteUser =
currentState.historicalRemoteUsers[authorIdentityPublicKey];
if (historicalRemoteUser != null) {
author = historicalRemoteUser;
} else {
final unknownRemoteUser =
currentState.unknownUsers[authorIdentityPublicKey];
if (unknownRemoteUser != null) {
author = unknownRemoteUser;
} else {
final unknownUser = _convertUnknownUser(authorIdentityPublicKey);
currentState = currentState.copyWith(
unknownUsers: currentState.unknownUsers
.add(authorIdentityPublicKey, unknownUser));
author = unknownUser;
}
}
}
}
types.Status? status; types.Status? status;
if (message.sendState != null) { if (message.sendState != null) {
assert(author == state.localUser, assert(author.id == _localUserIdentityKey.toString(),
'send state should only be on sent messages'); 'send state should only be on sent messages');
switch (message.sendState!) { switch (message.sendState!) {
case MessageSendState.sending: case MessageSendState.sending:
@ -192,7 +337,7 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
text: contextText.text, text: contextText.text,
showStatus: status != null, showStatus: status != null,
status: status); status: status);
return textMessage; return (currentState, textMessage);
case proto.Message_Kind.secret: case proto.Message_Kind.secret:
case proto.Message_Kind.delete: case proto.Message_Kind.delete:
case proto.Message_Kind.erase: case proto.Message_Kind.erase:
@ -201,17 +346,24 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
case proto.Message_Kind.membership: case proto.Message_Kind.membership:
case proto.Message_Kind.moderation: case proto.Message_Kind.moderation:
case proto.Message_Kind.notSet: case proto.Message_Kind.notSet:
return null; return (currentState, null);
} }
} }
AsyncValue<WindowState<types.Message>> _convertMessages( ChatComponentState _convertMessages(ChatComponentState currentState,
AsyncValue<WindowState<MessageState>> avMessagesState) { AsyncValue<WindowState<MessageState>> avMessagesState) {
// Clear out unknown users
currentState = state.copyWith(unknownUsers: const IMap.empty());
final asError = avMessagesState.asError; final asError = avMessagesState.asError;
if (asError != null) { if (asError != null) {
return AsyncValue.error(asError.error, asError.stackTrace); return currentState.copyWith(
unknownUsers: const IMap.empty(),
messageWindow: AsyncValue.error(asError.error, asError.stackTrace));
} else if (avMessagesState.asLoading != null) { } else if (avMessagesState.asLoading != null) {
return const AsyncValue.loading(); return currentState.copyWith(
unknownUsers: const IMap.empty(),
messageWindow: const AsyncValue.loading());
} }
final messagesState = avMessagesState.asData!.value; final messagesState = avMessagesState.asData!.value;
@ -219,7 +371,9 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
final chatMessages = <types.Message>[]; final chatMessages = <types.Message>[];
final tsSet = <String>{}; final tsSet = <String>{};
for (final message in messagesState.window) { for (final message in messagesState.window) {
final chatMessage = _messageStateToChatMessage(message); final (newState, chatMessage) =
_messageStateToChatMessage(currentState, message);
currentState = newState;
if (chatMessage == null) { if (chatMessage == null) {
continue; continue;
} }
@ -232,12 +386,13 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
assert(false, 'should not have duplicate id'); assert(false, 'should not have duplicate id');
} }
} }
return AsyncValue.data(WindowState<types.Message>( return currentState.copyWith(
messageWindow: AsyncValue.data(WindowState<types.Message>(
window: chatMessages.toIList(), window: chatMessages.toIList(),
length: messagesState.length, length: messagesState.length,
windowTail: messagesState.windowTail, windowTail: messagesState.windowTail,
windowCount: messagesState.windowCount, windowCount: messagesState.windowCount,
follow: messagesState.follow)); follow: messagesState.follow)));
} }
void _addTextMessage( void _addTextMessage(
@ -265,7 +420,21 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
final _initWait = WaitSet<void>(); final _initWait = WaitSet<void>();
final AccountInfo _accountInfo;
final AccountRecordCubit _accountRecordCubit;
final ContactListCubit _contactListCubit;
final List<ActiveConversationCubit> _conversationCubits;
final SingleContactMessagesCubit _messagesCubit; final SingleContactMessagesCubit _messagesCubit;
late final TypedKey _localUserIdentityKey;
late final StreamSubscription<AsyncValue<proto.Account>>
_accountRecordSubscription;
final Map<TypedKey, StreamSubscription<AsyncValue<ActiveConversationState>>>
_conversationSubscriptions = {};
late StreamSubscription<SingleContactMessagesState> _messagesSubscription; late StreamSubscription<SingleContactMessagesState> _messagesSubscription;
late StreamSubscription<
BlocBusyState<
AsyncValue<IList<DHTShortArrayElementState<proto.Contact>>>>>
_contactListSubscription;
double scrollOffset = 0; double scrollOffset = 0;
} }

View file

@ -3,6 +3,7 @@ import 'package:veilid_support/veilid_support.dart';
import '../../../proto/proto.dart' as proto; import '../../../proto/proto.dart' as proto;
import '../../../tools/tools.dart';
import 'author_input_source.dart'; import 'author_input_source.dart';
import 'message_integrity.dart'; import 'message_integrity.dart';
import 'output_position.dart'; import 'output_position.dart';
@ -84,6 +85,8 @@ class AuthorInputQueue {
if (_lastMessage != null) { if (_lastMessage != null) {
// Ensure the timestamp is not moving backward // Ensure the timestamp is not moving backward
if (nextMessage.value.timestamp < _lastMessage!.timestamp) { if (nextMessage.value.timestamp < _lastMessage!.timestamp) {
log.warning('timestamp backward: ${nextMessage.value.timestamp}'
' < ${_lastMessage!.timestamp}');
continue; continue;
} }
} }
@ -91,11 +94,14 @@ class AuthorInputQueue {
// Verify the id chain for the message // Verify the id chain for the message
final matchId = await _messageIntegrity.generateMessageId(_lastMessage); final matchId = await _messageIntegrity.generateMessageId(_lastMessage);
if (matchId.compare(nextMessage.value.idBytes) != 0) { if (matchId.compare(nextMessage.value.idBytes) != 0) {
log.warning(
'id chain invalid: $matchId != ${nextMessage.value.idBytes}');
continue; continue;
} }
// Verify the signature for the message // Verify the signature for the message
if (!await _messageIntegrity.verifyMessage(nextMessage.value)) { if (!await _messageIntegrity.verifyMessage(nextMessage.value)) {
log.warning('invalid message signature: ${nextMessage.value}');
continue; continue;
} }

View file

@ -50,13 +50,13 @@ typedef SingleContactMessagesState = AsyncValue<WindowState<MessageState>>;
// Builds the reconciled chat record from the local and remote conversation keys // Builds the reconciled chat record from the local and remote conversation keys
class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> { class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
SingleContactMessagesCubit({ SingleContactMessagesCubit({
required ActiveAccountInfo activeAccountInfo, required AccountInfo accountInfo,
required TypedKey remoteIdentityPublicKey, required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationRecordKey, required TypedKey localConversationRecordKey,
required TypedKey localMessagesRecordKey, required TypedKey localMessagesRecordKey,
required TypedKey remoteConversationRecordKey, required TypedKey remoteConversationRecordKey,
required TypedKey remoteMessagesRecordKey, required TypedKey remoteMessagesRecordKey,
}) : _activeAccountInfo = activeAccountInfo, }) : _accountInfo = accountInfo,
_remoteIdentityPublicKey = remoteIdentityPublicKey, _remoteIdentityPublicKey = remoteIdentityPublicKey,
_localConversationRecordKey = localConversationRecordKey, _localConversationRecordKey = localConversationRecordKey,
_localMessagesRecordKey = localMessagesRecordKey, _localMessagesRecordKey = localMessagesRecordKey,
@ -81,6 +81,16 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
await _sentMessagesCubit?.close(); await _sentMessagesCubit?.close();
await _rcvdMessagesCubit?.close(); await _rcvdMessagesCubit?.close();
await _reconciledMessagesCubit?.close(); await _reconciledMessagesCubit?.close();
// If the local conversation record is gone, then delete the reconciled
// messages table as well
final conversationDead = await DHTRecordPool.instance
.isDeletedRecordKey(_localConversationRecordKey);
if (conversationDead) {
await SingleContactMessagesCubit.cleanupAndDeleteMessages(
localConversationRecordKey: _localConversationRecordKey);
}
await super.close(); await super.close();
} }
@ -111,15 +121,15 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Make crypto // Make crypto
Future<void> _initCrypto() async { Future<void> _initCrypto() async {
_conversationCrypto = await _activeAccountInfo _conversationCrypto =
.makeConversationCrypto(_remoteIdentityPublicKey); await _accountInfo.makeConversationCrypto(_remoteIdentityPublicKey);
_senderMessageIntegrity = await MessageIntegrity.create( _senderMessageIntegrity = await MessageIntegrity.create(
author: _activeAccountInfo.identityTypedPublicKey); author: _accountInfo.identityTypedPublicKey);
} }
// Open local messages key // Open local messages key
Future<void> _initSentMessagesCubit() async { Future<void> _initSentMessagesCubit() async {
final writer = _activeAccountInfo.identityWriter; final writer = _accountInfo.identityWriter;
_sentMessagesCubit = DHTLogCubit( _sentMessagesCubit = DHTLogCubit(
open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer, open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer,
@ -149,7 +159,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
Future<VeilidCrypto> _makeLocalMessagesCrypto() async => Future<VeilidCrypto> _makeLocalMessagesCrypto() async =>
VeilidCryptoPrivate.fromTypedKey( VeilidCryptoPrivate.fromTypedKey(
_activeAccountInfo.userLogin.identitySecret, 'tabledb'); _accountInfo.userLogin!.identitySecret, 'tabledb');
// Open reconciled chat record key // Open reconciled chat record key
Future<void> _initReconciledMessagesCubit() async { Future<void> _initReconciledMessagesCubit() async {
@ -240,8 +250,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
return; return;
} }
_reconciliation.reconcileMessages(_activeAccountInfo.identityTypedPublicKey, _reconciliation.reconcileMessages(
sentMessages, _sentMessagesCubit!); _accountInfo.identityTypedPublicKey, sentMessages, _sentMessagesCubit!);
// Update the view // Update the view
_renderState(); _renderState();
@ -278,7 +288,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Now sign it // Now sign it
await _senderMessageIntegrity.signMessage( await _senderMessageIntegrity.signMessage(
message, _activeAccountInfo.identitySecretKey); message, _accountInfo.identitySecretKey);
} }
// Async process to send messages in the background // Async process to send messages in the background
@ -291,8 +301,14 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
previousMessage = message; previousMessage = message;
} }
// _sendingMessages = messages;
// _renderState();
await _sentMessagesCubit!.operateAppendEventual((writer) => await _sentMessagesCubit!.operateAppendEventual((writer) =>
writer.addAll(messages.map((m) => m.writeToBuffer()).toList())); writer.addAll(messages.map((m) => m.writeToBuffer()).toList()));
// _sendingMessages = const IList.empty();
} }
// Produce a state for this cubit from the input cubits and queues // Produce a state for this cubit from the input cubits and queues
@ -315,7 +331,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// final reconciledMessagesMap = // final reconciledMessagesMap =
// IMap<String, proto.ReconciledMessage>.fromValues( // IMap<String, proto.ReconciledMessage>.fromValues(
// keyMapper: (x) => x.content.authorUniqueIdString, // keyMapper: (x) => x.content.authorUniqueIdString,
// values: reconciledMessages.elements, // values: reconciledMessages.windowElements,
// ); // );
final sentMessagesMap = final sentMessagesMap =
IMap<String, OnlineElementState<proto.Message>>.fromValues( IMap<String, OnlineElementState<proto.Message>>.fromValues(
@ -328,10 +344,10 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// ); // );
final renderedElements = <RenderStateElement>[]; final renderedElements = <RenderStateElement>[];
final renderedIds = <String>{};
for (final m in reconciledMessages.windowElements) { for (final m in reconciledMessages.windowElements) {
final isLocal = m.content.author.toVeilid() == final isLocal =
_activeAccountInfo.identityTypedPublicKey; m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey;
final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime); final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime);
final sm = final sm =
isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null; isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null;
@ -345,8 +361,23 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
sent: sent, sent: sent,
sentOffline: sentOffline, sentOffline: sentOffline,
)); ));
renderedIds.add(m.content.authorUniqueIdString);
} }
// Render in-flight messages at the bottom
// for (final m in _sendingMessages) {
// if (renderedIds.contains(m.authorUniqueIdString)) {
// continue;
// }
// renderedElements.add(RenderStateElement(
// message: m,
// isLocal: true,
// sent: true,
// sentOffline: true,
// ));
// }
// Render the state // Render the state
final messages = renderedElements final messages = renderedElements
.map((x) => MessageState( .map((x) => MessageState(
@ -369,7 +400,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Add common fields // Add common fields
// id and signature will get set by _processMessageToSend // id and signature will get set by _processMessageToSend
message message
..author = _activeAccountInfo.identityTypedPublicKey.toProto() ..author = _accountInfo.identityTypedPublicKey.toProto()
..timestamp = Veilid.instance.now().toInt64(); ..timestamp = Veilid.instance.now().toInt64();
// Put in the queue // Put in the queue
@ -402,7 +433,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
///////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////
final WaitSet<void> _initWait = WaitSet(); final WaitSet<void> _initWait = WaitSet();
final ActiveAccountInfo _activeAccountInfo; late final AccountInfo _accountInfo;
final TypedKey _remoteIdentityPublicKey; final TypedKey _remoteIdentityPublicKey;
final TypedKey _localConversationRecordKey; final TypedKey _localConversationRecordKey;
final TypedKey _localMessagesRecordKey; final TypedKey _localMessagesRecordKey;
@ -419,7 +450,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
late final MessageReconciliation _reconciliation; late final MessageReconciliation _reconciliation;
late final PersistentQueue<proto.Message> _unsentMessagesQueue; late final PersistentQueue<proto.Message> _unsentMessagesQueue;
// IList<proto.Message> _sendingMessages = const IList.empty();
StreamSubscription<DHTLogBusyState<proto.Message>>? _sentSubscription; StreamSubscription<DHTLogBusyState<proto.Message>>? _sentSubscription;
StreamSubscription<DHTLogBusyState<proto.Message>>? _rcvdSubscription; StreamSubscription<DHTLogBusyState<proto.Message>>? _rcvdSubscription;
StreamSubscription<TableDBArrayProtobufBusyState<proto.ReconciledMessage>>? StreamSubscription<TableDBArrayProtobufBusyState<proto.ReconciledMessage>>?

View file

@ -20,9 +20,13 @@ class ChatComponentState with _$ChatComponentState {
// ScrollController for the chat // ScrollController for the chat
required AutoScrollController scrollController, required AutoScrollController scrollController,
// Local user // Local user
required User localUser, required User? localUser,
// Remote users // Active remote users
required IMap<TypedKey, User> remoteUsers, required IMap<TypedKey, User> remoteUsers,
// Historical remote users
required IMap<TypedKey, User> historicalRemoteUsers,
// Unknown users
required IMap<TypedKey, User> unknownUsers,
// Messages state // Messages state
required AsyncValue<WindowState<Message>> messageWindow, required AsyncValue<WindowState<Message>> messageWindow,
// Title of the chat // Title of the chat

View file

@ -21,8 +21,13 @@ mixin _$ChatComponentState {
throw _privateConstructorUsedError; // ScrollController for the chat throw _privateConstructorUsedError; // ScrollController for the chat
AutoScrollController get scrollController => AutoScrollController get scrollController =>
throw _privateConstructorUsedError; // Local user throw _privateConstructorUsedError; // Local user
User get localUser => throw _privateConstructorUsedError; // Remote users User? get localUser =>
throw _privateConstructorUsedError; // Active remote users
IMap<Typed<FixedEncodedString43>, User> get remoteUsers => IMap<Typed<FixedEncodedString43>, User> get remoteUsers =>
throw _privateConstructorUsedError; // Historical remote users
IMap<Typed<FixedEncodedString43>, User> get historicalRemoteUsers =>
throw _privateConstructorUsedError; // Unknown users
IMap<Typed<FixedEncodedString43>, User> get unknownUsers =>
throw _privateConstructorUsedError; // Messages state throw _privateConstructorUsedError; // Messages state
AsyncValue<WindowState<Message>> get messageWindow => AsyncValue<WindowState<Message>> get messageWindow =>
throw _privateConstructorUsedError; // Title of the chat throw _privateConstructorUsedError; // Title of the chat
@ -42,8 +47,10 @@ abstract class $ChatComponentStateCopyWith<$Res> {
$Res call( $Res call(
{GlobalKey<ChatState> chatKey, {GlobalKey<ChatState> chatKey,
AutoScrollController scrollController, AutoScrollController scrollController,
User localUser, User? localUser,
IMap<Typed<FixedEncodedString43>, User> remoteUsers, IMap<Typed<FixedEncodedString43>, User> remoteUsers,
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
IMap<Typed<FixedEncodedString43>, User> unknownUsers,
AsyncValue<WindowState<Message>> messageWindow, AsyncValue<WindowState<Message>> messageWindow,
String title}); String title});
@ -65,8 +72,10 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState>
$Res call({ $Res call({
Object? chatKey = null, Object? chatKey = null,
Object? scrollController = null, Object? scrollController = null,
Object? localUser = null, Object? localUser = freezed,
Object? remoteUsers = null, Object? remoteUsers = null,
Object? historicalRemoteUsers = null,
Object? unknownUsers = null,
Object? messageWindow = null, Object? messageWindow = null,
Object? title = null, Object? title = null,
}) { }) {
@ -79,14 +88,22 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState>
? _value.scrollController ? _value.scrollController
: scrollController // ignore: cast_nullable_to_non_nullable : scrollController // ignore: cast_nullable_to_non_nullable
as AutoScrollController, as AutoScrollController,
localUser: null == localUser localUser: freezed == localUser
? _value.localUser ? _value.localUser
: localUser // ignore: cast_nullable_to_non_nullable : localUser // ignore: cast_nullable_to_non_nullable
as User, as User?,
remoteUsers: null == remoteUsers remoteUsers: null == remoteUsers
? _value.remoteUsers ? _value.remoteUsers
: remoteUsers // ignore: cast_nullable_to_non_nullable : remoteUsers // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, User>, as IMap<Typed<FixedEncodedString43>, User>,
historicalRemoteUsers: null == historicalRemoteUsers
? _value.historicalRemoteUsers
: historicalRemoteUsers // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, User>,
unknownUsers: null == unknownUsers
? _value.unknownUsers
: unknownUsers // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, User>,
messageWindow: null == messageWindow messageWindow: null == messageWindow
? _value.messageWindow ? _value.messageWindow
: messageWindow // ignore: cast_nullable_to_non_nullable : messageWindow // ignore: cast_nullable_to_non_nullable
@ -119,8 +136,10 @@ abstract class _$$ChatComponentStateImplCopyWith<$Res>
$Res call( $Res call(
{GlobalKey<ChatState> chatKey, {GlobalKey<ChatState> chatKey,
AutoScrollController scrollController, AutoScrollController scrollController,
User localUser, User? localUser,
IMap<Typed<FixedEncodedString43>, User> remoteUsers, IMap<Typed<FixedEncodedString43>, User> remoteUsers,
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
IMap<Typed<FixedEncodedString43>, User> unknownUsers,
AsyncValue<WindowState<Message>> messageWindow, AsyncValue<WindowState<Message>> messageWindow,
String title}); String title});
@ -141,8 +160,10 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res>
$Res call({ $Res call({
Object? chatKey = null, Object? chatKey = null,
Object? scrollController = null, Object? scrollController = null,
Object? localUser = null, Object? localUser = freezed,
Object? remoteUsers = null, Object? remoteUsers = null,
Object? historicalRemoteUsers = null,
Object? unknownUsers = null,
Object? messageWindow = null, Object? messageWindow = null,
Object? title = null, Object? title = null,
}) { }) {
@ -155,14 +176,22 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res>
? _value.scrollController ? _value.scrollController
: scrollController // ignore: cast_nullable_to_non_nullable : scrollController // ignore: cast_nullable_to_non_nullable
as AutoScrollController, as AutoScrollController,
localUser: null == localUser localUser: freezed == localUser
? _value.localUser ? _value.localUser
: localUser // ignore: cast_nullable_to_non_nullable : localUser // ignore: cast_nullable_to_non_nullable
as User, as User?,
remoteUsers: null == remoteUsers remoteUsers: null == remoteUsers
? _value.remoteUsers ? _value.remoteUsers
: remoteUsers // ignore: cast_nullable_to_non_nullable : remoteUsers // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, User>, as IMap<Typed<FixedEncodedString43>, User>,
historicalRemoteUsers: null == historicalRemoteUsers
? _value.historicalRemoteUsers
: historicalRemoteUsers // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, User>,
unknownUsers: null == unknownUsers
? _value.unknownUsers
: unknownUsers // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, User>,
messageWindow: null == messageWindow messageWindow: null == messageWindow
? _value.messageWindow ? _value.messageWindow
: messageWindow // ignore: cast_nullable_to_non_nullable : messageWindow // ignore: cast_nullable_to_non_nullable
@ -183,6 +212,8 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
required this.scrollController, required this.scrollController,
required this.localUser, required this.localUser,
required this.remoteUsers, required this.remoteUsers,
required this.historicalRemoteUsers,
required this.unknownUsers,
required this.messageWindow, required this.messageWindow,
required this.title}); required this.title});
@ -194,10 +225,16 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
final AutoScrollController scrollController; final AutoScrollController scrollController;
// Local user // Local user
@override @override
final User localUser; final User? localUser;
// Remote users // Active remote users
@override @override
final IMap<Typed<FixedEncodedString43>, User> remoteUsers; final IMap<Typed<FixedEncodedString43>, User> remoteUsers;
// Historical remote users
@override
final IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers;
// Unknown users
@override
final IMap<Typed<FixedEncodedString43>, User> unknownUsers;
// Messages state // Messages state
@override @override
final AsyncValue<WindowState<Message>> messageWindow; final AsyncValue<WindowState<Message>> messageWindow;
@ -207,7 +244,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
@override @override
String toString() { String toString() {
return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, localUser: $localUser, remoteUsers: $remoteUsers, messageWindow: $messageWindow, title: $title)'; return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)';
} }
@override @override
@ -222,14 +259,26 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
other.localUser == localUser) && other.localUser == localUser) &&
(identical(other.remoteUsers, remoteUsers) || (identical(other.remoteUsers, remoteUsers) ||
other.remoteUsers == remoteUsers) && other.remoteUsers == remoteUsers) &&
(identical(other.historicalRemoteUsers, historicalRemoteUsers) ||
other.historicalRemoteUsers == historicalRemoteUsers) &&
(identical(other.unknownUsers, unknownUsers) ||
other.unknownUsers == unknownUsers) &&
(identical(other.messageWindow, messageWindow) || (identical(other.messageWindow, messageWindow) ||
other.messageWindow == messageWindow) && other.messageWindow == messageWindow) &&
(identical(other.title, title) || other.title == title)); (identical(other.title, title) || other.title == title));
} }
@override @override
int get hashCode => Object.hash(runtimeType, chatKey, scrollController, int get hashCode => Object.hash(
localUser, remoteUsers, messageWindow, title); runtimeType,
chatKey,
scrollController,
localUser,
remoteUsers,
historicalRemoteUsers,
unknownUsers,
messageWindow,
title);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@ -243,8 +292,11 @@ abstract class _ChatComponentState implements ChatComponentState {
const factory _ChatComponentState( const factory _ChatComponentState(
{required final GlobalKey<ChatState> chatKey, {required final GlobalKey<ChatState> chatKey,
required final AutoScrollController scrollController, required final AutoScrollController scrollController,
required final User localUser, required final User? localUser,
required final IMap<Typed<FixedEncodedString43>, User> remoteUsers, required final IMap<Typed<FixedEncodedString43>, User> remoteUsers,
required final IMap<Typed<FixedEncodedString43>, User>
historicalRemoteUsers,
required final IMap<Typed<FixedEncodedString43>, User> unknownUsers,
required final AsyncValue<WindowState<Message>> messageWindow, required final AsyncValue<WindowState<Message>> messageWindow,
required final String title}) = _$ChatComponentStateImpl; required final String title}) = _$ChatComponentStateImpl;
@ -253,9 +305,13 @@ abstract class _ChatComponentState implements ChatComponentState {
@override // ScrollController for the chat @override // ScrollController for the chat
AutoScrollController get scrollController; AutoScrollController get scrollController;
@override // Local user @override // Local user
User get localUser; User? get localUser;
@override // Remote users @override // Active remote users
IMap<Typed<FixedEncodedString43>, User> get remoteUsers; IMap<Typed<FixedEncodedString43>, User> get remoteUsers;
@override // Historical remote users
IMap<Typed<FixedEncodedString43>, User> get historicalRemoteUsers;
@override // Unknown users
IMap<Typed<FixedEncodedString43>, User> get unknownUsers;
@override // Messages state @override // Messages state
AsyncValue<WindowState<Message>> get messageWindow; AsyncValue<WindowState<Message>> get messageWindow;
@override // Title of the chat @override // Title of the chat

View file

@ -9,7 +9,8 @@ import 'package:flutter_chat_ui/flutter_chat_ui.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 '../../chat_list/chat_list.dart'; import '../../contacts/contacts.dart';
import '../../conversation/conversation.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../chat.dart'; import '../chat.dart';
@ -22,31 +23,29 @@ class ChatComponentWidget extends StatelessWidget {
static Widget builder( static Widget builder(
{required TypedKey localConversationRecordKey, Key? key}) => {required TypedKey localConversationRecordKey, Key? key}) =>
Builder(builder: (context) { Builder(builder: (context) {
// Get all watched dependendies // Get the account info
final activeAccountInfo = context.watch<ActiveAccountInfo>(); final accountInfo = context.watch<AccountInfoCubit>().state;
final accountRecordInfo =
context.watch<AccountRecordCubit>().state.asData?.value;
if (accountRecordInfo == null) {
return debugPage('should always have an account record here');
}
final avconversation = context.select<ActiveConversationsBlocMapCubit, // Get the account record cubit
AsyncValue<ActiveConversationState>?>( final accountRecordCubit = context.read<AccountRecordCubit>();
(x) => x.state[localConversationRecordKey]);
if (avconversation == null) { // Get the contact list cubit
final contactListCubit = context.watch<ContactListCubit>();
// Get the active conversation cubit
final activeConversationCubit = context
.select<ActiveConversationsBlocMapCubit, ActiveConversationCubit?>(
(x) => x.tryOperateSync(localConversationRecordKey,
closure: (cubit) => cubit));
if (activeConversationCubit == null) {
return waitingPage(); return waitingPage();
} }
final activeConversationState = avconversation.asData?.value;
if (activeConversationState == null) {
return avconversation.buildNotData();
}
// Get the messages cubit // Get the messages cubit
final messagesCubit = context.select< final messagesCubit = context.select<
ActiveSingleContactChatBlocMapCubit, ActiveSingleContactChatBlocMapCubit,
SingleContactMessagesCubit?>( SingleContactMessagesCubit?>(
(x) => x.tryOperate(localConversationRecordKey, (x) => x.tryOperateSync(localConversationRecordKey,
closure: (cubit) => cubit)); closure: (cubit) => cubit));
if (messagesCubit == null) { if (messagesCubit == null) {
return waitingPage(); return waitingPage();
@ -54,10 +53,12 @@ class ChatComponentWidget extends StatelessWidget {
// Make chat component state // Make chat component state
return BlocProvider( return BlocProvider(
key: key,
create: (context) => ChatComponentCubit.singleContact( create: (context) => ChatComponentCubit.singleContact(
activeAccountInfo: activeAccountInfo, accountInfo: accountInfo,
accountRecordInfo: accountRecordInfo, accountRecordCubit: accountRecordCubit,
activeConversationState: activeConversationState, contactListCubit: contactListCubit,
activeConversationCubit: activeConversationCubit,
messagesCubit: messagesCubit, messagesCubit: messagesCubit,
), ),
child: ChatComponentWidget._(key: key)); child: ChatComponentWidget._(key: key));
@ -159,6 +160,11 @@ class ChatComponentWidget extends StatelessWidget {
final chatComponentCubit = context.watch<ChatComponentCubit>(); final chatComponentCubit = context.watch<ChatComponentCubit>();
final chatComponentState = chatComponentCubit.state; final chatComponentState = chatComponentCubit.state;
final localUser = chatComponentState.localUser;
if (localUser == null) {
return waitingPage();
}
final messageWindow = chatComponentState.messageWindow.asData?.value; final messageWindow = chatComponentState.messageWindow.asData?.value;
if (messageWindow == null) { if (messageWindow == null) {
return chatComponentState.messageWindow.buildNotData(); return chatComponentState.messageWindow.buildNotData();
@ -281,7 +287,7 @@ class ChatComponentWidget extends StatelessWidget {
_handleSendPressed(chatComponentCubit, pt), _handleSendPressed(chatComponentCubit, pt),
//showUserAvatars: false, //showUserAvatars: false,
//showUserNames: true, //showUserNames: true,
user: chatComponentState.localUser, user: localUser,
emptyState: const EmptyChatWidget())), emptyState: const EmptyChatWidget())),
), ),
), ),

View file

@ -1,102 +0,0 @@
import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_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 'cubits.dart';
@immutable
class ActiveConversationState extends Equatable {
const ActiveConversationState({
required this.contact,
required this.localConversation,
required this.remoteConversation,
});
final proto.Contact contact;
final proto.Conversation localConversation;
final proto.Conversation remoteConversation;
@override
List<Object?> get props => [contact, localConversation, remoteConversation];
}
typedef ActiveConversationCubit = TransformerCubit<
AsyncValue<ActiveConversationState>, AsyncValue<ConversationState>>;
typedef ActiveConversationsBlocMapState
= BlocMapState<TypedKey, AsyncValue<ActiveConversationState>>;
// Map of localConversationRecordKey to ActiveConversationCubit
// Wraps a conversation cubit to only expose completely built conversations
// Automatically follows the state of a ChatListCubit.
// Even though 'conversations' are per-contact and not per-chat
// We currently only build the cubits for the chats that are active, not
// archived chats or contacts that are not actively in a chat.
class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<ActiveConversationState>, ActiveConversationCubit>
with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> {
ActiveConversationsBlocMapCubit(
{required ActiveAccountInfo activeAccountInfo,
required ContactListCubit contactListCubit})
: _activeAccountInfo = activeAccountInfo,
_contactListCubit = contactListCubit;
// Add an active conversation to be tracked for changes
Future<void> _addConversation({required proto.Contact contact}) async =>
add(() => MapEntry(
contact.localConversationRecordKey.toVeilid(),
TransformerCubit(
ConversationCubit(
activeAccountInfo: _activeAccountInfo,
remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(),
localConversationRecordKey:
contact.localConversationRecordKey.toVeilid(),
remoteConversationRecordKey:
contact.remoteConversationRecordKey.toVeilid(),
),
// Transformer that only passes through completed conversations
// along with the contact that corresponds to the completed
// conversation
transform: (avstate) => avstate.when(
data: (data) => (data.localConversation == null ||
data.remoteConversation == null)
? const AsyncValue.loading()
: AsyncValue.data(ActiveConversationState(
contact: contact,
localConversation: data.localConversation!,
remoteConversation: data.remoteConversation!)),
loading: AsyncValue.loading,
error: AsyncValue.error))));
/// StateFollower /////////////////////////
@override
Future<void> removeFromState(TypedKey key) => remove(key);
@override
Future<void> updateState(TypedKey key, proto.Chat value) async {
final contactList = _contactListCubit.state.state.asData?.value;
if (contactList == null) {
await addState(key, const AsyncValue.loading());
return;
}
final contactIndex = contactList.indexWhere(
(c) => c.value.localConversationRecordKey.toVeilid() == key);
if (contactIndex == -1) {
await addState(key, AsyncValue.error('Contact not found'));
return;
}
final contact = contactList[contactIndex];
await _addConversation(contact: contact.value);
}
////
final ActiveAccountInfo _activeAccountInfo;
final ContactListCubit _contactListCubit;
}

View file

@ -1,101 +0,0 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../chat/chat.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import 'active_conversations_bloc_map_cubit.dart';
import 'chat_list_cubit.dart';
// Map of localConversationRecordKey to MessagesCubit
// Wraps a MessagesCubit to stream the latest messages to the state
// Automatically follows the state of a ActiveConversationsBlocMapCubit.
class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
SingleContactMessagesState, SingleContactMessagesCubit>
with
StateMapFollower<ActiveConversationsBlocMapState, TypedKey,
AsyncValue<ActiveConversationState>> {
ActiveSingleContactChatBlocMapCubit(
{required ActiveAccountInfo activeAccountInfo,
required ContactListCubit contactListCubit,
required ChatListCubit chatListCubit})
: _activeAccountInfo = activeAccountInfo,
_contactListCubit = contactListCubit,
_chatListCubit = chatListCubit;
Future<void> _addConversationMessages(
{required proto.Contact contact,
required proto.Chat chat,
required proto.Conversation localConversation,
required proto.Conversation remoteConversation}) async =>
add(() => MapEntry(
contact.localConversationRecordKey.toVeilid(),
SingleContactMessagesCubit(
activeAccountInfo: _activeAccountInfo,
remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(),
localConversationRecordKey:
contact.localConversationRecordKey.toVeilid(),
remoteConversationRecordKey:
contact.remoteConversationRecordKey.toVeilid(),
localMessagesRecordKey: localConversation.messages.toVeilid(),
remoteMessagesRecordKey: remoteConversation.messages.toVeilid(),
)));
/// StateFollower /////////////////////////
@override
Future<void> removeFromState(TypedKey key) => remove(key);
@override
Future<void> updateState(
TypedKey key, AsyncValue<ActiveConversationState> value) async {
// Get the contact object for this single contact chat
final contactList = _contactListCubit.state.state.asData?.value;
if (contactList == null) {
await addState(key, const AsyncValue.loading());
return;
}
final contactIndex = contactList.indexWhere(
(c) => c.value.localConversationRecordKey.toVeilid() == key);
if (contactIndex == -1) {
await addState(
key, AsyncValue.error('Contact not found for conversation'));
return;
}
final contact = contactList[contactIndex].value;
// Get the chat object for this single contact chat
final chatList = _chatListCubit.state.state.asData?.value;
if (chatList == null) {
await addState(key, const AsyncValue.loading());
return;
}
final chatIndex = chatList.indexWhere(
(c) => c.value.localConversationRecordKey.toVeilid() == key);
if (contactIndex == -1) {
await addState(key, AsyncValue.error('Chat not found for conversation'));
return;
}
final chat = chatList[chatIndex].value;
await value.when(
data: (state) => _addConversationMessages(
contact: contact,
chat: chat,
localConversation: state.localConversation,
remoteConversation: state.remoteConversation),
loading: () => addState(key, const AsyncValue.loading()),
error: (error, stackTrace) =>
addState(key, AsyncValue.error(error, stackTrace)));
}
////
final ActiveAccountInfo _activeAccountInfo;
final ContactListCubit _contactListCubit;
final ChatListCubit _chatListCubit;
}

View file

@ -8,7 +8,6 @@ import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; 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';
////////////////////////////////////////////////// //////////////////////////////////////////////////
@ -19,33 +18,30 @@ typedef ChatListCubitState = DHTShortArrayBusyState<proto.Chat>;
class ChatListCubit extends DHTShortArrayCubit<proto.Chat> class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
with StateMapFollowable<ChatListCubitState, TypedKey, proto.Chat> { with StateMapFollowable<ChatListCubitState, TypedKey, proto.Chat> {
ChatListCubit({ ChatListCubit({
required ActiveAccountInfo activeAccountInfo, required AccountInfo accountInfo,
required proto.Account account, required OwnedDHTRecordPointer chatListRecordPointer,
required this.activeChatCubit, required ActiveChatCubit activeChatCubit,
}) : super( }) : _activeChatCubit = activeChatCubit,
open: () => _open(activeAccountInfo, account), super(
open: () => _open(accountInfo, chatListRecordPointer),
decodeElement: proto.Chat.fromBuffer); decodeElement: proto.Chat.fromBuffer);
static Future<DHTShortArray> _open( static Future<DHTShortArray> _open(AccountInfo accountInfo,
ActiveAccountInfo activeAccountInfo, proto.Account account) async { OwnedDHTRecordPointer chatListRecordPointer) async {
final accountRecordKey = final dhtRecord = await DHTShortArray.openOwned(chatListRecordPointer,
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; debugName: 'ChatListCubit::_open::ChatList',
parent: accountInfo.accountRecordKey);
final chatListRecordKey = account.chatList.toVeilid();
final dhtRecord = await DHTShortArray.openOwned(chatListRecordKey,
debugName: 'ChatListCubit::_open::ChatList', parent: accountRecordKey);
return dhtRecord; return dhtRecord;
} }
Future<proto.ChatSettings> getDefaultChatSettings( Future<proto.ChatSettings> getDefaultChatSettings(
proto.Contact contact) async { proto.Contact contact) async {
final pronouns = contact.editedProfile.pronouns.isEmpty final pronouns = contact.profile.pronouns.isEmpty
? '' ? ''
: ' (${contact.editedProfile.pronouns})'; : ' [${contact.profile.pronouns}])';
return proto.ChatSettings() return proto.ChatSettings()
..title = '${contact.editedProfile.name}$pronouns' ..title = '${contact.displayName}$pronouns'
..description = '' ..description = ''
..defaultExpiration = Int64.ZERO; ..defaultExpiration = Int64.ZERO;
} }
@ -57,12 +53,24 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
// Make local copy so we don't share the buffer // Make local copy so we don't share the buffer
final localConversationRecordKey = final localConversationRecordKey =
contact.localConversationRecordKey.toVeilid(); contact.localConversationRecordKey.toVeilid();
final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
final remoteConversationRecordKey = final remoteConversationRecordKey =
contact.remoteConversationRecordKey.toVeilid(); contact.remoteConversationRecordKey.toVeilid();
// Create 1:1 conversation type Chat
final chatMember = proto.ChatMember()
..remoteIdentityPublicKey = remoteIdentityPublicKey.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto();
final directChat = proto.DirectChat()
..settings = await getDefaultChatSettings(contact)
..localConversationRecordKey = localConversationRecordKey.toProto()
..remoteMember = chatMember;
final chat = proto.Chat()..direct = directChat;
// Add Chat to account's list // Add Chat to account's list
// if this fails, don't keep retrying, user can try again later await operateWriteEventual((writer) async {
await operateWrite((writer) async {
// See if we have added this chat already // See if we have added this chat already
for (var i = 0; i < writer.length; i++) { for (var i = 0; i < writer.length; i++) {
final cbuf = await writer.get(i); final cbuf = await writer.get(i);
@ -70,18 +78,26 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
throw Exception('Failed to get chat'); throw Exception('Failed to get chat');
} }
final c = proto.Chat.fromBuffer(cbuf); final c = proto.Chat.fromBuffer(cbuf);
if (c.localConversationRecordKey ==
switch (c.whichKind()) {
case proto.Chat_Kind.direct:
if (c.direct.localConversationRecordKey ==
contact.localConversationRecordKey) { contact.localConversationRecordKey) {
// Nothing to do here // Nothing to do here
return; return;
} }
break;
case proto.Chat_Kind.group:
if (c.group.localConversationRecordKey ==
contact.localConversationRecordKey) {
throw StateError('direct conversation record key should'
' not be used for group chats!');
}
break;
case proto.Chat_Kind.notSet:
throw StateError('unknown chat kind');
}
} }
// Create 1:1 conversation type Chat
final chat = proto.Chat()
..settings = await getDefaultChatSettings(contact)
..localConversationRecordKey = localConversationRecordKey.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto();
// Add chat // Add chat
await writer.add(chat.writeToBuffer()); await writer.add(chat.writeToBuffer());
@ -91,41 +107,23 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
/// Delete a chat /// Delete a chat
Future<void> deleteChat( Future<void> deleteChat(
{required TypedKey localConversationRecordKey}) async { {required TypedKey localConversationRecordKey}) async {
final localConversationRecordKeyProto =
localConversationRecordKey.toProto();
// Remove Chat from account's list // Remove Chat from account's list
// if this fails, don't keep retrying, user can try again later await operateWriteEventual((writer) async {
final deletedItem = if (_activeChatCubit.state == localConversationRecordKey) {
// Ensure followers get their changes before we return _activeChatCubit.setActiveChat(null);
await syncFollowers(() => operateWrite((writer) async {
if (activeChatCubit.state == localConversationRecordKey) {
activeChatCubit.setActiveChat(null);
} }
for (var i = 0; i < writer.length; i++) { for (var i = 0; i < writer.length; i++) {
final c = await writer.getProtobuf(proto.Chat.fromBuffer, i); final c = await writer.getProtobuf(proto.Chat.fromBuffer, i);
if (c == null) { if (c == null) {
throw Exception('Failed to get chat'); throw Exception('Failed to get chat');
} }
if (c.localConversationRecordKey ==
localConversationRecordKeyProto) { if (c.localConversationRecordKey == localConversationRecordKey) {
// Found the right chat
await writer.remove(i); await writer.remove(i);
return c; return;
}
}
return null;
}));
// Since followers are synced, we can safetly remove the reconciled
// chat record now
if (deletedItem != null) {
try {
await SingleContactMessagesCubit.cleanupAndDeleteMessages(
localConversationRecordKey: localConversationRecordKey);
} on Exception catch (e) {
log.debug('error removing reconciled chat table: $e', e);
} }
} }
});
} }
/// StateMapFollowable ///////////////////////// /// StateMapFollowable /////////////////////////
@ -136,9 +134,11 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
return IMap(); return IMap();
} }
return IMap.fromIterable(stateValue, return IMap.fromIterable(stateValue,
keyMapper: (e) => e.value.localConversationRecordKey.toVeilid(), keyMapper: (e) => e.value.localConversationRecordKey,
valueMapper: (e) => e.value); valueMapper: (e) => e.value);
} }
final ActiveChatCubit activeChatCubit; ////////////////////////////////////////////////////////////////////////////
final ActiveChatCubit _activeChatCubit;
} }

View file

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

View file

@ -0,0 +1,96 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:searchable_listview/searchable_listview.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import '../../proto/proto.dart';
import '../../theme/theme.dart';
import '../chat_list.dart';
class ChatListWidget extends StatelessWidget {
const ChatListWidget({super.key});
Widget _itemBuilderDirect(proto.DirectChat direct,
IMap<proto.TypedKey, proto.Contact> contactMap, bool busy) {
final contact = contactMap[direct.localConversationRecordKey];
if (contact == null) {
return const Text('...');
}
return ChatSingleContactItemWidget(contact: contact, disabled: busy)
.paddingLTRB(0, 4, 0, 0);
}
List<proto.Chat> _itemFilter(IMap<proto.TypedKey, proto.Contact> contactMap,
IList<DHTShortArrayElementState<Chat>> chatList, String filter) {
final lowerValue = filter.toLowerCase();
return chatList.map((x) => x.value).where((c) {
switch (c.whichKind()) {
case proto.Chat_Kind.direct:
final contact = contactMap[c.direct.localConversationRecordKey];
if (contact == null) {
return false;
}
return contact.nickname.toLowerCase().contains(lowerValue) ||
contact.profile.name.toLowerCase().contains(lowerValue) ||
contact.profile.pronouns.toLowerCase().contains(lowerValue);
case proto.Chat_Kind.group:
// xxx: how to filter group chats
return true;
case proto.Chat_Kind.notSet:
throw StateError('unknown chat kind');
}
}).toList();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final contactListV = context.watch<ContactListCubit>().state;
return contactListV.builder((context, contactList) {
final contactMap = IMap.fromIterable(contactList,
keyMapper: (c) => c.value.localConversationRecordKey,
valueMapper: (c) => c.value);
final chatListV = context.watch<ChatListCubit>().state;
return chatListV
.builder((context, chatList) => SizedBox.expand(
child: styledTitleContainer(
context: context,
title: translate('chat_list.chats'),
child: SizedBox.expand(
child: (chatList.isEmpty)
? const EmptyChatListWidget()
: SearchableList<proto.Chat>(
initialList: chatList.map((x) => x.value).toList(),
itemBuilder: (c) {
switch (c.whichKind()) {
case proto.Chat_Kind.direct:
return _itemBuilderDirect(
c.direct,
contactMap,
contactListV.busy || chatListV.busy);
case proto.Chat_Kind.group:
return const Text(
'group chats not yet supported!');
case proto.Chat_Kind.notSet:
throw StateError('unknown chat kind');
}
},
filter: (value) =>
_itemFilter(contactMap, chatList, value),
spaceBetweenSearchAndList: 4,
inputDecoration: InputDecoration(
labelText: translate('chat_list.search'),
),
),
).paddingAll(8))))
.paddingLTRB(8, 0, 8, 8);
});
}
}

View file

@ -28,13 +28,31 @@ class ChatSingleContactItemWidget extends StatelessWidget {
_contact.localConversationRecordKey.toVeilid(); _contact.localConversationRecordKey.toVeilid();
final selected = activeChatCubit.state == localConversationRecordKey; final selected = activeChatCubit.state == localConversationRecordKey;
late final String title;
late final String subtitle;
if (_contact.nickname.isNotEmpty) {
title = _contact.nickname;
if (_contact.profile.pronouns.isNotEmpty) {
subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})';
} else {
subtitle = _contact.profile.name;
}
} else {
title = _contact.profile.name;
if (_contact.profile.pronouns.isNotEmpty) {
subtitle = '(${_contact.profile.pronouns})';
} else {
subtitle = '';
}
}
return SliderTile( return SliderTile(
key: ObjectKey(_contact), key: ObjectKey(_contact),
disabled: _disabled, disabled: _disabled,
selected: selected, selected: selected,
tileScale: ScaleKind.secondary, tileScale: ScaleKind.secondary,
title: _contact.editedProfile.name, title: title,
subtitle: _contact.editedProfile.pronouns, subtitle: subtitle,
icon: Icons.chat, icon: Icons.chat,
onTap: () { onTap: () {
singleFuture(activeChatCubit, () async { singleFuture(activeChatCubit, () async {

View file

@ -1,73 +0,0 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:searchable_listview/searchable_listview.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import '../chat_list.dart';
class ChatSingleContactListWidget extends StatelessWidget {
const ChatSingleContactListWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final contactListV = context.watch<ContactListCubit>().state;
return contactListV.builder((context, contactList) {
final contactMap = IMap.fromIterable(contactList,
keyMapper: (c) => c.value.localConversationRecordKey,
valueMapper: (c) => c.value);
final chatListV = context.watch<ChatListCubit>().state;
return chatListV
.builder((context, chatList) => SizedBox.expand(
child: styledTitleContainer(
context: context,
title: translate('chat_list.chats'),
child: SizedBox.expand(
child: (chatList.isEmpty)
? const EmptyChatListWidget()
: SearchableList<proto.Chat>(
initialList: chatList.map((x) => x.value).toList(),
itemBuilder: (c) {
final contact =
contactMap[c.localConversationRecordKey];
if (contact == null) {
return const Text('...');
}
return ChatSingleContactItemWidget(
contact: contact,
disabled: contactListV.busy)
.paddingLTRB(0, 4, 0, 0);
},
filter: (value) {
final lowerValue = value.toLowerCase();
return chatList.map((x) => x.value).where((c) {
final contact =
contactMap[c.localConversationRecordKey];
if (contact == null) {
return false;
}
return contact.editedProfile.name
.toLowerCase()
.contains(lowerValue) ||
contact.editedProfile.pronouns
.toLowerCase()
.contains(lowerValue);
}).toList();
},
spaceBetweenSearchAndList: 4,
inputDecoration: InputDecoration(
labelText: translate('chat_list.search'),
),
),
).paddingAll(8))))
.paddingLTRB(8, 0, 8, 8);
});
}
}

View file

@ -1,3 +1,3 @@
export 'chat_list_widget.dart';
export 'chat_single_contact_item_widget.dart'; export 'chat_single_contact_item_widget.dart';
export 'chat_single_contact_list_widget.dart';
export 'empty_chat_list_widget.dart'; export 'empty_chat_list_widget.dart';

View file

@ -36,22 +36,16 @@ class ContactInvitationListCubit
StateMapFollowable<ContactInvitiationListState, TypedKey, StateMapFollowable<ContactInvitiationListState, TypedKey,
proto.ContactInvitationRecord> { proto.ContactInvitationRecord> {
ContactInvitationListCubit({ ContactInvitationListCubit({
required ActiveAccountInfo activeAccountInfo, required AccountInfo accountInfo,
required proto.Account account, required OwnedDHTRecordPointer contactInvitationListRecordPointer,
}) : _activeAccountInfo = activeAccountInfo, }) : _accountInfo = accountInfo,
_account = account,
super( super(
open: () => _open(activeAccountInfo, account), open: () => _open(accountInfo.accountRecordKey,
contactInvitationListRecordPointer),
decodeElement: proto.ContactInvitationRecord.fromBuffer); decodeElement: proto.ContactInvitationRecord.fromBuffer);
static Future<DHTShortArray> _open( static Future<DHTShortArray> _open(TypedKey accountRecordKey,
ActiveAccountInfo activeAccountInfo, proto.Account account) async { OwnedDHTRecordPointer contactInvitationListRecordPointer) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final contactInvitationListRecordPointer =
account.contactInvitationRecords.toVeilid();
final dhtRecord = await DHTShortArray.openOwned( final dhtRecord = await DHTShortArray.openOwned(
contactInvitationListRecordPointer, contactInvitationListRecordPointer,
debugName: 'ContactInvitationListCubit::_open::ContactInvitationList', debugName: 'ContactInvitationListCubit::_open::ContactInvitationList',
@ -61,7 +55,8 @@ class ContactInvitationListCubit
} }
Future<Uint8List> createInvitation( Future<Uint8List> createInvitation(
{required EncryptionKeyType encryptionKeyType, {required proto.Profile profile,
required EncryptionKeyType encryptionKeyType,
required String encryptionKey, required String encryptionKey,
required String message, required String message,
required Timestamp? expiration}) async { required Timestamp? expiration}) async {
@ -71,8 +66,8 @@ class ContactInvitationListCubit
final crcs = await pool.veilid.bestCryptoSystem(); final crcs = await pool.veilid.bestCryptoSystem();
final contactRequestWriter = await crcs.generateKeyPair(); final contactRequestWriter = await crcs.generateKeyPair();
final idcs = await _activeAccountInfo.identityCryptoSystem; final idcs = await _accountInfo.identityCryptoSystem;
final identityWriter = _activeAccountInfo.identityWriter; final identityWriter = _accountInfo.identityWriter;
// Encrypt the writer secret with the encryption key // Encrypt the writer secret with the encryption key
final encryptedSecret = await encryptionKeyType.encryptSecretToBytes( final encryptedSecret = await encryptionKeyType.encryptSecretToBytes(
@ -90,7 +85,7 @@ class ContactInvitationListCubit
await (await pool.createRecord( await (await pool.createRecord(
debugName: 'ContactInvitationListCubit::createInvitation::' debugName: 'ContactInvitationListCubit::createInvitation::'
'LocalConversation', 'LocalConversation',
parent: _activeAccountInfo.accountRecordKey, parent: _accountInfo.accountRecordKey,
schema: DHTSchema.smpl( schema: DHTSchema.smpl(
oCnt: 0, oCnt: 0,
members: [DHTSchemaMember(mKey: identityWriter.key, mCnt: 1)]))) members: [DHTSchemaMember(mKey: identityWriter.key, mCnt: 1)])))
@ -99,9 +94,8 @@ class ContactInvitationListCubit
// Make ContactRequestPrivate and encrypt with the writer secret // Make ContactRequestPrivate and encrypt with the writer secret
final crpriv = proto.ContactRequestPrivate() final crpriv = proto.ContactRequestPrivate()
..writerKey = contactRequestWriter.key.toProto() ..writerKey = contactRequestWriter.key.toProto()
..profile = _account.profile ..profile = profile
..superIdentityRecordKey = ..superIdentityRecordKey = _accountInfo.superIdentityRecordKey.toProto()
_activeAccountInfo.userLogin.superIdentityRecordKey.toProto()
..chatRecordKey = localConversation.key.toProto() ..chatRecordKey = localConversation.key.toProto()
..expiration = expiration?.toInt64() ?? Int64.ZERO; ..expiration = expiration?.toInt64() ?? Int64.ZERO;
final crprivbytes = crpriv.writeToBuffer(); final crprivbytes = crpriv.writeToBuffer();
@ -119,7 +113,7 @@ class ContactInvitationListCubit
await (await pool.createRecord( await (await pool.createRecord(
debugName: 'ContactInvitationListCubit::createInvitation::' debugName: 'ContactInvitationListCubit::createInvitation::'
'ContactRequestInbox', 'ContactRequestInbox',
parent: _activeAccountInfo.accountRecordKey, parent: _accountInfo.accountRecordKey,
schema: DHTSchema.smpl(oCnt: 1, members: [ schema: DHTSchema.smpl(oCnt: 1, members: [
DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key) DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key)
]), ]),
@ -158,8 +152,7 @@ class ContactInvitationListCubit
..message = message; ..message = message;
// Add ContactInvitationRecord to account's list // Add ContactInvitationRecord to account's list
// if this fails, don't keep retrying, user can try again later await operateWriteEventual((writer) async {
await operateWrite((writer) async {
await writer.add(cinvrec.writeToBuffer()); await writer.add(cinvrec.writeToBuffer());
}); });
}); });
@ -172,8 +165,6 @@ class ContactInvitationListCubit
{required bool accepted, {required bool accepted,
required TypedKey contactRequestInboxRecordKey}) async { required TypedKey contactRequestInboxRecordKey}) async {
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Remove ContactInvitationRecord from account's list // Remove ContactInvitationRecord from account's list
final deletedItem = await operateWrite((writer) async { final deletedItem = await operateWrite((writer) async {
@ -198,7 +189,7 @@ class ContactInvitationListCubit
await (await pool.openRecordOwned(contactRequestInbox, await (await pool.openRecordOwned(contactRequestInbox,
debugName: 'ContactInvitationListCubit::deleteInvitation::' debugName: 'ContactInvitationListCubit::deleteInvitation::'
'ContactRequestInbox', 'ContactRequestInbox',
parent: accountRecordKey)) parent: _accountInfo.accountRecordKey))
.scope((contactRequestInbox) async { .scope((contactRequestInbox) async {
// Wipe out old invitation so it shows up as invalid // Wipe out old invitation so it shows up as invalid
await contactRequestInbox.tryWriteBytes(Uint8List(0)); await contactRequestInbox.tryWriteBytes(Uint8List(0));
@ -240,7 +231,12 @@ class ContactInvitationListCubit
// inbox with our list of extant invitations // inbox with our list of extant invitations
// If we're chatting to ourselves, // If we're chatting to ourselves,
// we are validating an invitation we have created // we are validating an invitation we have created
final isSelf = state.state.asData!.value.indexWhere((cir) => final contactInvitationList = state.state.asData?.value;
if (contactInvitationList == null) {
return null;
}
final isSelf = contactInvitationList.indexWhere((cir) =>
cir.value.contactRequestInbox.recordKey.toVeilid() == cir.value.contactRequestInbox.recordKey.toVeilid() ==
contactRequestInboxKey) != contactRequestInboxKey) !=
-1; -1;
@ -248,7 +244,8 @@ class ContactInvitationListCubit
await (await pool.openRecordRead(contactRequestInboxKey, await (await pool.openRecordRead(contactRequestInboxKey,
debugName: 'ContactInvitationListCubit::validateInvitation::' debugName: 'ContactInvitationListCubit::validateInvitation::'
'ContactRequestInbox', 'ContactRequestInbox',
parent: _activeAccountInfo.accountRecordKey)) parent: pool.getParentRecordKey(contactRequestInboxKey) ??
_accountInfo.accountRecordKey))
.maybeDeleteScope(!isSelf, (contactRequestInbox) async { .maybeDeleteScope(!isSelf, (contactRequestInbox) async {
// //
final contactRequest = await contactRequestInbox final contactRequest = await contactRequestInbox
@ -293,8 +290,7 @@ class ContactInvitationListCubit
secret: writerSecret); secret: writerSecret);
out = ValidContactInvitation( out = ValidContactInvitation(
activeAccountInfo: _activeAccountInfo, accountInfo: _accountInfo,
account: _account,
contactRequestInboxKey: contactRequestInboxKey, contactRequestInboxKey: contactRequestInboxKey,
contactRequestPrivate: contactRequestPrivate, contactRequestPrivate: contactRequestPrivate,
contactSuperIdentity: contactSuperIdentity, contactSuperIdentity: contactSuperIdentity,
@ -318,6 +314,5 @@ class ContactInvitationListCubit
} }
// //
final ActiveAccountInfo _activeAccountInfo; final AccountInfo _accountInfo;
final proto.Account _account;
} }

View file

@ -7,27 +7,22 @@ import '../../proto/proto.dart' as proto;
class ContactRequestInboxCubit class ContactRequestInboxCubit
extends DefaultDHTRecordCubit<proto.SignedContactResponse?> { extends DefaultDHTRecordCubit<proto.SignedContactResponse?> {
ContactRequestInboxCubit( ContactRequestInboxCubit(
{required this.activeAccountInfo, required this.contactInvitationRecord}) {required AccountInfo accountInfo, required this.contactInvitationRecord})
: super( : super(
open: () => _open( open: () => _open(
activeAccountInfo: activeAccountInfo, accountInfo: accountInfo,
contactInvitationRecord: contactInvitationRecord), contactInvitationRecord: contactInvitationRecord),
decodeState: (buf) => buf.isEmpty decodeState: (buf) => buf.isEmpty
? null ? null
: proto.SignedContactResponse.fromBuffer(buf)); : proto.SignedContactResponse.fromBuffer(buf));
// ContactRequestInboxCubit.value(
// {required super.record,
// required this.activeAccountInfo,
// required this.contactInvitationRecord})
// : super.value(decodeState: proto.SignedContactResponse.fromBuffer);
static Future<DHTRecord> _open( static Future<DHTRecord> _open(
{required ActiveAccountInfo activeAccountInfo, {required AccountInfo accountInfo,
required proto.ContactInvitationRecord contactInvitationRecord}) async { required proto.ContactInvitationRecord contactInvitationRecord}) async {
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; final accountRecordKey = accountInfo.accountRecordKey;
final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); final writerSecret = contactInvitationRecord.writerSecret.toVeilid();
final recordKey = final recordKey =
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); contactInvitationRecord.contactRequestInbox.recordKey.toVeilid();
@ -42,6 +37,5 @@ class ContactRequestInboxCubit
defaultSubkey: 1); defaultSubkey: 1);
} }
final ActiveAccountInfo activeAccountInfo;
final proto.ContactInvitationRecord contactInvitationRecord; final proto.ContactInvitationRecord contactInvitationRecord;
} }

View file

@ -7,7 +7,7 @@ import 'package:meta/meta.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 '../../conversation/conversation.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../models/accepted_contact.dart'; import '../models/accepted_contact.dart';
@ -24,25 +24,27 @@ class InvitationStatus extends Equatable {
class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus, class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
proto.SignedContactResponse?> { proto.SignedContactResponse?> {
WaitingInvitationCubit(ContactRequestInboxCubit super.input, WaitingInvitationCubit(
{required ActiveAccountInfo activeAccountInfo, ContactRequestInboxCubit super.input, {
required proto.Account account, required AccountInfo accountInfo,
required proto.ContactInvitationRecord contactInvitationRecord}) required AccountRecordCubit accountRecordCubit,
: super( required proto.ContactInvitationRecord contactInvitationRecord,
}) : super(
transform: (signedContactResponse) => _transform( transform: (signedContactResponse) => _transform(
signedContactResponse, signedContactResponse,
activeAccountInfo: activeAccountInfo, accountInfo: accountInfo,
account: account, accountRecordCubit: accountRecordCubit,
contactInvitationRecord: contactInvitationRecord)); contactInvitationRecord: contactInvitationRecord));
static Future<AsyncValue<InvitationStatus>> _transform( static Future<AsyncValue<InvitationStatus>> _transform(
proto.SignedContactResponse? signedContactResponse, proto.SignedContactResponse? signedContactResponse,
{required ActiveAccountInfo activeAccountInfo, {required AccountInfo accountInfo,
required proto.Account account, required AccountRecordCubit accountRecordCubit,
required proto.ContactInvitationRecord contactInvitationRecord}) async { required proto.ContactInvitationRecord contactInvitationRecord}) async {
if (signedContactResponse == null) { if (signedContactResponse == null) {
return const AsyncValue.loading(); return const AsyncValue.loading();
} }
final contactResponseBytes = final contactResponseBytes =
Uint8List.fromList(signedContactResponse.contactResponse); Uint8List.fromList(signedContactResponse.contactResponse);
final contactResponse = final contactResponse =
@ -57,8 +59,11 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
// Verify // Verify
final idcs = await contactSuperIdentity.currentInstance.cryptoSystem; final idcs = await contactSuperIdentity.currentInstance.cryptoSystem;
final signature = signedContactResponse.identitySignature.toVeilid(); final signature = signedContactResponse.identitySignature.toVeilid();
await idcs.verify(contactSuperIdentity.currentInstance.publicKey, if (!await idcs.verify(contactSuperIdentity.currentInstance.publicKey,
contactResponseBytes, signature); contactResponseBytes, signature)) {
// Could not verify signature of contact response
return AsyncValue.error('Invalid signature on contact response.');
}
// Check for rejection // Check for rejection
if (!contactResponse.accept) { if (!contactResponse.accept) {
@ -71,7 +76,7 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
contactResponse.remoteConversationRecordKey.toVeilid(); contactResponse.remoteConversationRecordKey.toVeilid();
final conversation = ConversationCubit( final conversation = ConversationCubit(
activeAccountInfo: activeAccountInfo, accountInfo: accountInfo,
remoteIdentityPublicKey: remoteIdentityPublicKey:
contactSuperIdentity.currentInstance.typedPublicKey, contactSuperIdentity.currentInstance.typedPublicKey,
remoteConversationRecordKey: remoteConversationRecordKey); remoteConversationRecordKey: remoteConversationRecordKey);
@ -98,16 +103,13 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
final localConversationRecordKey = final localConversationRecordKey =
contactInvitationRecord.localConversationRecordKey.toVeilid(); contactInvitationRecord.localConversationRecordKey.toVeilid();
return conversation.initLocalConversation( return conversation.initLocalConversation(
profile: accountRecordCubit.state.asData!.value.profile,
existingConversationRecordKey: localConversationRecordKey, existingConversationRecordKey: localConversationRecordKey,
profile: account.profile, callback: (localConversation) async => AsyncValue.data(InvitationStatus(
// ignore: prefer_expression_function_bodies
callback: (localConversation) async {
return AsyncValue.data(InvitationStatus(
acceptedContact: AcceptedContact( acceptedContact: AcceptedContact(
remoteProfile: remoteProfile, remoteProfile: remoteProfile,
remoteIdentity: contactSuperIdentity, remoteIdentity: contactSuperIdentity,
remoteConversationRecordKey: remoteConversationRecordKey, remoteConversationRecordKey: remoteConversationRecordKey,
localConversationRecordKey: localConversationRecordKey))); localConversationRecordKey: localConversationRecordKey))));
});
} }
} }

View file

@ -3,6 +3,7 @@ import 'package:bloc_advanced_tools/bloc_advanced_tools.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 'cubits.dart'; import 'cubits.dart';
@ -18,7 +19,27 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
StateMapFollower<DHTShortArrayBusyState<proto.ContactInvitationRecord>, StateMapFollower<DHTShortArrayBusyState<proto.ContactInvitationRecord>,
TypedKey, proto.ContactInvitationRecord> { TypedKey, proto.ContactInvitationRecord> {
WaitingInvitationsBlocMapCubit( WaitingInvitationsBlocMapCubit(
{required this.activeAccountInfo, required this.account}); {required AccountInfo accountInfo,
required AccountRecordCubit accountRecordCubit,
required ContactInvitationListCubit contactInvitationListCubit,
required ContactListCubit contactListCubit})
: _accountInfo = accountInfo,
_accountRecordCubit = accountRecordCubit,
_contactInvitationListCubit = contactInvitationListCubit,
_contactListCubit = contactListCubit {
// React to invitation status changes
_singleInvitationStatusProcessor.follow(
stream, state, _invitationStatusListener);
// Follow the contact invitation list cubit
follow(contactInvitationListCubit);
}
@override
Future<void> close() async {
await _singleInvitationStatusProcessor.unfollow();
await super.close();
}
Future<void> _addWaitingInvitation( Future<void> _addWaitingInvitation(
{required proto.ContactInvitationRecord {required proto.ContactInvitationRecord
@ -27,22 +48,66 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(), contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(),
WaitingInvitationCubit( WaitingInvitationCubit(
ContactRequestInboxCubit( ContactRequestInboxCubit(
activeAccountInfo: activeAccountInfo, accountInfo: _accountInfo,
contactInvitationRecord: contactInvitationRecord), contactInvitationRecord: contactInvitationRecord),
activeAccountInfo: activeAccountInfo, accountInfo: _accountInfo,
account: account, accountRecordCubit: _accountRecordCubit,
contactInvitationRecord: contactInvitationRecord))); contactInvitationRecord: contactInvitationRecord)));
// Process all accepted or rejected invitations
Future<void> _invitationStatusListener(
WaitingInvitationsBlocMapState newState) async {
for (final entry in newState.entries) {
final contactRequestInboxRecordKey = entry.key;
final invStatus = entry.value.asData?.value;
// Skip invitations that have not yet been accepted or rejected
if (invStatus == null) {
continue;
}
// Delete invitation and process the accepted or rejected contact
final acceptedContact = invStatus.acceptedContact;
if (acceptedContact != null) {
await _contactInvitationListCubit.deleteInvitation(
accepted: true,
contactRequestInboxRecordKey: contactRequestInboxRecordKey);
// Accept
await _contactListCubit.createContact(
profile: acceptedContact.remoteProfile,
remoteSuperIdentity: acceptedContact.remoteIdentity,
remoteConversationRecordKey:
acceptedContact.remoteConversationRecordKey,
localConversationRecordKey:
acceptedContact.localConversationRecordKey,
);
} else {
// Reject
await _contactInvitationListCubit.deleteInvitation(
accepted: false,
contactRequestInboxRecordKey: contactRequestInboxRecordKey);
}
}
}
/// StateFollower ///////////////////////// /// StateFollower /////////////////////////
@override @override
Future<void> removeFromState(TypedKey key) => remove(key); Future<void> removeFromState(TypedKey key) => remove(key);
@override @override
Future<void> updateState(TypedKey key, proto.ContactInvitationRecord value) => Future<void> updateState(
_addWaitingInvitation(contactInvitationRecord: value); TypedKey key,
proto.ContactInvitationRecord? oldValue,
proto.ContactInvitationRecord newValue) async {
await _addWaitingInvitation(contactInvitationRecord: newValue);
}
//// ////
final ActiveAccountInfo activeAccountInfo; final AccountInfo _accountInfo;
final proto.Account account; final AccountRecordCubit _accountRecordCubit;
final ContactInvitationListCubit _contactInvitationListCubit;
final ContactListCubit _contactListCubit;
final _singleInvitationStatusProcessor =
SingleStateProcessor<WaitingInvitationsBlocMapState>();
} }

View file

@ -2,7 +2,7 @@ import 'package:meta/meta.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 '../../conversation/conversation.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import 'models.dart'; import 'models.dart';
@ -13,14 +13,12 @@ import 'models.dart';
class ValidContactInvitation { class ValidContactInvitation {
@internal @internal
ValidContactInvitation( ValidContactInvitation(
{required ActiveAccountInfo activeAccountInfo, {required AccountInfo accountInfo,
required proto.Account account,
required TypedKey contactRequestInboxKey, required TypedKey contactRequestInboxKey,
required proto.ContactRequestPrivate contactRequestPrivate, required proto.ContactRequestPrivate contactRequestPrivate,
required SuperIdentity contactSuperIdentity, required SuperIdentity contactSuperIdentity,
required KeyPair writer}) required KeyPair writer})
: _activeAccountInfo = activeAccountInfo, : _accountInfo = accountInfo,
_account = account,
_contactRequestInboxKey = contactRequestInboxKey, _contactRequestInboxKey = contactRequestInboxKey,
_contactRequestPrivate = contactRequestPrivate, _contactRequestPrivate = contactRequestPrivate,
_contactSuperIdentity = contactSuperIdentity, _contactSuperIdentity = contactSuperIdentity,
@ -28,44 +26,40 @@ class ValidContactInvitation {
proto.Profile get remoteProfile => _contactRequestPrivate.profile; proto.Profile get remoteProfile => _contactRequestPrivate.profile;
Future<AcceptedContact?> accept() async { Future<AcceptedContact?> accept(proto.Profile profile) async {
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
try { try {
// Ensure we don't delete this if we're trying to chat to self // Ensure we don't delete this if we're trying to chat to self
// The initiating side will delete the records in deleteInvitation() // The initiating side will delete the records in deleteInvitation()
final isSelf = _contactSuperIdentity.currentInstance.publicKey == final isSelf = _contactSuperIdentity.currentInstance.publicKey ==
_activeAccountInfo.identityPublicKey; _accountInfo.identityPublicKey;
final accountRecordKey = _activeAccountInfo.accountRecordKey;
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
debugName: 'ValidContactInvitation::accept::' debugName: 'ValidContactInvitation::accept::'
'ContactRequestInbox', 'ContactRequestInbox',
parent: accountRecordKey)) parent: pool.getParentRecordKey(_contactRequestInboxKey) ??
_accountInfo.accountRecordKey))
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
.maybeDeleteScope(!isSelf, (contactRequestInbox) async { .maybeDeleteScope(!isSelf, (contactRequestInbox) async {
// Create local conversation key for this // Create local conversation key for this
// contact and send via contact response // contact and send via contact response
final conversation = ConversationCubit( final conversation = ConversationCubit(
activeAccountInfo: _activeAccountInfo, accountInfo: _accountInfo,
remoteIdentityPublicKey: remoteIdentityPublicKey:
_contactSuperIdentity.currentInstance.typedPublicKey); _contactSuperIdentity.currentInstance.typedPublicKey);
return conversation.initLocalConversation( return conversation.initLocalConversation(
profile: _account.profile, profile: profile,
callback: (localConversation) async { callback: (localConversation) async {
final contactResponse = proto.ContactResponse() final contactResponse = proto.ContactResponse()
..accept = true ..accept = true
..remoteConversationRecordKey = localConversation.key.toProto() ..remoteConversationRecordKey = localConversation.key.toProto()
..superIdentityRecordKey = ..superIdentityRecordKey =
_activeAccountInfo.superIdentityRecordKey.toProto(); _accountInfo.superIdentityRecordKey.toProto();
final contactResponseBytes = contactResponse.writeToBuffer(); final contactResponseBytes = contactResponse.writeToBuffer();
final cs = await pool.veilid final cs = await _accountInfo.identityCryptoSystem;
.getCryptoSystem(_contactRequestInboxKey.kind); final identitySignature = await cs.signWithKeyPair(
_accountInfo.identityWriter, contactResponseBytes);
final identitySignature = await cs.sign(
_activeAccountInfo.identityWriter.key,
_activeAccountInfo.identityWriter.secret,
contactResponseBytes);
final signedContactResponse = proto.SignedContactResponse() final signedContactResponse = proto.SignedContactResponse()
..contactResponse = contactResponseBytes ..contactResponse = contactResponseBytes
@ -95,27 +89,22 @@ class ValidContactInvitation {
// Ensure we don't delete this if we're trying to chat to self // Ensure we don't delete this if we're trying to chat to self
final isSelf = _contactSuperIdentity.currentInstance.publicKey == final isSelf = _contactSuperIdentity.currentInstance.publicKey ==
_activeAccountInfo.identityPublicKey; _accountInfo.identityPublicKey;
final accountRecordKey = _activeAccountInfo.accountRecordKey;
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
debugName: 'ValidContactInvitation::reject::' debugName: 'ValidContactInvitation::reject::'
'ContactRequestInbox', 'ContactRequestInbox',
parent: accountRecordKey)) parent: _accountInfo.accountRecordKey))
.maybeDeleteScope(!isSelf, (contactRequestInbox) async { .maybeDeleteScope(!isSelf, (contactRequestInbox) async {
final cs =
await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind);
final contactResponse = proto.ContactResponse() final contactResponse = proto.ContactResponse()
..accept = false ..accept = false
..superIdentityRecordKey = ..superIdentityRecordKey =
_activeAccountInfo.superIdentityRecordKey.toProto(); _accountInfo.superIdentityRecordKey.toProto();
final contactResponseBytes = contactResponse.writeToBuffer(); final contactResponseBytes = contactResponse.writeToBuffer();
final identitySignature = await cs.sign( final cs = await _accountInfo.identityCryptoSystem;
_activeAccountInfo.identityWriter.key, final identitySignature = await cs.signWithKeyPair(
_activeAccountInfo.identityWriter.secret, _accountInfo.identityWriter, contactResponseBytes);
contactResponseBytes);
final signedContactResponse = proto.SignedContactResponse() final signedContactResponse = proto.SignedContactResponse()
..contactResponse = contactResponseBytes ..contactResponse = contactResponseBytes
@ -129,8 +118,7 @@ class ValidContactInvitation {
} }
// //
final ActiveAccountInfo _activeAccountInfo; final AccountInfo _accountInfo;
final proto.Account _account;
final TypedKey _contactRequestInboxKey; final TypedKey _contactRequestInboxKey;
final SuperIdentity _contactSuperIdentity; final SuperIdentity _contactSuperIdentity;
final KeyPair _writer; final KeyPair _writer;

View file

@ -46,7 +46,6 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
//final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
final signedContactInvitationBytesV = final signedContactInvitationBytesV =
@ -58,6 +57,9 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
return PopControl( return PopControl(
dismissible: !signedContactInvitationBytesV.isLoading, dismissible: !signedContactInvitationBytesV.isLoading,
child: Dialog( child: Dialog(
shape: RoundedRectangleBorder(
side: const BorderSide(width: 2),
borderRadius: BorderRadius.circular(16)),
backgroundColor: Colors.white, backgroundColor: Colors.white,
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
@ -90,6 +92,10 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
.paddingAll(8), .paddingAll(8),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.copy), icon: const Icon(Icons.copy),
style: ElevatedButton.styleFrom(
foregroundColor: Colors.black,
backgroundColor: Colors.white,
side: const BorderSide()),
label: Text(translate( label: Text(translate(
'create_invitation_dialog.copy_invitation')), 'create_invitation_dialog.copy_invitation')),
onPressed: () async { onPressed: () async {

View file

@ -140,8 +140,18 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
// Start generation // Start generation
final contactInvitationListCubit = final contactInvitationListCubit =
widget.modalContext.read<ContactInvitationListCubit>(); widget.modalContext.read<ContactInvitationListCubit>();
final profile = widget.modalContext
.read<AccountRecordCubit>()
.state
.asData
?.value
.profile;
if (profile == null) {
return;
}
final generator = contactInvitationListCubit.createInvitation( final generator = contactInvitationListCubit.createInvitation(
profile: profile,
encryptionKeyType: _encryptionKeyType, encryptionKeyType: _encryptionKeyType,
encryptionKey: _encryptionKey, encryptionKey: _encryptionKey,
message: _messageTextController.text, message: _messageTextController.text,

View file

@ -3,8 +3,8 @@ import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:provider/provider.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';
@ -15,13 +15,14 @@ import '../contact_invitation.dart';
class InvitationDialog extends StatefulWidget { class InvitationDialog extends StatefulWidget {
const InvitationDialog( const InvitationDialog(
{required this.modalContext, {required Locator locator,
required this.onValidationCancelled, required this.onValidationCancelled,
required this.onValidationSuccess, required this.onValidationSuccess,
required this.onValidationFailed, required this.onValidationFailed,
required this.inviteControlIsValid, required this.inviteControlIsValid,
required this.buildInviteControl, required this.buildInviteControl,
super.key}); super.key})
: _locator = locator;
final void Function() onValidationCancelled; final void Function() onValidationCancelled;
final void Function() onValidationSuccess; final void Function() onValidationSuccess;
@ -32,7 +33,7 @@ class InvitationDialog extends StatefulWidget {
InvitationDialogState dialogState, InvitationDialogState dialogState,
Future<void> Function({required Uint8List inviteData}) Future<void> Function({required Uint8List inviteData})
validateInviteData) buildInviteControl; validateInviteData) buildInviteControl;
final BuildContext modalContext; final Locator _locator;
@override @override
InvitationDialogState createState() => InvitationDialogState(); InvitationDialogState createState() => InvitationDialogState();
@ -54,8 +55,7 @@ class InvitationDialog extends StatefulWidget {
InvitationDialogState dialogState, InvitationDialogState dialogState,
Future<void> Function({required Uint8List inviteData}) Future<void> Function({required Uint8List inviteData})
validateInviteData)>.has( validateInviteData)>.has(
'buildInviteControl', buildInviteControl)) 'buildInviteControl', buildInviteControl));
..add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
} }
} }
@ -74,23 +74,25 @@ class InvitationDialogState extends State<InvitationDialog> {
Future<void> _onAccept() async { Future<void> _onAccept() async {
final navigator = Navigator.of(context); final navigator = Navigator.of(context);
final activeAccountInfo = widget.modalContext.read<ActiveAccountInfo>(); final accountInfo = widget._locator<AccountInfoCubit>().state;
final contactList = widget.modalContext.read<ContactListCubit>(); final contactList = widget._locator<ContactListCubit>();
final profile =
widget._locator<AccountRecordCubit>().state.asData!.value.profile;
setState(() { setState(() {
_isAccepting = true; _isAccepting = true;
}); });
final validInvitation = _validInvitation; final validInvitation = _validInvitation;
if (validInvitation != null) { if (validInvitation != null) {
final acceptedContact = await validInvitation.accept(); final acceptedContact = await validInvitation.accept(profile);
if (acceptedContact != null) { if (acceptedContact != null) {
// initiator when accept is received will create // initiator when accept is received will create
// contact in the case of a 'note to self' // contact in the case of a 'note to self'
final isSelf = activeAccountInfo.identityPublicKey == final isSelf = accountInfo.identityPublicKey ==
acceptedContact.remoteIdentity.currentInstance.publicKey; acceptedContact.remoteIdentity.currentInstance.publicKey;
if (!isSelf) { if (!isSelf) {
await contactList.createContact( await contactList.createContact(
remoteProfile: acceptedContact.remoteProfile, profile: acceptedContact.remoteProfile,
remoteSuperIdentity: acceptedContact.remoteIdentity, remoteSuperIdentity: acceptedContact.remoteIdentity,
remoteConversationRecordKey: remoteConversationRecordKey:
acceptedContact.remoteConversationRecordKey, acceptedContact.remoteConversationRecordKey,
@ -137,7 +139,7 @@ class InvitationDialogState extends State<InvitationDialog> {
}) async { }) async {
try { try {
final contactInvitationListCubit = final contactInvitationListCubit =
widget.modalContext.read<ContactInvitationListCubit>(); widget._locator<ContactInvitationListCubit>();
setState(() { setState(() {
_isValidating = true; _isValidating = true;

View file

@ -4,6 +4,7 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
@ -11,29 +12,23 @@ import '../../tools/tools.dart';
import 'invitation_dialog.dart'; import 'invitation_dialog.dart';
class PasteInvitationDialog extends StatefulWidget { class PasteInvitationDialog extends StatefulWidget {
const PasteInvitationDialog({required this.modalContext, super.key}); const PasteInvitationDialog({required Locator locator, super.key})
: _locator = locator;
@override @override
PasteInvitationDialogState createState() => PasteInvitationDialogState(); PasteInvitationDialogState createState() => PasteInvitationDialogState();
static Future<void> show(BuildContext context) async { static Future<void> show(BuildContext context) async {
final modalContext = context; final locator = context.read;
await showPopControlDialog<void>( await showPopControlDialog<void>(
context: context, context: context,
builder: (context) => StyledDialog( builder: (context) => StyledDialog(
title: translate('paste_invitation_dialog.title'), title: translate('paste_invitation_dialog.title'),
child: PasteInvitationDialog(modalContext: modalContext))); child: PasteInvitationDialog(locator: locator)));
} }
final BuildContext modalContext; final Locator _locator;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
.add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
}
} }
class PasteInvitationDialogState extends State<PasteInvitationDialog> { class PasteInvitationDialogState extends State<PasteInvitationDialog> {
@ -138,7 +133,7 @@ class PasteInvitationDialogState extends State<PasteInvitationDialog> {
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InvitationDialog( return InvitationDialog(
modalContext: widget.modalContext, locator: widget._locator,
onValidationCancelled: onValidationCancelled, onValidationCancelled: onValidationCancelled,
onValidationSuccess: onValidationSuccess, onValidationSuccess: onValidationSuccess,
onValidationFailed: onValidationFailed, onValidationFailed: onValidationFailed,

View file

@ -9,6 +9,7 @@ import 'package:flutter_translate/flutter_translate.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:pasteboard/pasteboard.dart'; import 'package:pasteboard/pasteboard.dart';
import 'package:provider/provider.dart';
import 'package:zxing2/qrcode.dart'; import 'package:zxing2/qrcode.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
@ -102,28 +103,22 @@ class ScannerOverlay extends CustomPainter {
} }
class ScanInvitationDialog extends StatefulWidget { class ScanInvitationDialog extends StatefulWidget {
const ScanInvitationDialog({required this.modalContext, super.key}); const ScanInvitationDialog({required Locator locator, super.key})
: _locator = locator;
@override @override
ScanInvitationDialogState createState() => ScanInvitationDialogState(); ScanInvitationDialogState createState() => ScanInvitationDialogState();
static Future<void> show(BuildContext context) async { static Future<void> show(BuildContext context) async {
final modalContext = context; final locator = context.read;
await showPopControlDialog<void>( await showPopControlDialog<void>(
context: context, context: context,
builder: (context) => StyledDialog( builder: (context) => StyledDialog(
title: translate('scan_invitation_dialog.title'), title: translate('scan_invitation_dialog.title'),
child: ScanInvitationDialog(modalContext: modalContext))); child: ScanInvitationDialog(locator: locator)));
} }
final BuildContext modalContext; final Locator _locator;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
.add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
}
} }
class ScanInvitationDialogState extends State<ScanInvitationDialog> { class ScanInvitationDialogState extends State<ScanInvitationDialog> {
@ -396,7 +391,7 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InvitationDialog( return InvitationDialog(
modalContext: widget.modalContext, locator: widget._locator,
onValidationCancelled: onValidationCancelled, onValidationCancelled: onValidationCancelled,
onValidationSuccess: onValidationSuccess, onValidationSuccess: onValidationSuccess,
onValidationFailed: onValidationFailed, onValidationFailed: onValidationFailed,

View file

@ -1,79 +1,117 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:async_tools/async_tools.dart';
import 'package:protobuf/protobuf.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 '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import 'conversation_cubit.dart';
////////////////////////////////////////////////// //////////////////////////////////////////////////
// Mutable state for per-account contacts // Mutable state for per-account contacts
class ContactListCubit extends DHTShortArrayCubit<proto.Contact> { class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
ContactListCubit({ ContactListCubit({
required ActiveAccountInfo activeAccountInfo, required AccountInfo accountInfo,
required proto.Account account, required OwnedDHTRecordPointer contactListRecordPointer,
}) : _activeAccountInfo = activeAccountInfo, }) : super(
super( open: () =>
open: () => _open(activeAccountInfo, account), _open(accountInfo.accountRecordKey, contactListRecordPointer),
decodeElement: proto.Contact.fromBuffer); decodeElement: proto.Contact.fromBuffer);
static Future<DHTShortArray> _open( static Future<DHTShortArray> _open(TypedKey accountRecordKey,
ActiveAccountInfo activeAccountInfo, proto.Account account) async { OwnedDHTRecordPointer contactListRecordPointer) async {
final accountRecordKey = final dhtRecord = await DHTShortArray.openOwned(contactListRecordPointer,
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final contactListRecordKey = account.contactList.toVeilid();
final dhtRecord = await DHTShortArray.openOwned(contactListRecordKey,
debugName: 'ContactListCubit::_open::ContactList', debugName: 'ContactListCubit::_open::ContactList',
parent: accountRecordKey); parent: accountRecordKey);
return dhtRecord; return dhtRecord;
} }
Future<void> createContact({ @override
required proto.Profile remoteProfile, Future<void> close() async {
required SuperIdentity remoteSuperIdentity, await _contactProfileUpdateMap.close();
required TypedKey remoteConversationRecordKey, await super.close();
}
////////////////////////////////////////////////////////////////////////////
// Public Interface
void followContactProfileChanges(TypedKey localConversationRecordKey,
Stream<proto.Profile?> profileStream, proto.Profile? profileState) {
_contactProfileUpdateMap
.follow(localConversationRecordKey, profileStream, profileState,
(remoteProfile) async {
if (remoteProfile == null) {
return;
}
return updateContactProfile(
localConversationRecordKey: localConversationRecordKey,
profile: remoteProfile);
});
}
Future<void> updateContactProfile({
required TypedKey localConversationRecordKey, required TypedKey localConversationRecordKey,
required proto.Profile profile,
}) async {
// Update contact's remoteProfile
await operateWriteEventual((writer) async {
for (var pos = 0; pos < writer.length; pos++) {
final c = await writer.getProtobuf(proto.Contact.fromBuffer, pos);
if (c != null &&
c.localConversationRecordKey.toVeilid() ==
localConversationRecordKey) {
if (c.profile == profile) {
// Unchanged
break;
}
final newContact = c.deepCopy()..profile = profile;
final updated = await writer.tryWriteItemProtobuf(
proto.Contact.fromBuffer, pos, newContact);
if (!updated) {
throw DHTExceptionTryAgain();
}
break;
}
}
});
}
Future<void> createContact({
required proto.Profile profile,
required SuperIdentity remoteSuperIdentity,
required TypedKey localConversationRecordKey,
required TypedKey remoteConversationRecordKey,
}) async { }) async {
// Create Contact // Create Contact
final contact = proto.Contact() final contact = proto.Contact()
..editedProfile = remoteProfile ..profile = profile
..remoteProfile = remoteProfile
..superIdentityJson = jsonEncode(remoteSuperIdentity.toJson()) ..superIdentityJson = jsonEncode(remoteSuperIdentity.toJson())
..identityPublicKey = ..identityPublicKey =
remoteSuperIdentity.currentInstance.typedPublicKey.toProto() remoteSuperIdentity.currentInstance.typedPublicKey.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto()
..localConversationRecordKey = localConversationRecordKey.toProto() ..localConversationRecordKey = localConversationRecordKey.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto()
..showAvailability = false; ..showAvailability = false;
// Add Contact to account's list // Add Contact to account's list
// if this fails, don't keep retrying, user can try again later await operateWriteEventual((writer) async {
await operateWrite((writer) async {
await writer.add(contact.writeToBuffer()); await writer.add(contact.writeToBuffer());
}); });
} }
Future<void> deleteContact({required proto.Contact contact}) async { Future<void> deleteContact(
final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid(); {required TypedKey localConversationRecordKey}) async {
final localConversationRecordKey =
contact.localConversationRecordKey.toVeilid();
final remoteConversationRecordKey =
contact.remoteConversationRecordKey.toVeilid();
// Remove Contact from account's list // Remove Contact from account's list
final deletedItem = await operateWrite((writer) async { final deletedItem = await operateWriteEventual((writer) async {
for (var i = 0; i < writer.length; i++) { for (var i = 0; i < writer.length; i++) {
final item = await writer.getProtobuf(proto.Contact.fromBuffer, i); final item = await writer.getProtobuf(proto.Contact.fromBuffer, i);
if (item == null) { if (item == null) {
throw Exception('Failed to get contact'); throw Exception('Failed to get contact');
} }
if (item.localConversationRecordKey == if (item.localConversationRecordKey.toVeilid() ==
contact.localConversationRecordKey) { localConversationRecordKey) {
await writer.remove(i); await writer.remove(i);
return item; return item;
} }
@ -83,21 +121,17 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
if (deletedItem != null) { if (deletedItem != null) {
try { try {
// Make a conversation cubit to manipulate the conversation // Mark the conversation records for deletion
final conversationCubit = ConversationCubit( await DHTRecordPool.instance
activeAccountInfo: _activeAccountInfo, .deleteRecord(deletedItem.localConversationRecordKey.toVeilid());
remoteIdentityPublicKey: remoteIdentityPublicKey, await DHTRecordPool.instance
localConversationRecordKey: localConversationRecordKey, .deleteRecord(deletedItem.remoteConversationRecordKey.toVeilid());
remoteConversationRecordKey: remoteConversationRecordKey,
);
// Delete the local and remote conversation records
await conversationCubit.delete();
} on Exception catch (e) { } on Exception catch (e) {
log.debug('error deleting conversation records: $e', e); log.debug('error deleting conversation records: $e', e);
} }
} }
} }
final ActiveAccountInfo _activeAccountInfo; final _contactProfileUpdateMap =
SingleStateProcessorMap<TypedKey, proto.Profile?>();
} }

View file

@ -1,2 +1 @@
export 'contact_list_cubit.dart'; export 'contact_list_cubit.dart';
export 'conversation_cubit.dart';

View file

@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -11,18 +10,9 @@ import '../contacts.dart';
class ContactItemWidget extends StatelessWidget { class ContactItemWidget extends StatelessWidget {
const ContactItemWidget( const ContactItemWidget(
{required this.contact, required this.disabled, super.key}); {required proto.Contact contact, required bool disabled, super.key})
: _disabled = disabled,
final proto.Contact contact; _contact = contact;
final bool disabled;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<proto.Contact>('contact', contact))
..add(DiagnosticsProperty<bool>('disabled', disabled));
}
@override @override
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
@ -30,26 +20,44 @@ class ContactItemWidget extends StatelessWidget {
BuildContext context, BuildContext context,
) { ) {
final localConversationRecordKey = final localConversationRecordKey =
contact.localConversationRecordKey.toVeilid(); _contact.localConversationRecordKey.toVeilid();
const selected = false; // xxx: eventually when we have selectable contacts: const selected = false; // xxx: eventually when we have selectable contacts:
// activeContactCubit.state == localConversationRecordKey; // activeContactCubit.state == localConversationRecordKey;
final tileDisabled = disabled || context.watch<ContactListCubit>().isBusy; final tileDisabled = _disabled || context.watch<ContactListCubit>().isBusy;
late final String title;
late final String subtitle;
if (_contact.nickname.isNotEmpty) {
title = _contact.nickname;
if (_contact.profile.pronouns.isNotEmpty) {
subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})';
} else {
subtitle = _contact.profile.name;
}
} else {
title = _contact.profile.name;
if (_contact.profile.pronouns.isNotEmpty) {
subtitle = '(${_contact.profile.pronouns})';
} else {
subtitle = '';
}
}
return SliderTile( return SliderTile(
key: ObjectKey(contact), key: ObjectKey(_contact),
disabled: tileDisabled, disabled: tileDisabled,
selected: selected, selected: selected,
tileScale: ScaleKind.primary, tileScale: ScaleKind.primary,
title: contact.editedProfile.name, title: title,
subtitle: contact.editedProfile.pronouns, subtitle: subtitle,
icon: Icons.person, icon: Icons.person,
onTap: () async { onTap: () async {
// Start a chat // Start a chat
final chatListCubit = context.read<ChatListCubit>(); final chatListCubit = context.read<ChatListCubit>();
await chatListCubit.getOrCreateChatSingleContact(contact: contact); await chatListCubit.getOrCreateChatSingleContact(contact: _contact);
// Click over to chats // Click over to chats
if (context.mounted) { if (context.mounted) {
await MainPager.of(context) await MainPager.of(context)
@ -66,14 +74,20 @@ class ContactItemWidget extends StatelessWidget {
final contactListCubit = context.read<ContactListCubit>(); final contactListCubit = context.read<ContactListCubit>();
final chatListCubit = context.read<ChatListCubit>(); final chatListCubit = context.read<ChatListCubit>();
// Delete the contact itself
await contactListCubit.deleteContact(
localConversationRecordKey: localConversationRecordKey);
// Remove any chats for this contact // Remove any chats for this contact
await chatListCubit.deleteChat( await chatListCubit.deleteChat(
localConversationRecordKey: localConversationRecordKey); localConversationRecordKey: localConversationRecordKey);
// Delete the contact itself
await contactListCubit.deleteContact(contact: contact);
}) })
], ],
); );
} }
////////////////////////////////////////////////////////////////////////////
final proto.Contact _contact;
final bool _disabled;
} }

View file

@ -45,10 +45,13 @@ class ContactListWidget extends StatelessWidget {
final lowerValue = value.toLowerCase(); final lowerValue = value.toLowerCase();
return contactList return contactList
.where((element) => .where((element) =>
element.editedProfile.name element.nickname
.toLowerCase() .toLowerCase()
.contains(lowerValue) || .contains(lowerValue) ||
element.editedProfile.pronouns element.profile.name
.toLowerCase()
.contains(lowerValue) ||
element.profile.pronouns
.toLowerCase() .toLowerCase()
.contains(lowerValue)) .contains(lowerValue))
.toList(); .toList();

View file

@ -0,0 +1 @@
export 'cubits/cubits.dart';

View file

@ -0,0 +1,180 @@
import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_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 '../../chat_list/cubits/cubits.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import '../conversation.dart';
@immutable
class ActiveConversationState extends Equatable {
const ActiveConversationState({
required this.remoteIdentityPublicKey,
required this.localConversationRecordKey,
required this.remoteConversationRecordKey,
required this.localConversation,
required this.remoteConversation,
});
final TypedKey remoteIdentityPublicKey;
final TypedKey localConversationRecordKey;
final TypedKey remoteConversationRecordKey;
final proto.Conversation localConversation;
final proto.Conversation remoteConversation;
@override
List<Object?> get props => [
remoteIdentityPublicKey,
localConversationRecordKey,
remoteConversationRecordKey,
localConversation,
remoteConversation
];
}
typedef ActiveConversationCubit = TransformerCubit<
AsyncValue<ActiveConversationState>,
AsyncValue<ConversationState>,
ConversationCubit>;
typedef ActiveConversationsBlocMapState
= BlocMapState<TypedKey, AsyncValue<ActiveConversationState>>;
// Map of localConversationRecordKey to ActiveConversationCubit
// Wraps a conversation cubit to only expose completely built conversations
// Automatically follows the state of a ChatListCubit.
// We currently only build the cubits for the chats that are active, not
// archived chats or contacts that are not actively in a chat.
//
// TODO: Polling contacts for new inactive chats is yet to be done
//
class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<ActiveConversationState>, ActiveConversationCubit>
with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> {
ActiveConversationsBlocMapCubit({
required AccountInfo accountInfo,
required AccountRecordCubit accountRecordCubit,
required ChatListCubit chatListCubit,
required ContactListCubit contactListCubit,
}) : _accountInfo = accountInfo,
_accountRecordCubit = accountRecordCubit,
_contactListCubit = contactListCubit {
// Follow the chat list cubit
follow(chatListCubit);
}
////////////////////////////////////////////////////////////////////////////
// Public Interface
////////////////////////////////////////////////////////////////////////////
// Private Implementation
// Add an active conversation to be tracked for changes
Future<void> _addDirectConversation(
{required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationRecordKey,
required TypedKey remoteConversationRecordKey}) async =>
add(() {
// Conversation cubit the tracks the state between the local
// and remote halves of a contact's relationship with this account
final conversationCubit = ConversationCubit(
accountInfo: _accountInfo,
remoteIdentityPublicKey: remoteIdentityPublicKey,
localConversationRecordKey: localConversationRecordKey,
remoteConversationRecordKey: remoteConversationRecordKey,
);
// When remote conversation changes its profile,
// update our local contact
_contactListCubit.followContactProfileChanges(
localConversationRecordKey,
conversationCubit.stream.map((x) => x.map(
data: (d) => d.value.remoteConversation?.profile,
loading: (_) => null,
error: (_) => null)),
conversationCubit.state.asData?.value.remoteConversation?.profile);
// When our local account profile changes, send it to the conversation
conversationCubit.watchAccountChanges(
_accountRecordCubit.stream, _accountRecordCubit.state);
// Transformer that only passes through completed/active conversations
// along with the contact that corresponds to the completed
// conversation
final transformedCubit = TransformerCubit<
AsyncValue<ActiveConversationState>,
AsyncValue<ConversationState>,
ConversationCubit>(conversationCubit,
transform: (avstate) => avstate.when(
data: (data) => (data.localConversation == null ||
data.remoteConversation == null)
? const AsyncValue.loading()
: AsyncValue.data(ActiveConversationState(
localConversation: data.localConversation!,
remoteConversation: data.remoteConversation!,
remoteIdentityPublicKey: remoteIdentityPublicKey,
localConversationRecordKey: localConversationRecordKey,
remoteConversationRecordKey:
remoteConversationRecordKey)),
loading: AsyncValue.loading,
error: AsyncValue.error));
return MapEntry(localConversationRecordKey, transformedCubit);
});
/// StateFollower /////////////////////////
@override
Future<void> removeFromState(TypedKey key) => remove(key);
@override
Future<void> updateState(
TypedKey key, proto.Chat? oldValue, proto.Chat newValue) async {
switch (newValue.whichKind()) {
case proto.Chat_Kind.notSet:
throw StateError('unknown chat kind');
case proto.Chat_Kind.direct:
final localConversationRecordKey =
newValue.direct.localConversationRecordKey.toVeilid();
final remoteIdentityPublicKey =
newValue.direct.remoteMember.remoteIdentityPublicKey.toVeilid();
final remoteConversationRecordKey =
newValue.direct.remoteMember.remoteConversationRecordKey.toVeilid();
if (oldValue != null) {
final oldLocalConversationRecordKey =
oldValue.direct.localConversationRecordKey.toVeilid();
final oldRemoteIdentityPublicKey =
oldValue.direct.remoteMember.remoteIdentityPublicKey.toVeilid();
final oldRemoteConversationRecordKey = oldValue
.direct.remoteMember.remoteConversationRecordKey
.toVeilid();
if (oldLocalConversationRecordKey == localConversationRecordKey &&
oldRemoteIdentityPublicKey == remoteIdentityPublicKey &&
oldRemoteConversationRecordKey == remoteConversationRecordKey) {
return;
}
}
await _addDirectConversation(
remoteIdentityPublicKey: remoteIdentityPublicKey,
localConversationRecordKey: localConversationRecordKey,
remoteConversationRecordKey: remoteConversationRecordKey);
break;
case proto.Chat_Kind.group:
break;
}
}
////
final AccountInfo _accountInfo;
final AccountRecordCubit _accountRecordCubit;
final ContactListCubit _contactListCubit;
}

View file

@ -0,0 +1,114 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_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 '../../chat/chat.dart';
import '../../proto/proto.dart' as proto;
import '../conversation.dart';
import 'active_conversations_bloc_map_cubit.dart';
@immutable
class _SingleContactChatState extends Equatable {
const _SingleContactChatState(
{required this.remoteIdentityPublicKey,
required this.localConversationRecordKey,
required this.remoteConversationRecordKey,
required this.localMessagesRecordKey,
required this.remoteMessagesRecordKey});
final TypedKey remoteIdentityPublicKey;
final TypedKey localConversationRecordKey;
final TypedKey remoteConversationRecordKey;
final TypedKey localMessagesRecordKey;
final TypedKey remoteMessagesRecordKey;
@override
List<Object?> get props => [
remoteIdentityPublicKey,
localConversationRecordKey,
remoteConversationRecordKey,
localMessagesRecordKey,
remoteMessagesRecordKey
];
}
// Map of localConversationRecordKey to MessagesCubit
// Wraps a MessagesCubit to stream the latest messages to the state
// Automatically follows the state of a ActiveConversationsBlocMapCubit.
class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
SingleContactMessagesState, SingleContactMessagesCubit>
with
StateMapFollower<ActiveConversationsBlocMapState, TypedKey,
AsyncValue<ActiveConversationState>> {
ActiveSingleContactChatBlocMapCubit({
required AccountInfo accountInfo,
required ActiveConversationsBlocMapCubit activeConversationsBlocMapCubit,
}) : _accountInfo = accountInfo {
// Follow the active conversations bloc map cubit
follow(activeConversationsBlocMapCubit);
}
Future<void> _addConversationMessages(_SingleContactChatState state) async =>
add(() => MapEntry(
state.localConversationRecordKey,
SingleContactMessagesCubit(
accountInfo: _accountInfo,
remoteIdentityPublicKey: state.remoteIdentityPublicKey,
localConversationRecordKey: state.localConversationRecordKey,
remoteConversationRecordKey: state.remoteConversationRecordKey,
localMessagesRecordKey: state.localMessagesRecordKey,
remoteMessagesRecordKey: state.remoteMessagesRecordKey,
)));
_SingleContactChatState? _mapStateValue(
AsyncValue<ActiveConversationState> avInputState) {
final inputState = avInputState.asData?.value;
if (inputState == null) {
return null;
}
return _SingleContactChatState(
remoteIdentityPublicKey: inputState.remoteIdentityPublicKey,
localConversationRecordKey: inputState.localConversationRecordKey,
remoteConversationRecordKey: inputState.remoteConversationRecordKey,
localMessagesRecordKey:
inputState.localConversation.messages.toVeilid(),
remoteMessagesRecordKey:
inputState.remoteConversation.messages.toVeilid());
}
/// StateFollower /////////////////////////
@override
Future<void> removeFromState(TypedKey key) => remove(key);
@override
Future<void> updateState(
TypedKey key,
AsyncValue<ActiveConversationState>? oldValue,
AsyncValue<ActiveConversationState> newValue) async {
final newState = _mapStateValue(newValue);
if (oldValue != null) {
final oldState = _mapStateValue(oldValue);
if (oldState == newState) {
return;
}
}
if (newState != null) {
await _addConversationMessages(newState);
} else if (newValue.isLoading) {
await addState(key, const AsyncValue.loading());
} else {
final (error, stackTrace) =
(newValue.asError!.error, newValue.asError!.stackTrace);
await addState(key, AsyncValue.error(error, stackTrace));
}
}
////
final AccountInfo _accountInfo;
}

View file

@ -9,11 +9,13 @@ import 'package:async_tools/async_tools.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:protobuf/protobuf.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 '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
const _sfUpdateAccountChange = 'updateAccountChange';
@immutable @immutable
class ConversationState extends Equatable { class ConversationState extends Equatable {
@ -27,32 +29,35 @@ class ConversationState extends Equatable {
List<Object?> get props => [localConversation, remoteConversation]; List<Object?> get props => [localConversation, remoteConversation];
} }
/// Represents the control channel between two contacts
/// Used to pass profile, identity and status changes, and the messages key for
/// 1-1 chats
class ConversationCubit extends Cubit<AsyncValue<ConversationState>> { class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
ConversationCubit( ConversationCubit(
{required ActiveAccountInfo activeAccountInfo, {required AccountInfo accountInfo,
required TypedKey remoteIdentityPublicKey, required TypedKey remoteIdentityPublicKey,
TypedKey? localConversationRecordKey, TypedKey? localConversationRecordKey,
TypedKey? remoteConversationRecordKey}) TypedKey? remoteConversationRecordKey})
: _activeAccountInfo = activeAccountInfo, : _accountInfo = accountInfo,
_localConversationRecordKey = localConversationRecordKey, _localConversationRecordKey = localConversationRecordKey,
_remoteIdentityPublicKey = remoteIdentityPublicKey, _remoteIdentityPublicKey = remoteIdentityPublicKey,
_remoteConversationRecordKey = remoteConversationRecordKey, _remoteConversationRecordKey = remoteConversationRecordKey,
super(const AsyncValue.loading()) { super(const AsyncValue.loading()) {
_identityWriter = _accountInfo.identityWriter;
if (_localConversationRecordKey != null) { if (_localConversationRecordKey != null) {
_initWait.add(() async { _initWait.add(() async {
await _setLocalConversation(() async { await _setLocalConversation(() async {
final accountRecordKey = _activeAccountInfo
.userLogin.accountRecordInfo.accountRecord.recordKey;
// Open local record key if it is specified // Open local record key if it is specified
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
final crypto = await _cachedConversationCrypto(); final crypto = await _cachedConversationCrypto();
final writer = _activeAccountInfo.identityWriter; final writer = _identityWriter;
final record = await pool.openRecordWrite( final record = await pool.openRecordWrite(
_localConversationRecordKey!, writer, _localConversationRecordKey!, writer,
debugName: 'ConversationCubit::LocalConversation', debugName: 'ConversationCubit::LocalConversation',
parent: accountRecordKey, parent: accountInfo.accountRecordKey,
crypto: crypto); crypto: crypto);
return record; return record;
}); });
}); });
@ -61,15 +66,13 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
if (_remoteConversationRecordKey != null) { if (_remoteConversationRecordKey != null) {
_initWait.add(() async { _initWait.add(() async {
await _setRemoteConversation(() async { await _setRemoteConversation(() async {
final accountRecordKey = _activeAccountInfo
.userLogin.accountRecordInfo.accountRecord.recordKey;
// Open remote record key if it is specified // Open remote record key if it is specified
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
final crypto = await _cachedConversationCrypto(); final crypto = await _cachedConversationCrypto();
final record = await pool.openRecordRead(_remoteConversationRecordKey, final record = await pool.openRecordRead(_remoteConversationRecordKey,
debugName: 'ConversationCubit::RemoteConversation', debugName: 'ConversationCubit::RemoteConversation',
parent: accountRecordKey, parent: pool.getParentRecordKey(_remoteConversationRecordKey) ??
accountInfo.accountRecordKey,
crypto: crypto); crypto: crypto);
return record; return record;
}); });
@ -80,6 +83,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
@override @override
Future<void> close() async { Future<void> close() async {
await _initWait(); await _initWait();
await _accountSubscription?.cancel();
await _localSubscription?.cancel(); await _localSubscription?.cancel();
await _remoteSubscription?.cancel(); await _remoteSubscription?.cancel();
await _localConversationCubit?.close(); await _localConversationCubit?.close();
@ -88,6 +92,130 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
await super.close(); await super.close();
} }
////////////////////////////////////////////////////////////////////////////
// Public Interface
/// 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
Future<T> initLocalConversation<T>(
{required proto.Profile profile,
required FutureOr<T> Function(DHTRecord) callback,
TypedKey? existingConversationRecordKey}) async {
assert(_localConversationRecordKey == null,
'must not have a local conversation yet');
final pool = DHTRecordPool.instance;
final crypto = await _cachedConversationCrypto();
final accountRecordKey = _accountInfo.accountRecordKey;
final writer = _accountInfo.identityWriter;
// Open with SMPL schema for identity writer
late final DHTRecord localConversationRecord;
if (existingConversationRecordKey != null) {
localConversationRecord = await pool.openRecordWrite(
existingConversationRecordKey, writer,
debugName:
'ConversationCubit::initLocalConversation::LocalConversation',
parent: accountRecordKey,
crypto: crypto);
} else {
localConversationRecord = await pool.createRecord(
debugName:
'ConversationCubit::initLocalConversation::LocalConversation',
parent: accountRecordKey,
crypto: crypto,
writer: writer,
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(
localConversationKey: localConversation.key,
callback: (messages) async {
// Create initial local conversation key contents
final conversation = proto.Conversation()
..profile = profile
..superIdentityJson =
jsonEncode(_accountInfo.localAccount.superIdentity.toJson())
..messages = messages.recordKey.toProto();
// 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));
return out;
});
});
// If success, save the new local conversation record key in this object
_localConversationRecordKey = localConversationRecord.key;
await _setLocalConversation(() async => localConversationRecord);
return out;
}
/// Force refresh of conversation keys
Future<void> refresh() async {
await _initWait();
final lcc = _localConversationCubit;
final rcc = _remoteConversationCubit;
if (lcc != null) {
await lcc.refreshDefault();
}
if (rcc != null) {
await rcc.refreshDefault();
}
}
/// 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
void _updateAccountChange(AsyncValue<proto.Account> avaccount) {
final account = avaccount.asData?.value;
if (account == null) {
return;
}
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) { void _updateLocalConversationState(AsyncValue<proto.Conversation> avconv) {
final newState = avconv.when( final newState = avconv.when(
data: (conv) { data: (conv) {
@ -140,6 +268,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
open: open, decodeState: proto.Conversation.fromBuffer); open: open, decodeState: proto.Conversation.fromBuffer);
_localSubscription = _localSubscription =
_localConversationCubit!.stream.listen(_updateLocalConversationState); _localConversationCubit!.stream.listen(_updateLocalConversationState);
_updateLocalConversationState(_localConversationCubit!.state);
} }
// Open remote converation key // Open remote converation key
@ -150,146 +279,16 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
open: open, decodeState: proto.Conversation.fromBuffer); open: open, decodeState: proto.Conversation.fromBuffer);
_remoteSubscription = _remoteSubscription =
_remoteConversationCubit!.stream.listen(_updateRemoteConversationState); _remoteConversationCubit!.stream.listen(_updateRemoteConversationState);
} _updateRemoteConversationState(_remoteConversationCubit!.state);
Future<bool> delete() async {
final pool = DHTRecordPool.instance;
await _initWait();
final localConversationCubit = _localConversationCubit;
final remoteConversationCubit = _remoteConversationCubit;
final deleteSet = DelayedWaitSet<void>();
if (localConversationCubit != null) {
final data = localConversationCubit.state.asData;
if (data == null) {
log.warning('could not delete local conversation');
return false;
}
deleteSet.add(() async {
_localConversationCubit = null;
await localConversationCubit.close();
final conversation = data.value;
final messagesKey = conversation.messages.toVeilid();
await pool.deleteRecord(messagesKey);
await pool.deleteRecord(_localConversationRecordKey!);
_localConversationRecordKey = null;
});
}
if (remoteConversationCubit != null) {
final data = remoteConversationCubit.state.asData;
if (data == null) {
log.warning('could not delete remote conversation');
return false;
}
deleteSet.add(() async {
_remoteConversationCubit = null;
await remoteConversationCubit.close();
final conversation = data.value;
final messagesKey = conversation.messages.toVeilid();
await pool.deleteRecord(messagesKey);
await pool.deleteRecord(_remoteConversationRecordKey!);
});
}
// Commit the delete futures
await deleteSet();
return true;
}
// 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
Future<T> initLocalConversation<T>(
{required proto.Profile profile,
required FutureOr<T> Function(DHTRecord) callback,
TypedKey? existingConversationRecordKey}) async {
assert(_localConversationRecordKey == null,
'must not have a local conversation yet');
final pool = DHTRecordPool.instance;
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final crypto = await _cachedConversationCrypto();
final writer = _activeAccountInfo.identityWriter;
// Open with SMPL scheme for identity writer
late final DHTRecord localConversationRecord;
if (existingConversationRecordKey != null) {
localConversationRecord = await pool.openRecordWrite(
existingConversationRecordKey, writer,
debugName:
'ConversationCubit::initLocalConversation::LocalConversation',
parent: accountRecordKey,
crypto: crypto);
} else {
localConversationRecord = await pool.createRecord(
debugName:
'ConversationCubit::initLocalConversation::LocalConversation',
parent: accountRecordKey,
crypto: crypto,
writer: writer,
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(
activeAccountInfo: _activeAccountInfo,
remoteIdentityPublicKey: _remoteIdentityPublicKey,
localConversationKey: localConversation.key,
callback: (messages) async {
// Create initial local conversation key contents
final conversation = proto.Conversation()
..profile = profile
..superIdentityJson = jsonEncode(
_activeAccountInfo.localAccount.superIdentity.toJson())
..messages = messages.recordKey.toProto();
// 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));
return out;
});
});
// If success, save the new local conversation record key in this object
_localConversationRecordKey = localConversationRecord.key;
await _setLocalConversation(() async => localConversationRecord);
return out;
} }
// Initialize local messages // Initialize local messages
Future<T> _initLocalMessages<T>({ Future<T> _initLocalMessages<T>({
required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationKey, required TypedKey localConversationKey,
required FutureOr<T> Function(DHTLog) callback, required FutureOr<T> Function(DHTLog) callback,
}) async { }) async {
final crypto = final crypto = await _cachedConversationCrypto();
await activeAccountInfo.makeConversationCrypto(remoteIdentityPublicKey); final writer = _identityWriter;
final writer = activeAccountInfo.identityWriter;
return (await DHTLog.create( return (await DHTLog.create(
debugName: 'ConversationCubit::initLocalMessages::LocalMessages', debugName: 'ConversationCubit::initLocalMessages::LocalMessages',
@ -299,47 +298,23 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
.deleteScope((messages) async => await callback(messages)); .deleteScope((messages) async => await callback(messages));
} }
// Force refresh of conversation keys
Future<void> refresh() async {
await _initWait();
final lcc = _localConversationCubit;
final rcc = _remoteConversationCubit;
if (lcc != null) {
await lcc.refreshDefault();
}
if (rcc != null) {
await rcc.refreshDefault();
}
}
Future<proto.Conversation?> writeLocalConversation({
required proto.Conversation conversation,
}) async {
final update = await _localConversationCubit!.record
.tryWriteProtobuf(proto.Conversation.fromBuffer, conversation);
if (update != null) {
_updateLocalConversationState(AsyncValue.data(conversation));
}
return update;
}
Future<VeilidCrypto> _cachedConversationCrypto() async { Future<VeilidCrypto> _cachedConversationCrypto() async {
var conversationCrypto = _conversationCrypto; var conversationCrypto = _conversationCrypto;
if (conversationCrypto != null) { if (conversationCrypto != null) {
return conversationCrypto; return conversationCrypto;
} }
conversationCrypto = await _activeAccountInfo conversationCrypto =
.makeConversationCrypto(_remoteIdentityPublicKey); await _accountInfo.makeConversationCrypto(_remoteIdentityPublicKey);
_conversationCrypto = conversationCrypto; _conversationCrypto = conversationCrypto;
return conversationCrypto; return conversationCrypto;
} }
final ActiveAccountInfo _activeAccountInfo; ////////////////////////////////////////////////////////////////////////////
// Fields
TypedKey get remoteIdentityPublicKey => _remoteIdentityPublicKey;
final AccountInfo _accountInfo;
late final KeyPair _identityWriter;
final TypedKey _remoteIdentityPublicKey; final TypedKey _remoteIdentityPublicKey;
TypedKey? _localConversationRecordKey; TypedKey? _localConversationRecordKey;
final TypedKey? _remoteConversationRecordKey; final TypedKey? _remoteConversationRecordKey;
@ -347,9 +322,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit; DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit;
StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription; StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription;
StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription; StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription;
StreamSubscription<AsyncValue<proto.Account>>? _accountSubscription;
ConversationState _incrementalState = const ConversationState( ConversationState _incrementalState = const ConversationState(
localConversation: null, remoteConversation: null); localConversation: null, remoteConversation: null);
//
VeilidCrypto? _conversationCrypto; VeilidCrypto? _conversationCrypto;
final WaitSet<void> _initWait = WaitSet(); final WaitSet<void> _initWait = WaitSet();
} }

View file

@ -0,0 +1,3 @@
export 'active_conversations_bloc_map_cubit.dart';
export 'active_single_contact_chat_bloc_map_cubit.dart';
export 'conversation_cubit.dart';

View file

@ -0,0 +1,37 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
class ActiveAccountPageControllerWrapper {
ActiveAccountPageControllerWrapper(Locator locator, int initialPage) {
pageController = PageController(initialPage: initialPage, keepPage: false);
final activeLocalAccountCubit = locator<ActiveLocalAccountCubit>();
_subscription =
activeLocalAccountCubit.stream.listen((activeLocalAccountRecordKey) {
singleFuture(this, () async {
final localAccounts = locator<LocalAccountsCubit>().state;
final activeIndex = localAccounts.indexWhere(
(x) => x.superIdentity.recordKey == activeLocalAccountRecordKey);
if (pageController.page == activeIndex) {
return;
}
await pageController.animateToPage(activeIndex,
duration: const Duration(milliseconds: 250),
curve: Curves.fastOutSlowIn);
});
});
}
void dispose() {
unawaited(_subscription.cancel());
}
late PageController pageController;
late StreamSubscription<TypedKey?> _subscription;
}

View file

@ -0,0 +1,316 @@
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart';
import '../../../proto/proto.dart' as proto;
import '../../../theme/theme.dart';
import '../../../tools/tools.dart';
import '../../../veilid_processor/veilid_processor.dart';
import 'menu_item_widget.dart';
class DrawerMenu extends StatefulWidget {
const DrawerMenu({super.key});
@override
State createState() => _DrawerMenuState();
}
class _DrawerMenuState extends State<DrawerMenu> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
void _doSwitchClick(TypedKey superIdentityRecordKey) {
singleFuture(this, () async {
await AccountRepository.instance.switchToAccount(superIdentityRecordKey);
});
}
void _doEditClick(
TypedKey superIdentityRecordKey, proto.Profile existingProfile) {
singleFuture(this, () async {
await GoRouterHelper(context).push('/edit_account',
extra: [superIdentityRecordKey, existingProfile]);
});
}
Widget _wrapInBox({required Widget child, required Color color}) =>
DecoratedBox(
decoration: ShapeDecoration(
color: color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16))),
child: child);
Widget _makeAccountWidget(
{required String name,
required bool selected,
required ScaleColor scale,
required bool loggedIn,
required void Function()? callback,
required void Function()? footerCallback}) {
final theme = Theme.of(context);
final abbrev = name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join();
late final String shortname;
if (abbrev.length >= 3) {
shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1];
} else {
shortname = abbrev;
}
final avatar = Container(
height: 34,
width: 34,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: loggedIn ? scale.border : scale.subtleBorder,
width: 2,
strokeAlign: BorderSide.strokeAlignOutside),
color: Colors.blue,
),
child: AvatarImage(
//size: 32,
backgroundColor: loggedIn ? scale.primary : scale.elementBackground,
foregroundColor: loggedIn ? scale.primaryText : scale.subtleText,
child: Text(shortname, style: theme.textTheme.titleLarge)));
return AnimatedPadding(
padding: EdgeInsets.fromLTRB(selected ? 0 : 0, 0, selected ? 0 : 8, 0),
duration: const Duration(milliseconds: 50),
child: MenuItemWidget(
title: name,
headerWidget: avatar,
titleStyle: theme.textTheme.titleLarge!,
foregroundColor: scale.primary,
backgroundColor: selected
? scale.activeElementBackground
: scale.elementBackground,
backgroundHoverColor: scale.hoverElementBackground,
backgroundFocusColor: scale.activeElementBackground,
borderColor: scale.border,
borderHoverColor: scale.hoverBorder,
borderFocusColor: scale.primary,
callback: callback,
footerButtonIcon: loggedIn ? Icons.edit_outlined : null,
footerCallback: footerCallback,
footerButtonIconColor: scale.border,
footerButtonIconHoverColor: scale.hoverElementBackground,
footerButtonIconFocusColor: scale.activeElementBackground,
));
}
Widget _getAccountList(
{required IList<LocalAccount> localAccounts,
required TypedKey? activeLocalAccount,
required PerAccountCollectionBlocMapState
perAccountCollectionBlocMapState}) {
final theme = Theme.of(context);
final scaleScheme = theme.extension<ScaleScheme>()!;
final loggedInAccounts = <Widget>[];
final loggedOutAccounts = <Widget>[];
for (final la in localAccounts) {
final superIdentityRecordKey = la.superIdentity.recordKey;
// See if this account is logged in
final avAccountRecordState = perAccountCollectionBlocMapState
.get(superIdentityRecordKey)
?.avAccountRecordState;
if (avAccountRecordState != null) {
// Account is logged in
final scale = theme.extension<ScaleScheme>()!.tertiaryScale;
final loggedInAccount = avAccountRecordState.when(
data: (value) => _makeAccountWidget(
name: value.profile.name,
scale: scale,
selected: superIdentityRecordKey == activeLocalAccount,
loggedIn: true,
callback: () {
_doSwitchClick(superIdentityRecordKey);
},
footerCallback: () {
_doEditClick(superIdentityRecordKey, value.profile);
}),
loading: () => _wrapInBox(
child: buildProgressIndicator(),
color: scaleScheme.grayScale.subtleBorder),
error: (err, st) => _wrapInBox(
child: errorPage(err, st),
color: scaleScheme.errorScale.subtleBorder),
);
loggedInAccounts.add(loggedInAccount.paddingLTRB(0, 0, 0, 8));
} else {
// Account is not logged in
final scale = theme.extension<ScaleScheme>()!.grayScale;
final loggedOutAccount = _makeAccountWidget(
name: la.name,
scale: scale,
selected: superIdentityRecordKey == activeLocalAccount,
loggedIn: false,
callback: () => {_doSwitchClick(superIdentityRecordKey)},
footerCallback: null,
);
loggedOutAccounts.add(loggedOutAccount);
}
}
// Assemble main menu
final mainMenu = <Widget>[...loggedInAccounts, ...loggedOutAccounts];
// Return main menu widgets
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[...mainMenu],
);
}
Widget _getButton(
{required Icon icon,
required ScaleColor scale,
required String tooltip,
required void Function()? onPressed}) =>
IconButton(
icon: icon,
color: scale.hoverBorder,
constraints: const BoxConstraints.expand(height: 64, width: 64),
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return scale.hoverElementBackground;
}
if (states.contains(WidgetState.focused)) {
return scale.activeElementBackground;
}
return scale.elementBackground;
}), shape: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return RoundedRectangleBorder(
side: BorderSide(color: scale.hoverBorder),
borderRadius: const BorderRadius.all(Radius.circular(16)));
}
if (states.contains(WidgetState.focused)) {
return RoundedRectangleBorder(
side: BorderSide(color: scale.primary),
borderRadius: const BorderRadius.all(Radius.circular(16)));
}
return RoundedRectangleBorder(
side: BorderSide(color: scale.border),
borderRadius: const BorderRadius.all(Radius.circular(16)));
})),
tooltip: tooltip,
onPressed: onPressed);
Widget _getBottomButtons() {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final settingsButton = _getButton(
icon: const Icon(Icons.settings),
tooltip: translate('menu.settings_tooltip'),
scale: scale.tertiaryScale,
onPressed: () async {
await GoRouterHelper(context).push('/settings');
}).paddingLTRB(0, 0, 16, 0);
final addButton = _getButton(
icon: const Icon(Icons.add),
tooltip: translate('menu.add_account_tooltip'),
scale: scale.tertiaryScale,
onPressed: () async {
await GoRouterHelper(context).push('/new_account');
}).paddingLTRB(0, 0, 16, 0);
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [settingsButton, addButton]).paddingLTRB(0, 16, 0, 0);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
//final textTheme = theme.textTheme;
final localAccounts = context.watch<LocalAccountsCubit>().state;
final perAccountCollectionBlocMapState =
context.watch<PerAccountCollectionBlocMapCubit>().state;
final activeLocalAccount = context.watch<ActiveLocalAccountCubit>().state;
final gradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
scale.tertiaryScale.hoverElementBackground,
scale.tertiaryScale.subtleBackground,
]);
return DecoratedBox(
decoration: ShapeDecoration(
shadows: [
BoxShadow(
color: scale.tertiaryScale.appBackground,
blurRadius: 6,
offset: const Offset(
0,
3,
),
),
],
gradient: gradient,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(16),
bottomRight: Radius.circular(16)))),
child: Column(children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Row(children: [
SvgPicture.asset(
height: 48,
'assets/images/icon.svg',
colorFilter: scaleConfig.useVisualIndicators
? grayColorFilter
: null)
.paddingLTRB(0, 0, 16, 0),
SvgPicture.asset(
height: 48,
'assets/images/title.svg',
colorFilter:
scaleConfig.useVisualIndicators ? grayColorFilter : null),
])),
const Spacer(),
_getAccountList(
localAccounts: localAccounts,
activeLocalAccount: activeLocalAccount,
perAccountCollectionBlocMapState: perAccountCollectionBlocMapState),
_getBottomButtons(),
const Spacer(),
Row(children: [
Text('Version $packageInfoVersion',
style: theme.textTheme.labelMedium!
.copyWith(color: scale.tertiaryScale.hoverBorder)),
const Spacer(),
SignalStrengthMeterWidget(
color: scale.tertiaryScale.hoverBorder,
inactiveColor: scale.tertiaryScale.border,
),
])
]).paddingAll(16),
);
}
}

View file

@ -0,0 +1,130 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class MenuItemWidget extends StatelessWidget {
const MenuItemWidget({
required this.title,
required this.titleStyle,
required this.foregroundColor,
this.headerWidget,
this.widthBox,
this.callback,
this.backgroundColor,
this.backgroundHoverColor,
this.backgroundFocusColor,
this.borderColor,
this.borderHoverColor,
this.borderFocusColor,
this.footerButtonIcon,
this.footerButtonIconColor,
this.footerButtonIconHoverColor,
this.footerButtonIconFocusColor,
this.footerCallback,
super.key,
});
@override
Widget build(BuildContext context) => TextButton(
onPressed: callback,
style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith(
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return backgroundHoverColor;
}
if (states.contains(WidgetState.focused)) {
return backgroundFocusColor;
}
return backgroundColor;
}),
side: WidgetStateBorderSide.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return borderColor != null
? BorderSide(color: borderHoverColor!)
: null;
}
if (states.contains(WidgetState.focused)) {
return borderColor != null
? BorderSide(color: borderFocusColor!)
: null;
}
return borderColor != null ? BorderSide(color: borderColor!) : null;
}),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)))),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (headerWidget != null) headerWidget!,
if (widthBox != null) widthBox!,
Expanded(
child: FittedBox(
alignment: Alignment.centerLeft,
fit: BoxFit.scaleDown,
child: Text(
title,
style: titleStyle,
).paddingAll(8)),
),
if (footerButtonIcon != null)
IconButton.outlined(
color: footerButtonIconColor,
focusColor: footerButtonIconFocusColor,
hoverColor: footerButtonIconHoverColor,
icon: Icon(
footerButtonIcon,
size: 24,
),
onPressed: footerCallback),
],
),
));
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<TextStyle?>('textStyle', titleStyle))
..add(ObjectFlagProperty<void Function()?>.has('callback', callback))
..add(DiagnosticsProperty<Color>('foregroundColor', foregroundColor))
..add(StringProperty('title', title))
..add(
DiagnosticsProperty<IconData?>('footerButtonIcon', footerButtonIcon))
..add(ObjectFlagProperty<void Function()?>.has(
'footerCallback', footerCallback))
..add(ColorProperty('footerButtonIconColor', footerButtonIconColor))
..add(ColorProperty(
'footerButtonIconHoverColor', footerButtonIconHoverColor))
..add(ColorProperty(
'footerButtonIconFocusColor', footerButtonIconFocusColor))
..add(ColorProperty('backgroundColor', backgroundColor))
..add(ColorProperty('backgroundHoverColor', backgroundHoverColor))
..add(ColorProperty('backgroundFocusColor', backgroundFocusColor))
..add(ColorProperty('borderColor', borderColor))
..add(ColorProperty('borderHoverColor', borderHoverColor))
..add(ColorProperty('borderFocusColor', borderFocusColor));
}
////////////////////////////////////////////////////////////////////////////
final String title;
final Widget? headerWidget;
final Widget? widthBox;
final TextStyle titleStyle;
final Color foregroundColor;
final void Function()? callback;
final IconData? footerButtonIcon;
final void Function()? footerCallback;
final Color? backgroundColor;
final Color? backgroundHoverColor;
final Color? backgroundFocusColor;
final Color? borderColor;
final Color? borderHoverColor;
final Color? borderFocusColor;
final Color? footerButtonIconColor;
final Color? footerButtonIconHoverColor;
final Color? footerButtonIconFocusColor;
}

View file

@ -1,6 +1,8 @@
export 'active_account_page_controller_wrapper.dart';
export 'drawer_menu/drawer_menu.dart';
export 'home_account_invalid.dart'; export 'home_account_invalid.dart';
export 'home_account_locked.dart'; export 'home_account_locked.dart';
export 'home_account_missing.dart'; export 'home_account_missing.dart';
export 'home_account_ready/home_account_ready.dart'; export 'home_account_ready/home_account_ready.dart';
export 'home_no_active.dart'; export 'home_no_active.dart';
export 'home_shell.dart'; export 'home_screen.dart';

View file

@ -21,13 +21,3 @@ class HomeAccountMissingState extends State<HomeAccountMissing> {
@override @override
Widget build(BuildContext context) => const Text('Account missing'); Widget build(BuildContext context) => const Text('Account missing');
} }
// xxx click to delete missing account or add to postframecallback
// Future.delayed(0.ms, () async {
// await showErrorModal(context, translate('home.missing_account_title'),
// translate('home.missing_account_text'));
// // Delete account
// await AccountRepository.instance.deleteLocalAccount(activeUserLogin);
// // Switch to no active user login
// await AccountRepository.instance.switchToAccount(null);
// });

View file

@ -1,3 +1,2 @@
export 'home_account_ready_chat.dart'; export 'home_account_ready_chat.dart';
export 'home_account_ready_main.dart'; export 'home_account_ready_main.dart';
export 'home_account_ready_shell.dart';

View file

@ -2,10 +2,11 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
import '../../../account_manager/account_manager.dart'; import '../../../account_manager/account_manager.dart';
import '../../../chat/chat.dart'; import '../../../chat/chat.dart';
import '../../../proto/proto.dart' as proto;
import '../../../theme/theme.dart'; import '../../../theme/theme.dart';
import '../../../tools/tools.dart'; import '../../../tools/tools.dart';
import 'main_pager/main_pager.dart'; import 'main_pager/main_pager.dart';
@ -29,14 +30,15 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
} }
Widget buildUserPanel() => Builder(builder: (context) { Widget buildUserPanel() => Builder(builder: (context) {
final account = context.watch<AccountRecordCubit>().state; final profile = context.select<AccountRecordCubit, proto.Profile>(
(c) => c.state.asData!.value.profile);
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
return Column(children: <Widget>[ return Column(children: <Widget>[
Row(children: [ Row(children: [
IconButton( IconButton(
icon: const Icon(Icons.settings), icon: const Icon(Icons.menu),
color: scale.secondaryScale.borderText, color: scale.secondaryScale.borderText,
constraints: const BoxConstraints.expand(height: 64, width: 64), constraints: const BoxConstraints.expand(height: 64, width: 64),
style: ButtonStyle( style: ButtonStyle(
@ -44,13 +46,13 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
WidgetStateProperty.all(scale.primaryScale.hoverBorder), WidgetStateProperty.all(scale.primaryScale.hoverBorder),
shape: WidgetStateProperty.all(const RoundedRectangleBorder( shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16))))), borderRadius: BorderRadius.all(Radius.circular(16))))),
tooltip: translate('app_bar.settings_tooltip'), tooltip: translate('menu.settings_tooltip'),
onPressed: () async { onPressed: () async {
await GoRouterHelper(context).push('/settings'); final ctrl = context.read<ZoomDrawerController>();
await ctrl.toggle?.call();
//await GoRouterHelper(context).push('/settings');
}).paddingLTRB(0, 0, 8, 0), }).paddingLTRB(0, 0, 8, 0),
asyncValueBuilder(account, ProfileWidget(profile: profile).expanded(),
(_, account) => ProfileWidget(profile: account.profile))
.expanded(),
]).paddingAll(8), ]).paddingAll(8),
const MainPager().expanded() const MainPager().expanded()
]); ]);
@ -71,7 +73,7 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
} }
return ChatComponentWidget.builder( return ChatComponentWidget.builder(
localConversationRecordKey: activeChatLocalConversationKey, localConversationRecordKey: activeChatLocalConversationKey,
); key: ValueKey(activeChatLocalConversationKey));
} }
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies

View file

@ -1,159 +0,0 @@
import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart';
import '../../../chat/chat.dart';
import '../../../chat_list/chat_list.dart';
import '../../../contact_invitation/contact_invitation.dart';
import '../../../contacts/contacts.dart';
import '../../../router/router.dart';
import '../../../theme/theme.dart';
class HomeAccountReadyShell extends StatefulWidget {
factory HomeAccountReadyShell(
{required BuildContext context, required Widget child, Key? key}) {
// These must exist in order for the account to
// be considered 'ready' for this widget subtree
final activeLocalAccount = context.read<ActiveLocalAccountCubit>().state!;
final activeAccountInfo = context.read<ActiveAccountInfo>();
final routerCubit = context.read<RouterCubit>();
return HomeAccountReadyShell._(
activeLocalAccount: activeLocalAccount,
activeAccountInfo: activeAccountInfo,
routerCubit: routerCubit,
key: key,
child: child);
}
const HomeAccountReadyShell._(
{required this.activeLocalAccount,
required this.activeAccountInfo,
required this.routerCubit,
required this.child,
super.key});
@override
HomeAccountReadyShellState createState() => HomeAccountReadyShellState();
final Widget child;
final TypedKey activeLocalAccount;
final ActiveAccountInfo activeAccountInfo;
final RouterCubit routerCubit;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<TypedKey>(
'activeLocalAccount', activeLocalAccount))
..add(DiagnosticsProperty<ActiveAccountInfo>(
'activeAccountInfo', activeAccountInfo))
..add(DiagnosticsProperty<RouterCubit>('routerCubit', routerCubit));
}
}
class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
final SingleStateProcessor<WaitingInvitationsBlocMapState>
_singleInvitationStatusProcessor = SingleStateProcessor();
@override
void initState() {
super.initState();
}
// Process all accepted or rejected invitations
void _invitationStatusListener(
BuildContext context, WaitingInvitationsBlocMapState state) {
_singleInvitationStatusProcessor.updateState(state, (newState) async {
final contactListCubit = context.read<ContactListCubit>();
final contactInvitationListCubit =
context.read<ContactInvitationListCubit>();
for (final entry in newState.entries) {
final contactRequestInboxRecordKey = entry.key;
final invStatus = entry.value.asData?.value;
// Skip invitations that have not yet been accepted or rejected
if (invStatus == null) {
continue;
}
// Delete invitation and process the accepted or rejected contact
final acceptedContact = invStatus.acceptedContact;
if (acceptedContact != null) {
await contactInvitationListCubit.deleteInvitation(
accepted: true,
contactRequestInboxRecordKey: contactRequestInboxRecordKey);
// Accept
await contactListCubit.createContact(
remoteProfile: acceptedContact.remoteProfile,
remoteSuperIdentity: acceptedContact.remoteIdentity,
remoteConversationRecordKey:
acceptedContact.remoteConversationRecordKey,
localConversationRecordKey:
acceptedContact.localConversationRecordKey,
);
} else {
// Reject
await contactInvitationListCubit.deleteInvitation(
accepted: false,
contactRequestInboxRecordKey: contactRequestInboxRecordKey);
}
}
});
}
@override
Widget build(BuildContext context) {
final account = context.watch<AccountRecordCubit>().state.asData?.value;
if (account == null) {
return waitingPage();
}
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => ContactInvitationListCubit(
activeAccountInfo: widget.activeAccountInfo,
account: account)),
BlocProvider(
create: (context) => ContactListCubit(
activeAccountInfo: widget.activeAccountInfo,
account: account)),
BlocProvider(
create: (context) => ActiveChatCubit(null)
..withStateListen((event) {
widget.routerCubit.setHasActiveChat(event != null);
})),
BlocProvider(
create: (context) => ChatListCubit(
activeAccountInfo: widget.activeAccountInfo,
activeChatCubit: context.read<ActiveChatCubit>(),
account: account)),
BlocProvider(
create: (context) => ActiveConversationsBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo,
contactListCubit: context.read<ContactListCubit>())
..follow(context.read<ChatListCubit>())),
BlocProvider(
create: (context) => ActiveSingleContactChatBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo,
contactListCubit: context.read<ContactListCubit>(),
chatListCubit: context.read<ChatListCubit>())
..follow(context.read<ActiveConversationsBlocMapCubit>())),
BlocProvider(
create: (context) => WaitingInvitationsBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo, account: account)
..follow(context.read<ContactInvitationListCubit>()))
],
child: MultiBlocListener(listeners: [
BlocListener<WaitingInvitationsBlocMapCubit,
WaitingInvitationsBlocMapState>(
listener: _invitationStatusListener,
)
], child: widget.child));
}
}

View file

@ -25,7 +25,7 @@ class ChatsPageState extends State<ChatsPage> {
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column(children: <Widget>[ return Column(children: <Widget>[
const ChatSingleContactListWidget().expanded(), const ChatListWidget().expanded(),
]); ]);
} }
} }

View file

@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:preload_page_view/preload_page_view.dart'; import 'package:preload_page_view/preload_page_view.dart';
import 'package:provider/provider.dart';
import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
import '../../../../chat/chat.dart'; import '../../../../chat/chat.dart';
@ -117,7 +118,7 @@ class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
style: TextStyle(fontSize: 24), style: TextStyle(fontSize: 24),
), ),
content: ScanInvitationDialog( content: ScanInvitationDialog(
modalContext: context, locator: context.read,
)); ));
}); });
} }

View file

@ -0,0 +1,160 @@
import 'dart:math';
import 'package:async_tools/async_tools.dart';
import 'package:flutter/material.dart';
import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../chat/chat.dart';
import '../../theme/theme.dart';
import '../../tools/tools.dart';
import 'active_account_page_controller_wrapper.dart';
import 'drawer_menu/drawer_menu.dart';
import 'home_account_invalid.dart';
import 'home_account_locked.dart';
import 'home_account_missing.dart';
import 'home_account_ready/home_account_ready.dart';
import 'home_no_active.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
HomeScreenState createState() => HomeScreenState();
}
class HomeScreenState extends State<HomeScreen> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
Widget _buildAccountReadyDeviceSpecific(BuildContext context) {
final hasActiveChat = context.watch<ActiveChatCubit>().state != null;
if (responsiveVisibility(
context: context,
tablet: false,
tabletLandscape: false,
desktop: false)) {
if (hasActiveChat) {
return const HomeAccountReadyChat();
}
}
return const HomeAccountReadyMain();
}
Widget _buildAccount(BuildContext context, TypedKey superIdentityRecordKey,
PerAccountCollectionState perAccountCollectionState) {
switch (perAccountCollectionState.accountInfo.status) {
case AccountInfoStatus.accountInvalid:
return const HomeAccountInvalid();
case AccountInfoStatus.accountLocked:
return const HomeAccountLocked();
case AccountInfoStatus.accountUnlocked:
// Are we ready to render?
if (!perAccountCollectionState.isReady) {
return waitingPage();
}
// Re-export all ready blocs to the account display subtree
return perAccountCollectionState.provide(
child: Builder(builder: _buildAccountReadyDeviceSpecific));
}
}
Widget _buildAccountPageView(BuildContext context) {
final localAccounts = context.watch<LocalAccountsCubit>().state;
final activeLocalAccount = context.watch<ActiveLocalAccountCubit>().state;
final perAccountCollectionBlocMapState =
context.watch<PerAccountCollectionBlocMapCubit>().state;
final activeIndex = localAccounts
.indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount);
if (activeIndex == -1) {
return const HomeNoActive();
}
return Provider<ActiveAccountPageControllerWrapper>(
lazy: false,
create: (context) =>
ActiveAccountPageControllerWrapper(context.read, activeIndex),
dispose: (context, value) {
value.dispose();
},
child: Builder(
builder: (context) => PageView.builder(
onPageChanged: (idx) {
singleFuture(this, () async {
await AccountRepository.instance.switchToAccount(
localAccounts[idx].superIdentity.recordKey);
});
},
controller: context
.read<ActiveAccountPageControllerWrapper>()
.pageController,
itemCount: localAccounts.length,
itemBuilder: (context, index) {
final superIdentityRecordKey =
localAccounts[index].superIdentity.recordKey;
final perAccountCollectionState =
perAccountCollectionBlocMapState
.get(superIdentityRecordKey);
if (perAccountCollectionState == null) {
return HomeAccountMissing(
key: ValueKey(superIdentityRecordKey));
}
return _buildAccount(context, superIdentityRecordKey,
perAccountCollectionState);
})));
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final gradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
scale.tertiaryScale.subtleBackground,
scale.tertiaryScale.appBackground,
]);
return SafeArea(
child: DecoratedBox(
decoration: BoxDecoration(gradient: gradient),
child: ZoomDrawer(
controller: _zoomDrawerController,
//menuBackgroundColor: Colors.transparent,
menuScreen: const DrawerMenu(),
mainScreen: DecoratedBox(
decoration: BoxDecoration(
color: scale.primaryScale.activeElementBackground),
child: Provider<ZoomDrawerController>.value(
value: _zoomDrawerController,
child: Builder(builder: _buildAccountPageView))),
borderRadius: 24,
showShadow: true,
angle: 0,
drawerShadowsBackgroundColor: theme.shadowColor,
mainScreenOverlayColor: theme.shadowColor.withAlpha(0x3F),
openCurve: Curves.fastEaseInToSlowEaseOut,
// duration: const Duration(milliseconds: 250),
// reverseDuration: const Duration(milliseconds: 250),
menuScreenTapClose: true,
mainScreenTapClose: true,
mainScreenScale: .25,
slideWidth: min(360, MediaQuery.of(context).size.width * 0.9),
)));
}
final _zoomDrawerController = ZoomDrawerController();
}

View file

@ -1,74 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import '../../account_manager/account_manager.dart';
import '../../theme/theme.dart';
import 'home_account_invalid.dart';
import 'home_account_locked.dart';
import 'home_account_missing.dart';
import 'home_no_active.dart';
class HomeShell extends StatefulWidget {
const HomeShell({required this.accountReadyBuilder, super.key});
@override
HomeShellState createState() => HomeShellState();
final Builder accountReadyBuilder;
}
class HomeShellState extends State<HomeShell> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
Widget buildWithLogin(BuildContext context) {
final activeLocalAccount = context.watch<ActiveLocalAccountCubit>().state;
if (activeLocalAccount == null) {
// If no logged in user is active, show the loading panel
return const HomeNoActive();
}
final accountInfo =
AccountRepository.instance.getAccountInfo(activeLocalAccount);
switch (accountInfo.status) {
case AccountInfoStatus.noAccount:
return const HomeAccountMissing();
case AccountInfoStatus.accountInvalid:
return const HomeAccountInvalid();
case AccountInfoStatus.accountLocked:
return const HomeAccountLocked();
case AccountInfoStatus.accountReady:
return Provider<ActiveAccountInfo>.value(
value: accountInfo.activeAccountInfo!,
child: BlocProvider(
create: (context) => AccountRecordCubit(
open: () async => AccountRepository.instance
.openAccountRecord(
accountInfo.activeAccountInfo!.userLogin)),
child: widget.accountReadyBuilder));
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
// XXX: eventually write account switcher here
return SafeArea(
child: DecoratedBox(
decoration: BoxDecoration(
color: scale.primaryScale.activeElementBackground),
child: buildWithLogin(context)));
}
}

View file

@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:stack_trace/stack_trace.dart'; import 'package:stack_trace/stack_trace.dart';
import 'app.dart'; import 'app.dart';
@ -45,6 +46,9 @@ void main() async {
fallbackLocale: 'en_US', supportedLocales: ['en_US']); fallbackLocale: 'en_US', supportedLocales: ['en_US']);
await initializeDateFormatting(); await initializeDateFormatting();
// Get package info
await initPackageInfo();
// Run the app // Run the app
// Hot reloads will only restart this part, not Veilid // Hot reloads will only restart this part, not Veilid
runApp(LocalizedApp(localizationDelegate, runApp(LocalizedApp(localizationDelegate,

View file

@ -29,3 +29,21 @@ extension MessageExt on proto.Message {
static int compareTimestamp(proto.Message a, proto.Message b) => static int compareTimestamp(proto.Message a, proto.Message b) =>
a.timestamp.compareTo(b.timestamp); a.timestamp.compareTo(b.timestamp);
} }
extension ContactExt on proto.Contact {
String get displayName =>
nickname.isNotEmpty ? '$nickname (${profile.name})' : profile.name;
}
extension ChatExt on proto.Chat {
TypedKey get localConversationRecordKey {
switch (whichKind()) {
case proto.Chat_Kind.direct:
return direct.localConversationRecordKey.toVeilid();
case proto.Chat_Kind.group:
return group.localConversationRecordKey.toVeilid();
case proto.Chat_Kind.notSet:
throw StateError('unknown chat kind');
}
}
}

View file

@ -20,6 +20,177 @@ import 'veilidchat.pbenum.dart';
export 'veilidchat.pbenum.dart'; export 'veilidchat.pbenum.dart';
class DHTDataReference extends $pb.GeneratedMessage {
factory DHTDataReference() => create();
DHTDataReference._() : super();
factory DHTDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory DHTDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DHTDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'hash', subBuilder: $0.TypedKey.create)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
DHTDataReference clone() => DHTDataReference()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
DHTDataReference copyWith(void Function(DHTDataReference) updates) => super.copyWith((message) => updates(message as DHTDataReference)) as DHTDataReference;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static DHTDataReference create() => DHTDataReference._();
DHTDataReference createEmptyInstance() => create();
static $pb.PbList<DHTDataReference> createRepeated() => $pb.PbList<DHTDataReference>();
@$core.pragma('dart2js:noInline')
static DHTDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DHTDataReference>(create);
static DHTDataReference? _defaultInstance;
@$pb.TagNumber(1)
$0.TypedKey get dhtData => $_getN(0);
@$pb.TagNumber(1)
set dhtData($0.TypedKey v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasDhtData() => $_has(0);
@$pb.TagNumber(1)
void clearDhtData() => clearField(1);
@$pb.TagNumber(1)
$0.TypedKey ensureDhtData() => $_ensure(0);
@$pb.TagNumber(2)
$0.TypedKey get hash => $_getN(1);
@$pb.TagNumber(2)
set hash($0.TypedKey v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasHash() => $_has(1);
@$pb.TagNumber(2)
void clearHash() => clearField(2);
@$pb.TagNumber(2)
$0.TypedKey ensureHash() => $_ensure(1);
}
class BlockStoreDataReference extends $pb.GeneratedMessage {
factory BlockStoreDataReference() => create();
BlockStoreDataReference._() : super();
factory BlockStoreDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory BlockStoreDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BlockStoreDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'block', subBuilder: $0.TypedKey.create)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
BlockStoreDataReference clone() => BlockStoreDataReference()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
BlockStoreDataReference copyWith(void Function(BlockStoreDataReference) updates) => super.copyWith((message) => updates(message as BlockStoreDataReference)) as BlockStoreDataReference;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static BlockStoreDataReference create() => BlockStoreDataReference._();
BlockStoreDataReference createEmptyInstance() => create();
static $pb.PbList<BlockStoreDataReference> createRepeated() => $pb.PbList<BlockStoreDataReference>();
@$core.pragma('dart2js:noInline')
static BlockStoreDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BlockStoreDataReference>(create);
static BlockStoreDataReference? _defaultInstance;
@$pb.TagNumber(1)
$0.TypedKey get block => $_getN(0);
@$pb.TagNumber(1)
set block($0.TypedKey v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasBlock() => $_has(0);
@$pb.TagNumber(1)
void clearBlock() => clearField(1);
@$pb.TagNumber(1)
$0.TypedKey ensureBlock() => $_ensure(0);
}
enum DataReference_Kind {
dhtData,
blockStoreData,
notSet
}
class DataReference extends $pb.GeneratedMessage {
factory DataReference() => create();
DataReference._() : super();
factory DataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory DataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static const $core.Map<$core.int, DataReference_Kind> _DataReference_KindByTag = {
1 : DataReference_Kind.dhtData,
2 : DataReference_Kind.blockStoreData,
0 : DataReference_Kind.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..oo(0, [1, 2])
..aOM<DHTDataReference>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: DHTDataReference.create)
..aOM<BlockStoreDataReference>(2, _omitFieldNames ? '' : 'blockStoreData', subBuilder: BlockStoreDataReference.create)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
DataReference clone() => DataReference()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
DataReference copyWith(void Function(DataReference) updates) => super.copyWith((message) => updates(message as DataReference)) as DataReference;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static DataReference create() => DataReference._();
DataReference createEmptyInstance() => create();
static $pb.PbList<DataReference> createRepeated() => $pb.PbList<DataReference>();
@$core.pragma('dart2js:noInline')
static DataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DataReference>(create);
static DataReference? _defaultInstance;
DataReference_Kind whichKind() => _DataReference_KindByTag[$_whichOneof(0)]!;
void clearKind() => clearField($_whichOneof(0));
@$pb.TagNumber(1)
DHTDataReference get dhtData => $_getN(0);
@$pb.TagNumber(1)
set dhtData(DHTDataReference v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasDhtData() => $_has(0);
@$pb.TagNumber(1)
void clearDhtData() => clearField(1);
@$pb.TagNumber(1)
DHTDataReference ensureDhtData() => $_ensure(0);
@$pb.TagNumber(2)
BlockStoreDataReference get blockStoreData => $_getN(1);
@$pb.TagNumber(2)
set blockStoreData(BlockStoreDataReference v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasBlockStoreData() => $_has(1);
@$pb.TagNumber(2)
void clearBlockStoreData() => clearField(2);
@$pb.TagNumber(2)
BlockStoreDataReference ensureBlockStoreData() => $_ensure(1);
}
enum Attachment_Kind { enum Attachment_Kind {
media, media,
notSet notSet
@ -98,7 +269,7 @@ class AttachmentMedia extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AttachmentMedia', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AttachmentMedia', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'mime') ..aOS(1, _omitFieldNames ? '' : 'mime')
..aOS(2, _omitFieldNames ? '' : 'name') ..aOS(2, _omitFieldNames ? '' : 'name')
..aOM<$1.DataReference>(3, _omitFieldNames ? '' : 'content', subBuilder: $1.DataReference.create) ..aOM<DataReference>(3, _omitFieldNames ? '' : 'content', subBuilder: DataReference.create)
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -142,15 +313,15 @@ class AttachmentMedia extends $pb.GeneratedMessage {
void clearName() => clearField(2); void clearName() => clearField(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$1.DataReference get content => $_getN(2); DataReference get content => $_getN(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
set content($1.DataReference v) { setField(3, v); } set content(DataReference v) { setField(3, v); }
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.bool hasContent() => $_has(2); $core.bool hasContent() => $_has(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
void clearContent() => clearField(3); void clearContent() => clearField(3);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$1.DataReference ensureContent() => $_ensure(2); DataReference ensureContent() => $_ensure(2);
} }
class Permissions extends $pb.GeneratedMessage { class Permissions extends $pb.GeneratedMessage {
@ -276,7 +447,7 @@ class ChatSettings extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatSettings', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatSettings', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'title') ..aOS(1, _omitFieldNames ? '' : 'title')
..aOS(2, _omitFieldNames ? '' : 'description') ..aOS(2, _omitFieldNames ? '' : 'description')
..aOM<$1.DataReference>(3, _omitFieldNames ? '' : 'icon', subBuilder: $1.DataReference.create) ..aOM<DataReference>(3, _omitFieldNames ? '' : 'icon', subBuilder: DataReference.create)
..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'defaultExpiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'defaultExpiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -321,15 +492,15 @@ class ChatSettings extends $pb.GeneratedMessage {
void clearDescription() => clearField(2); void clearDescription() => clearField(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$1.DataReference get icon => $_getN(2); DataReference get icon => $_getN(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
set icon($1.DataReference v) { setField(3, v); } set icon(DataReference v) { setField(3, v); }
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.bool hasIcon() => $_has(2); $core.bool hasIcon() => $_has(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
void clearIcon() => clearField(3); void clearIcon() => clearField(3);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$1.DataReference ensureIcon() => $_ensure(2); DataReference ensureIcon() => $_ensure(2);
@$pb.TagNumber(4) @$pb.TagNumber(4)
$fixnum.Int64 get defaultExpiration => $_getI64(3); $fixnum.Int64 get defaultExpiration => $_getI64(3);
@ -1084,16 +1255,15 @@ class Conversation extends $pb.GeneratedMessage {
$0.TypedKey ensureMessages() => $_ensure(2); $0.TypedKey ensureMessages() => $_ensure(2);
} }
class Chat extends $pb.GeneratedMessage { class ChatMember extends $pb.GeneratedMessage {
factory Chat() => create(); factory ChatMember() => create();
Chat._() : super(); ChatMember._() : super();
factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory ChatMember.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); factory ChatMember.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatMember', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'remoteIdentityPublicKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -1101,22 +1271,79 @@ class Chat extends $pb.GeneratedMessage {
'Using this can add significant overhead to your binary. ' 'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version') 'Will be removed in next major version')
Chat clone() => Chat()..mergeFromMessage(this); ChatMember clone() => ChatMember()..mergeFromMessage(this);
@$core.Deprecated( @$core.Deprecated(
'Using this can add significant overhead to your binary. ' 'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version') 'Will be removed in next major version')
Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat; ChatMember copyWith(void Function(ChatMember) updates) => super.copyWith((message) => updates(message as ChatMember)) as ChatMember;
$pb.BuilderInfo get info_ => _i; $pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline') @$core.pragma('dart2js:noInline')
static Chat create() => Chat._(); static ChatMember create() => ChatMember._();
Chat createEmptyInstance() => create(); ChatMember createEmptyInstance() => create();
static $pb.PbList<Chat> createRepeated() => $pb.PbList<Chat>(); static $pb.PbList<ChatMember> createRepeated() => $pb.PbList<ChatMember>();
@$core.pragma('dart2js:noInline') @$core.pragma('dart2js:noInline')
static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Chat>(create); static ChatMember getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ChatMember>(create);
static Chat? _defaultInstance; static ChatMember? _defaultInstance;
@$pb.TagNumber(1)
$0.TypedKey get remoteIdentityPublicKey => $_getN(0);
@$pb.TagNumber(1)
set remoteIdentityPublicKey($0.TypedKey v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasRemoteIdentityPublicKey() => $_has(0);
@$pb.TagNumber(1)
void clearRemoteIdentityPublicKey() => clearField(1);
@$pb.TagNumber(1)
$0.TypedKey ensureRemoteIdentityPublicKey() => $_ensure(0);
@$pb.TagNumber(2)
$0.TypedKey get remoteConversationRecordKey => $_getN(1);
@$pb.TagNumber(2)
set remoteConversationRecordKey($0.TypedKey v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasRemoteConversationRecordKey() => $_has(1);
@$pb.TagNumber(2)
void clearRemoteConversationRecordKey() => clearField(2);
@$pb.TagNumber(2)
$0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(1);
}
class DirectChat extends $pb.GeneratedMessage {
factory DirectChat() => create();
DirectChat._() : super();
factory DirectChat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory DirectChat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DirectChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
..aOM<ChatMember>(3, _omitFieldNames ? '' : 'remoteMember', subBuilder: ChatMember.create)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
DirectChat clone() => DirectChat()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
DirectChat copyWith(void Function(DirectChat) updates) => super.copyWith((message) => updates(message as DirectChat)) as DirectChat;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static DirectChat create() => DirectChat._();
DirectChat createEmptyInstance() => create();
static $pb.PbList<DirectChat> createRepeated() => $pb.PbList<DirectChat>();
@$core.pragma('dart2js:noInline')
static DirectChat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DirectChat>(create);
static DirectChat? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
ChatSettings get settings => $_getN(0); ChatSettings get settings => $_getN(0);
@ -1141,15 +1368,15 @@ class Chat extends $pb.GeneratedMessage {
$0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1); $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$0.TypedKey get remoteConversationRecordKey => $_getN(2); ChatMember get remoteMember => $_getN(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
set remoteConversationRecordKey($0.TypedKey v) { setField(3, v); } set remoteMember(ChatMember v) { setField(3, v); }
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.bool hasRemoteConversationRecordKey() => $_has(2); $core.bool hasRemoteMember() => $_has(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
void clearRemoteConversationRecordKey() => clearField(3); void clearRemoteMember() => clearField(3);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2); ChatMember ensureRemoteMember() => $_ensure(2);
} }
class GroupChat extends $pb.GeneratedMessage { class GroupChat extends $pb.GeneratedMessage {
@ -1160,8 +1387,10 @@ class GroupChat extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GroupChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GroupChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) ..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) ..aOM<Membership>(2, _omitFieldNames ? '' : 'membership', subBuilder: Membership.create)
..pc<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKeys', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) ..aOM<Permissions>(3, _omitFieldNames ? '' : 'permissions', subBuilder: Permissions.create)
..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
..pc<ChatMember>(5, _omitFieldNames ? '' : 'remoteMembers', $pb.PbFieldType.PM, subBuilder: ChatMember.create)
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -1198,18 +1427,111 @@ class GroupChat extends $pb.GeneratedMessage {
ChatSettings ensureSettings() => $_ensure(0); ChatSettings ensureSettings() => $_ensure(0);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$0.TypedKey get localConversationRecordKey => $_getN(1); Membership get membership => $_getN(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set localConversationRecordKey($0.TypedKey v) { setField(2, v); } set membership(Membership v) { setField(2, v); }
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasLocalConversationRecordKey() => $_has(1); $core.bool hasMembership() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearLocalConversationRecordKey() => clearField(2); void clearMembership() => clearField(2);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1); Membership ensureMembership() => $_ensure(1);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.List<$0.TypedKey> get remoteConversationRecordKeys => $_getList(2); Permissions get permissions => $_getN(2);
@$pb.TagNumber(3)
set permissions(Permissions v) { setField(3, v); }
@$pb.TagNumber(3)
$core.bool hasPermissions() => $_has(2);
@$pb.TagNumber(3)
void clearPermissions() => clearField(3);
@$pb.TagNumber(3)
Permissions ensurePermissions() => $_ensure(2);
@$pb.TagNumber(4)
$0.TypedKey get localConversationRecordKey => $_getN(3);
@$pb.TagNumber(4)
set localConversationRecordKey($0.TypedKey v) { setField(4, v); }
@$pb.TagNumber(4)
$core.bool hasLocalConversationRecordKey() => $_has(3);
@$pb.TagNumber(4)
void clearLocalConversationRecordKey() => clearField(4);
@$pb.TagNumber(4)
$0.TypedKey ensureLocalConversationRecordKey() => $_ensure(3);
@$pb.TagNumber(5)
$core.List<ChatMember> get remoteMembers => $_getList(4);
}
enum Chat_Kind {
direct,
group,
notSet
}
class Chat extends $pb.GeneratedMessage {
factory Chat() => create();
Chat._() : super();
factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static const $core.Map<$core.int, Chat_Kind> _Chat_KindByTag = {
1 : Chat_Kind.direct,
2 : Chat_Kind.group,
0 : Chat_Kind.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..oo(0, [1, 2])
..aOM<DirectChat>(1, _omitFieldNames ? '' : 'direct', subBuilder: DirectChat.create)
..aOM<GroupChat>(2, _omitFieldNames ? '' : 'group', subBuilder: GroupChat.create)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
Chat clone() => Chat()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Chat create() => Chat._();
Chat createEmptyInstance() => create();
static $pb.PbList<Chat> createRepeated() => $pb.PbList<Chat>();
@$core.pragma('dart2js:noInline')
static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Chat>(create);
static Chat? _defaultInstance;
Chat_Kind whichKind() => _Chat_KindByTag[$_whichOneof(0)]!;
void clearKind() => clearField($_whichOneof(0));
@$pb.TagNumber(1)
DirectChat get direct => $_getN(0);
@$pb.TagNumber(1)
set direct(DirectChat v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasDirect() => $_has(0);
@$pb.TagNumber(1)
void clearDirect() => clearField(1);
@$pb.TagNumber(1)
DirectChat ensureDirect() => $_ensure(0);
@$pb.TagNumber(2)
GroupChat get group => $_getN(1);
@$pb.TagNumber(2)
set group(GroupChat v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasGroup() => $_has(1);
@$pb.TagNumber(2)
void clearGroup() => clearField(2);
@$pb.TagNumber(2)
GroupChat ensureGroup() => $_ensure(1);
} }
class Profile extends $pb.GeneratedMessage { class Profile extends $pb.GeneratedMessage {
@ -1224,7 +1546,8 @@ class Profile extends $pb.GeneratedMessage {
..aOS(3, _omitFieldNames ? '' : 'about') ..aOS(3, _omitFieldNames ? '' : 'about')
..aOS(4, _omitFieldNames ? '' : 'status') ..aOS(4, _omitFieldNames ? '' : 'status')
..e<Availability>(5, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values) ..e<Availability>(5, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values)
..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'avatar', subBuilder: $0.TypedKey.create) ..aOM<DataReference>(6, _omitFieldNames ? '' : 'avatar', subBuilder: DataReference.create)
..a<$fixnum.Int64>(7, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -1295,15 +1618,24 @@ class Profile extends $pb.GeneratedMessage {
void clearAvailability() => clearField(5); void clearAvailability() => clearField(5);
@$pb.TagNumber(6) @$pb.TagNumber(6)
$0.TypedKey get avatar => $_getN(5); DataReference get avatar => $_getN(5);
@$pb.TagNumber(6) @$pb.TagNumber(6)
set avatar($0.TypedKey v) { setField(6, v); } set avatar(DataReference v) { setField(6, v); }
@$pb.TagNumber(6) @$pb.TagNumber(6)
$core.bool hasAvatar() => $_has(5); $core.bool hasAvatar() => $_has(5);
@$pb.TagNumber(6) @$pb.TagNumber(6)
void clearAvatar() => clearField(6); void clearAvatar() => clearField(6);
@$pb.TagNumber(6) @$pb.TagNumber(6)
$0.TypedKey ensureAvatar() => $_ensure(5); DataReference ensureAvatar() => $_ensure(5);
@$pb.TagNumber(7)
$fixnum.Int64 get timestamp => $_getI64(6);
@$pb.TagNumber(7)
set timestamp($fixnum.Int64 v) { $_setInt64(6, v); }
@$pb.TagNumber(7)
$core.bool hasTimestamp() => $_has(6);
@$pb.TagNumber(7)
void clearTimestamp() => clearField(7);
} }
class Account extends $pb.GeneratedMessage { class Account extends $pb.GeneratedMessage {
@ -1425,13 +1757,14 @@ class Contact extends $pb.GeneratedMessage {
factory Contact.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); factory Contact.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Contact', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Contact', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<Profile>(1, _omitFieldNames ? '' : 'editedProfile', subBuilder: Profile.create) ..aOS(1, _omitFieldNames ? '' : 'nickname')
..aOM<Profile>(2, _omitFieldNames ? '' : 'remoteProfile', subBuilder: Profile.create) ..aOM<Profile>(2, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create)
..aOS(3, _omitFieldNames ? '' : 'superIdentityJson') ..aOS(3, _omitFieldNames ? '' : 'superIdentityJson')
..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $0.TypedKey.create) ..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) ..aOM<$0.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) ..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
..aOB(7, _omitFieldNames ? '' : 'showAvailability') ..aOB(7, _omitFieldNames ? '' : 'showAvailability')
..aOS(8, _omitFieldNames ? '' : 'notes')
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -1457,26 +1790,24 @@ class Contact extends $pb.GeneratedMessage {
static Contact? _defaultInstance; static Contact? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
Profile get editedProfile => $_getN(0); $core.String get nickname => $_getSZ(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
set editedProfile(Profile v) { setField(1, v); } set nickname($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasEditedProfile() => $_has(0); $core.bool hasNickname() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearEditedProfile() => clearField(1); void clearNickname() => clearField(1);
@$pb.TagNumber(1)
Profile ensureEditedProfile() => $_ensure(0);
@$pb.TagNumber(2) @$pb.TagNumber(2)
Profile get remoteProfile => $_getN(1); Profile get profile => $_getN(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set remoteProfile(Profile v) { setField(2, v); } set profile(Profile v) { setField(2, v); }
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasRemoteProfile() => $_has(1); $core.bool hasProfile() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearRemoteProfile() => clearField(2); void clearProfile() => clearField(2);
@$pb.TagNumber(2) @$pb.TagNumber(2)
Profile ensureRemoteProfile() => $_ensure(1); Profile ensureProfile() => $_ensure(1);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.String get superIdentityJson => $_getSZ(2); $core.String get superIdentityJson => $_getSZ(2);
@ -1528,6 +1859,15 @@ class Contact extends $pb.GeneratedMessage {
$core.bool hasShowAvailability() => $_has(6); $core.bool hasShowAvailability() => $_has(6);
@$pb.TagNumber(7) @$pb.TagNumber(7)
void clearShowAvailability() => clearField(7); void clearShowAvailability() => clearField(7);
@$pb.TagNumber(8)
$core.String get notes => $_getSZ(7);
@$pb.TagNumber(8)
set notes($core.String v) { $_setString(7, v); }
@$pb.TagNumber(8)
$core.bool hasNotes() => $_has(7);
@$pb.TagNumber(8)
void clearNotes() => clearField(8);
} }
class ContactInvitation extends $pb.GeneratedMessage { class ContactInvitation extends $pb.GeneratedMessage {

View file

@ -65,6 +65,51 @@ final $typed_data.Uint8List scopeDescriptor = $convert.base64Decode(
'CgVTY29wZRIMCghXQVRDSEVSUxAAEg0KCU1PREVSQVRFRBABEgsKB1RBTEtFUlMQAhIOCgpNT0' 'CgVTY29wZRIMCghXQVRDSEVSUxAAEg0KCU1PREVSQVRFRBABEgsKB1RBTEtFUlMQAhIOCgpNT0'
'RFUkFUT1JTEAMSCgoGQURNSU5TEAQ='); 'RFUkFUT1JTEAMSCgoGQURNSU5TEAQ=');
@$core.Deprecated('Use dHTDataReferenceDescriptor instead')
const DHTDataReference$json = {
'1': 'DHTDataReference',
'2': [
{'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'dhtData'},
{'1': 'hash', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'hash'},
],
};
/// Descriptor for `DHTDataReference`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List dHTDataReferenceDescriptor = $convert.base64Decode(
'ChBESFREYXRhUmVmZXJlbmNlEisKCGRodF9kYXRhGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5Ug'
'dkaHREYXRhEiQKBGhhc2gYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGhhc2g=');
@$core.Deprecated('Use blockStoreDataReferenceDescriptor instead')
const BlockStoreDataReference$json = {
'1': 'BlockStoreDataReference',
'2': [
{'1': 'block', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'block'},
],
};
/// Descriptor for `BlockStoreDataReference`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List blockStoreDataReferenceDescriptor = $convert.base64Decode(
'ChdCbG9ja1N0b3JlRGF0YVJlZmVyZW5jZRImCgVibG9jaxgBIAEoCzIQLnZlaWxpZC5UeXBlZE'
'tleVIFYmxvY2s=');
@$core.Deprecated('Use dataReferenceDescriptor instead')
const DataReference$json = {
'1': 'DataReference',
'2': [
{'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.DHTDataReference', '9': 0, '10': 'dhtData'},
{'1': 'block_store_data', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.BlockStoreDataReference', '9': 0, '10': 'blockStoreData'},
],
'8': [
{'1': 'kind'},
],
};
/// Descriptor for `DataReference`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List dataReferenceDescriptor = $convert.base64Decode(
'Cg1EYXRhUmVmZXJlbmNlEjkKCGRodF9kYXRhGAEgASgLMhwudmVpbGlkY2hhdC5ESFREYXRhUm'
'VmZXJlbmNlSABSB2RodERhdGESTwoQYmxvY2tfc3RvcmVfZGF0YRgCIAEoCzIjLnZlaWxpZGNo'
'YXQuQmxvY2tTdG9yZURhdGFSZWZlcmVuY2VIAFIOYmxvY2tTdG9yZURhdGFCBgoEa2luZA==');
@$core.Deprecated('Use attachmentDescriptor instead') @$core.Deprecated('Use attachmentDescriptor instead')
const Attachment$json = { const Attachment$json = {
'1': 'Attachment', '1': 'Attachment',
@ -89,14 +134,14 @@ const AttachmentMedia$json = {
'2': [ '2': [
{'1': 'mime', '3': 1, '4': 1, '5': 9, '10': 'mime'}, {'1': 'mime', '3': 1, '4': 1, '5': 9, '10': 'mime'},
{'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'}, {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
{'1': 'content', '3': 3, '4': 1, '5': 11, '6': '.dht.DataReference', '10': 'content'}, {'1': 'content', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '10': 'content'},
], ],
}; };
/// Descriptor for `AttachmentMedia`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `AttachmentMedia`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List attachmentMediaDescriptor = $convert.base64Decode( final $typed_data.Uint8List attachmentMediaDescriptor = $convert.base64Decode(
'Cg9BdHRhY2htZW50TWVkaWESEgoEbWltZRgBIAEoCVIEbWltZRISCgRuYW1lGAIgASgJUgRuYW' 'Cg9BdHRhY2htZW50TWVkaWESEgoEbWltZRgBIAEoCVIEbWltZRISCgRuYW1lGAIgASgJUgRuYW'
'1lEiwKB2NvbnRlbnQYAyABKAsyEi5kaHQuRGF0YVJlZmVyZW5jZVIHY29udGVudA=='); '1lEjMKB2NvbnRlbnQYAyABKAsyGS52ZWlsaWRjaGF0LkRhdGFSZWZlcmVuY2VSB2NvbnRlbnQ=');
@$core.Deprecated('Use permissionsDescriptor instead') @$core.Deprecated('Use permissionsDescriptor instead')
const Permissions$json = { const Permissions$json = {
@ -140,7 +185,7 @@ const ChatSettings$json = {
'2': [ '2': [
{'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'}, {'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'},
{'1': 'description', '3': 2, '4': 1, '5': 9, '10': 'description'}, {'1': 'description', '3': 2, '4': 1, '5': 9, '10': 'description'},
{'1': 'icon', '3': 3, '4': 1, '5': 11, '6': '.dht.DataReference', '9': 0, '10': 'icon', '17': true}, {'1': 'icon', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '9': 0, '10': 'icon', '17': true},
{'1': 'default_expiration', '3': 4, '4': 1, '5': 4, '10': 'defaultExpiration'}, {'1': 'default_expiration', '3': 4, '4': 1, '5': 4, '10': 'defaultExpiration'},
], ],
'8': [ '8': [
@ -151,9 +196,9 @@ const ChatSettings$json = {
/// Descriptor for `ChatSettings`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ChatSettings`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatSettingsDescriptor = $convert.base64Decode( final $typed_data.Uint8List chatSettingsDescriptor = $convert.base64Decode(
'CgxDaGF0U2V0dGluZ3MSFAoFdGl0bGUYASABKAlSBXRpdGxlEiAKC2Rlc2NyaXB0aW9uGAIgAS' 'CgxDaGF0U2V0dGluZ3MSFAoFdGl0bGUYASABKAlSBXRpdGxlEiAKC2Rlc2NyaXB0aW9uGAIgAS'
'gJUgtkZXNjcmlwdGlvbhIrCgRpY29uGAMgASgLMhIuZGh0LkRhdGFSZWZlcmVuY2VIAFIEaWNv' 'gJUgtkZXNjcmlwdGlvbhIyCgRpY29uGAMgASgLMhkudmVpbGlkY2hhdC5EYXRhUmVmZXJlbmNl'
'bogBARItChJkZWZhdWx0X2V4cGlyYXRpb24YBCABKARSEWRlZmF1bHRFeHBpcmF0aW9uQgcKBV' 'SABSBGljb26IAQESLQoSZGVmYXVsdF9leHBpcmF0aW9uGAQgASgEUhFkZWZhdWx0RXhwaXJhdG'
'9pY29u'); 'lvbkIHCgVfaWNvbg==');
@$core.Deprecated('Use messageDescriptor instead') @$core.Deprecated('Use messageDescriptor instead')
const Message$json = { const Message$json = {
@ -320,41 +365,76 @@ final $typed_data.Uint8List conversationDescriptor = $convert.base64Decode(
'JvZmlsZRIuChNzdXBlcl9pZGVudGl0eV9qc29uGAIgASgJUhFzdXBlcklkZW50aXR5SnNvbhIs' 'JvZmlsZRIuChNzdXBlcl9pZGVudGl0eV9qc29uGAIgASgJUhFzdXBlcklkZW50aXR5SnNvbhIs'
'CghtZXNzYWdlcxgDIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIIbWVzc2FnZXM='); 'CghtZXNzYWdlcxgDIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIIbWVzc2FnZXM=');
@$core.Deprecated('Use chatDescriptor instead') @$core.Deprecated('Use chatMemberDescriptor instead')
const Chat$json = { const ChatMember$json = {
'1': 'Chat', '1': 'ChatMember',
'2': [ '2': [
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, {'1': 'remote_identity_public_key', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteIdentityPublicKey'},
{'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, {'1': 'remote_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
{'1': 'remote_conversation_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
], ],
}; };
/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ChatMember`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatDescriptor = $convert.base64Decode( final $typed_data.Uint8List chatMemberDescriptor = $convert.base64Decode(
'CgRDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3NSCHNldH' 'CgpDaGF0TWVtYmVyEk0KGnJlbW90ZV9pZGVudGl0eV9wdWJsaWNfa2V5GAEgASgLMhAudmVpbG'
'RpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVpbGlkLlR5' 'lkLlR5cGVkS2V5UhdyZW1vdGVJZGVudGl0eVB1YmxpY0tleRJVCh5yZW1vdGVfY29udmVyc2F0'
'cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRJVCh5yZW1vdGVfY29udmVyc2F0aW' 'aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdG'
'9uX3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdGlv' 'lvblJlY29yZEtleQ==');
'blJlY29yZEtleQ==');
@$core.Deprecated('Use directChatDescriptor instead')
const DirectChat$json = {
'1': 'DirectChat',
'2': [
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'},
{'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
{'1': 'remote_member', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.ChatMember', '10': 'remoteMember'},
],
};
/// Descriptor for `DirectChat`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List directChatDescriptor = $convert.base64Decode(
'CgpEaXJlY3RDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3'
'NSCHNldHRpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVp'
'bGlkLlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRI7Cg1yZW1vdGVfbWVtYm'
'VyGAMgASgLMhYudmVpbGlkY2hhdC5DaGF0TWVtYmVyUgxyZW1vdGVNZW1iZXI=');
@$core.Deprecated('Use groupChatDescriptor instead') @$core.Deprecated('Use groupChatDescriptor instead')
const GroupChat$json = { const GroupChat$json = {
'1': 'GroupChat', '1': 'GroupChat',
'2': [ '2': [
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'},
{'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, {'1': 'membership', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Membership', '10': 'membership'},
{'1': 'remote_conversation_record_keys', '3': 3, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKeys'}, {'1': 'permissions', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.Permissions', '10': 'permissions'},
{'1': 'local_conversation_record_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
{'1': 'remote_members', '3': 5, '4': 3, '5': 11, '6': '.veilidchat.ChatMember', '10': 'remoteMembers'},
], ],
}; };
/// Descriptor for `GroupChat`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `GroupChat`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List groupChatDescriptor = $convert.base64Decode( final $typed_data.Uint8List groupChatDescriptor = $convert.base64Decode(
'CglHcm91cENoYXQSNAoIc2V0dGluZ3MYASABKAsyGC52ZWlsaWRjaGF0LkNoYXRTZXR0aW5nc1' 'CglHcm91cENoYXQSNAoIc2V0dGluZ3MYASABKAsyGC52ZWlsaWRjaGF0LkNoYXRTZXR0aW5nc1'
'IIc2V0dGluZ3MSUwodbG9jYWxfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWls' 'IIc2V0dGluZ3MSNgoKbWVtYmVyc2hpcBgCIAEoCzIWLnZlaWxpZGNoYXQuTWVtYmVyc2hpcFIK'
'aWQuVHlwZWRLZXlSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5ElcKH3JlbW90ZV9jb252ZX' 'bWVtYmVyc2hpcBI5CgtwZXJtaXNzaW9ucxgDIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbn'
'JzYXRpb25fcmVjb3JkX2tleXMYAyADKAsyEC52ZWlsaWQuVHlwZWRLZXlSHHJlbW90ZUNvbnZl' 'NSC3Blcm1pc3Npb25zElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAQgASgLMhAu'
'cnNhdGlvblJlY29yZEtleXM='); 'dmVpbGlkLlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRI9Cg5yZW1vdGVfbW'
'VtYmVycxgFIAMoCzIWLnZlaWxpZGNoYXQuQ2hhdE1lbWJlclINcmVtb3RlTWVtYmVycw==');
@$core.Deprecated('Use chatDescriptor instead')
const Chat$json = {
'1': 'Chat',
'2': [
{'1': 'direct', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.DirectChat', '9': 0, '10': 'direct'},
{'1': 'group', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.GroupChat', '9': 0, '10': 'group'},
],
'8': [
{'1': 'kind'},
],
};
/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatDescriptor = $convert.base64Decode(
'CgRDaGF0EjAKBmRpcmVjdBgBIAEoCzIWLnZlaWxpZGNoYXQuRGlyZWN0Q2hhdEgAUgZkaXJlY3'
'QSLQoFZ3JvdXAYAiABKAsyFS52ZWlsaWRjaGF0Lkdyb3VwQ2hhdEgAUgVncm91cEIGCgRraW5k');
@$core.Deprecated('Use profileDescriptor instead') @$core.Deprecated('Use profileDescriptor instead')
const Profile$json = { const Profile$json = {
@ -365,7 +445,8 @@ const Profile$json = {
{'1': 'about', '3': 3, '4': 1, '5': 9, '10': 'about'}, {'1': 'about', '3': 3, '4': 1, '5': 9, '10': 'about'},
{'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'}, {'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'},
{'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'}, {'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'},
{'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '9': 0, '10': 'avatar', '17': true}, {'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '9': 0, '10': 'avatar', '17': true},
{'1': 'timestamp', '3': 7, '4': 1, '5': 4, '10': 'timestamp'},
], ],
'8': [ '8': [
{'1': '_avatar'}, {'1': '_avatar'},
@ -376,9 +457,9 @@ const Profile$json = {
final $typed_data.Uint8List profileDescriptor = $convert.base64Decode( final $typed_data.Uint8List profileDescriptor = $convert.base64Decode(
'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW' 'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW'
'5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp' '5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp'
'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ei' 'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ej'
'0KBmF2YXRhchgGIAEoCzIQLnZlaWxpZC5UeXBlZEtleUgAUgZhdmF0YXKIAQFCCQoHX2F2YXRh' 'YKBmF2YXRhchgGIAEoCzIZLnZlaWxpZGNoYXQuRGF0YVJlZmVyZW5jZUgAUgZhdmF0YXKIAQES'
'cg=='); 'HAoJdGltZXN0YW1wGAcgASgEUgl0aW1lc3RhbXBCCQoHX2F2YXRhcg==');
@$core.Deprecated('Use accountDescriptor instead') @$core.Deprecated('Use accountDescriptor instead')
const Account$json = { const Account$json = {
@ -409,27 +490,27 @@ final $typed_data.Uint8List accountDescriptor = $convert.base64Decode(
const Contact$json = { const Contact$json = {
'1': 'Contact', '1': 'Contact',
'2': [ '2': [
{'1': 'edited_profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'editedProfile'}, {'1': 'nickname', '3': 1, '4': 1, '5': 9, '10': 'nickname'},
{'1': 'remote_profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'remoteProfile'}, {'1': 'profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'},
{'1': 'super_identity_json', '3': 3, '4': 1, '5': 9, '10': 'superIdentityJson'}, {'1': 'super_identity_json', '3': 3, '4': 1, '5': 9, '10': 'superIdentityJson'},
{'1': 'identity_public_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityPublicKey'}, {'1': 'identity_public_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityPublicKey'},
{'1': 'remote_conversation_record_key', '3': 5, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, {'1': 'remote_conversation_record_key', '3': 5, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
{'1': 'local_conversation_record_key', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, {'1': 'local_conversation_record_key', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
{'1': 'show_availability', '3': 7, '4': 1, '5': 8, '10': 'showAvailability'}, {'1': 'show_availability', '3': 7, '4': 1, '5': 8, '10': 'showAvailability'},
{'1': 'notes', '3': 8, '4': 1, '5': 9, '10': 'notes'},
], ],
}; };
/// Descriptor for `Contact`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `Contact`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List contactDescriptor = $convert.base64Decode( final $typed_data.Uint8List contactDescriptor = $convert.base64Decode(
'CgdDb250YWN0EjoKDmVkaXRlZF9wcm9maWxlGAEgASgLMhMudmVpbGlkY2hhdC5Qcm9maWxlUg' 'CgdDb250YWN0EhoKCG5pY2tuYW1lGAEgASgJUghuaWNrbmFtZRItCgdwcm9maWxlGAIgASgLMh'
'1lZGl0ZWRQcm9maWxlEjoKDnJlbW90ZV9wcm9maWxlGAIgASgLMhMudmVpbGlkY2hhdC5Qcm9m' 'MudmVpbGlkY2hhdC5Qcm9maWxlUgdwcm9maWxlEi4KE3N1cGVyX2lkZW50aXR5X2pzb24YAyAB'
'aWxlUg1yZW1vdGVQcm9maWxlEi4KE3N1cGVyX2lkZW50aXR5X2pzb24YAyABKAlSEXN1cGVySW' 'KAlSEXN1cGVySWRlbnRpdHlKc29uEkAKE2lkZW50aXR5X3B1YmxpY19rZXkYBCABKAsyEC52ZW'
'RlbnRpdHlKc29uEkAKE2lkZW50aXR5X3B1YmxpY19rZXkYBCABKAsyEC52ZWlsaWQuVHlwZWRL' 'lsaWQuVHlwZWRLZXlSEWlkZW50aXR5UHVibGljS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25f'
'ZXlSEWlkZW50aXR5UHVibGljS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleR' 'cmVjb3JkX2tleRgFIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUm'
'gFIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5ElMK' 'Vjb3JkS2V5ElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAYgASgLMhAudmVpbGlk'
'HWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAYgASgLMhAudmVpbGlkLlR5cGVkS2V5Uh' 'LlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRIrChFzaG93X2F2YWlsYWJpbG'
'psb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRIrChFzaG93X2F2YWlsYWJpbGl0eRgHIAEoCFIQ' 'l0eRgHIAEoCFIQc2hvd0F2YWlsYWJpbGl0eRIUCgVub3RlcxgIIAEoCVIFbm90ZXM=');
'c2hvd0F2YWlsYWJpbGl0eQ==');
@$core.Deprecated('Use contactInvitationDescriptor instead') @$core.Deprecated('Use contactInvitationDescriptor instead')
const ContactInvitation$json = { const ContactInvitation$json = {

View file

@ -47,6 +47,31 @@ enum Scope {
ADMINS = 4; ADMINS = 4;
} }
////////////////////////////////////////////////////////////////////////////////////
// Data
////////////////////////////////////////////////////////////////////////////////////
// Reference to data on the DHT
message DHTDataReference {
veilid.TypedKey dht_data = 1;
veilid.TypedKey hash = 2;
}
// Reference to data on the BlockStore
message BlockStoreDataReference {
veilid.TypedKey block = 1;
}
// DataReference
// Pointer to data somewhere in Veilid
// Abstraction over DHTData and BlockStore
message DataReference {
oneof kind {
DHTDataReference dht_data = 1;
BlockStoreDataReference block_store_data = 2;
}
}
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
// Attachments // Attachments
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
@ -67,10 +92,9 @@ message AttachmentMedia {
// Title or filename // Title or filename
string name = 2; string name = 2;
// Pointer to the data content // Pointer to the data content
dht.DataReference content = 3; DataReference content = 3;
} }
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
// Chat room controls // Chat room controls
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
@ -106,7 +130,7 @@ message ChatSettings {
// Description for the chat // Description for the chat
string description = 2; string description = 2;
// Icon for the chat // Icon for the chat
optional dht.DataReference icon = 3; optional DataReference icon = 3;
// Default message expiration duration (in us) // Default message expiration duration (in us)
uint64 default_expiration = 4; uint64 default_expiration = 4;
} }
@ -243,15 +267,23 @@ message Conversation {
veilid.TypedKey messages = 3; veilid.TypedKey messages = 3;
} }
// Either a 1-1 conversation or a group chat // A member of chat which may or may not be associated with a contact
message ChatMember {
// The identity public key most recently associated with the chat member
veilid.TypedKey remote_identity_public_key = 1;
// Conversation key for the other party
veilid.TypedKey remote_conversation_record_key = 2;
}
// A 1-1 chat
// Privately encrypted, this is the local user's copy of the chat // Privately encrypted, this is the local user's copy of the chat
message Chat { message DirectChat {
// Settings // Settings
ChatSettings settings = 1; ChatSettings settings = 1;
// Conversation key for this user // Conversation key for this user
veilid.TypedKey local_conversation_record_key = 2; veilid.TypedKey local_conversation_record_key = 2;
// Conversation key for the other party // Conversation key for the other party
veilid.TypedKey remote_conversation_record_key = 3; ChatMember remote_member = 3;
} }
// A group chat // A group chat
@ -259,10 +291,22 @@ message Chat {
message GroupChat { message GroupChat {
// Settings // Settings
ChatSettings settings = 1; ChatSettings settings = 1;
// Membership
Membership membership = 2;
// Permissions
Permissions permissions = 3;
// Conversation key for this user // Conversation key for this user
veilid.TypedKey local_conversation_record_key = 2; veilid.TypedKey local_conversation_record_key = 4;
// Conversation keys for the other parties // Conversation keys for the other parties
repeated veilid.TypedKey remote_conversation_record_keys = 3; repeated ChatMember remote_members = 5;
}
// Some kind of chat
message Chat {
oneof kind {
DirectChat direct = 1;
GroupChat group = 2;
}
} }
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
@ -285,8 +329,10 @@ message Profile {
string status = 4; string status = 4;
// Availability // Availability
Availability availability = 5; Availability availability = 5;
// Avatar DHTData // Avatar
optional veilid.TypedKey avatar = 6; optional DataReference avatar = 6;
// Timestamp of last change
uint64 timestamp = 7;
} }
// A record of an individual account // A record of an individual account
@ -323,10 +369,10 @@ message Account {
// //
// Stored in ContactList DHTList // Stored in ContactList DHTList
message Contact { message Contact {
// Friend's profile as locally edited // Friend's nickname
Profile edited_profile = 1; string nickname = 1;
// Copy of friend's profile from remote conversation // Copy of friend's profile from remote conversation
Profile remote_profile = 2; Profile profile = 2;
// Copy of friend's SuperIdentity in JSON from remote conversation // Copy of friend's SuperIdentity in JSON from remote conversation
string super_identity_json = 3; string super_identity_json = 3;
// Copy of friend's most recent identity public key from their identityMaster // Copy of friend's most recent identity public key from their identityMaster
@ -337,6 +383,8 @@ message Contact {
veilid.TypedKey local_conversation_record_key = 6; veilid.TypedKey local_conversation_record_key = 6;
// Show availability to this contact // Show availability to this contact
bool show_availability = 7; bool show_availability = 7;
// Notes about this friend
string notes = 8;
} }
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////

View file

@ -7,9 +7,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:stream_transform/stream_transform.dart'; import 'package:stream_transform/stream_transform.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart'; import '../../../account_manager/account_manager.dart';
import '../../layout/layout.dart'; import '../../layout/layout.dart';
import '../../proto/proto.dart' as proto;
import '../../settings/settings.dart'; import '../../settings/settings.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../../veilid_processor/views/developer.dart'; import '../../veilid_processor/views/developer.dart';
@ -18,13 +20,12 @@ part 'router_cubit.freezed.dart';
part 'router_cubit.g.dart'; part 'router_cubit.g.dart';
final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'rootNavKey'); final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'rootNavKey');
final _homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'homeNavKey');
@freezed @freezed
class RouterState with _$RouterState { class RouterState with _$RouterState {
const factory RouterState( const factory RouterState({
{required bool hasAnyAccount, required bool hasAnyAccount,
required bool hasActiveChat}) = _RouterState; }) = _RouterState;
factory RouterState.fromJson(dynamic json) => factory RouterState.fromJson(dynamic json) =>
_$RouterStateFromJson(json as Map<String, dynamic>); _$RouterStateFromJson(json as Map<String, dynamic>);
@ -34,7 +35,6 @@ class RouterCubit extends Cubit<RouterState> {
RouterCubit(AccountRepository accountRepository) RouterCubit(AccountRepository accountRepository)
: super(RouterState( : super(RouterState(
hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty, hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty,
hasActiveChat: false,
)) { )) {
// Subscribe to repository streams // Subscribe to repository streams
_accountRepositorySubscription = accountRepository.stream.listen((event) { _accountRepositorySubscription = accountRepository.stream.listen((event) {
@ -50,10 +50,6 @@ class RouterCubit extends Cubit<RouterState> {
}); });
} }
void setHasActiveChat(bool active) {
emit(state.copyWith(hasActiveChat: active));
}
@override @override
Future<void> close() async { Future<void> close() async {
await _accountRepositorySubscription.cancel(); await _accountRepositorySubscription.cancel();
@ -62,27 +58,29 @@ class RouterCubit extends Cubit<RouterState> {
/// Our application routes /// Our application routes
List<RouteBase> get routes => [ List<RouteBase> get routes => [
ShellRoute(
navigatorKey: _homeNavKey,
builder: (context, state, child) => HomeShell(
accountReadyBuilder: Builder(
builder: (context) =>
HomeAccountReadyShell(context: context, child: child))),
routes: [
GoRoute( GoRoute(
path: '/', path: '/',
builder: (context, state) => const HomeAccountReadyMain(), builder: (context, state) => const HomeScreen(),
), ),
GoRoute( GoRoute(
path: '/chat', path: '/edit_account',
builder: (context, state) => const HomeAccountReadyChat(), builder: (context, state) {
), final extra = state.extra! as List<Object?>;
], return EditAccountPage(
superIdentityRecordKey: extra[0]! as TypedKey,
existingProfile: extra[1]! as proto.Profile,
);
},
), ),
GoRoute( GoRoute(
path: '/new_account', path: '/new_account',
builder: (context, state) => const NewAccountPage(), builder: (context, state) => const NewAccountPage(),
), ),
GoRoute(
path: '/new_account/recovery_key',
builder: (context, state) =>
ShowRecoveryKeyPage(secretKey: state.extra! as SecretKey),
),
GoRoute( GoRoute(
path: '/settings', path: '/settings',
builder: (context, state) => const SettingsPage(), builder: (context, state) => const SettingsPage(),
@ -98,37 +96,14 @@ class RouterCubit extends Cubit<RouterState> {
// No matter where we are, if there's not // No matter where we are, if there's not
switch (goRouterState.matchedLocation) { switch (goRouterState.matchedLocation) {
case '/new_account':
return state.hasAnyAccount ? '/' : null;
case '/': case '/':
if (!state.hasAnyAccount) { if (!state.hasAnyAccount) {
return '/new_account'; return '/new_account';
} }
if (responsiveVisibility(
context: context,
tablet: false,
tabletLandscape: false,
desktop: false)) {
if (state.hasActiveChat) {
return '/chat';
}
}
return null; return null;
case '/chat': case '/new_account':
if (!state.hasAnyAccount) { return null;
return '/new_account'; case '/new_account/recovery_key':
}
if (responsiveVisibility(
context: context,
tablet: false,
tabletLandscape: false,
desktop: false)) {
if (!state.hasActiveChat) {
return '/';
}
} else {
return '/';
}
return null; return null;
case '/settings': case '/settings':
return null; return null;

View file

@ -21,7 +21,6 @@ RouterState _$RouterStateFromJson(Map<String, dynamic> json) {
/// @nodoc /// @nodoc
mixin _$RouterState { mixin _$RouterState {
bool get hasAnyAccount => throw _privateConstructorUsedError; bool get hasAnyAccount => throw _privateConstructorUsedError;
bool get hasActiveChat => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
@ -35,7 +34,7 @@ abstract class $RouterStateCopyWith<$Res> {
RouterState value, $Res Function(RouterState) then) = RouterState value, $Res Function(RouterState) then) =
_$RouterStateCopyWithImpl<$Res, RouterState>; _$RouterStateCopyWithImpl<$Res, RouterState>;
@useResult @useResult
$Res call({bool hasAnyAccount, bool hasActiveChat}); $Res call({bool hasAnyAccount});
} }
/// @nodoc /// @nodoc
@ -52,17 +51,12 @@ class _$RouterStateCopyWithImpl<$Res, $Val extends RouterState>
@override @override
$Res call({ $Res call({
Object? hasAnyAccount = null, Object? hasAnyAccount = null,
Object? hasActiveChat = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
hasAnyAccount: null == hasAnyAccount hasAnyAccount: null == hasAnyAccount
? _value.hasAnyAccount ? _value.hasAnyAccount
: hasAnyAccount // ignore: cast_nullable_to_non_nullable : hasAnyAccount // ignore: cast_nullable_to_non_nullable
as bool, as bool,
hasActiveChat: null == hasActiveChat
? _value.hasActiveChat
: hasActiveChat // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val); ) as $Val);
} }
} }
@ -75,7 +69,7 @@ abstract class _$$RouterStateImplCopyWith<$Res>
__$$RouterStateImplCopyWithImpl<$Res>; __$$RouterStateImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({bool hasAnyAccount, bool hasActiveChat}); $Res call({bool hasAnyAccount});
} }
/// @nodoc /// @nodoc
@ -90,17 +84,12 @@ class __$$RouterStateImplCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? hasAnyAccount = null, Object? hasAnyAccount = null,
Object? hasActiveChat = null,
}) { }) {
return _then(_$RouterStateImpl( return _then(_$RouterStateImpl(
hasAnyAccount: null == hasAnyAccount hasAnyAccount: null == hasAnyAccount
? _value.hasAnyAccount ? _value.hasAnyAccount
: hasAnyAccount // ignore: cast_nullable_to_non_nullable : hasAnyAccount // ignore: cast_nullable_to_non_nullable
as bool, as bool,
hasActiveChat: null == hasActiveChat
? _value.hasActiveChat
: hasActiveChat // ignore: cast_nullable_to_non_nullable
as bool,
)); ));
} }
} }
@ -108,20 +97,17 @@ class __$$RouterStateImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
const _$RouterStateImpl( const _$RouterStateImpl({required this.hasAnyAccount});
{required this.hasAnyAccount, required this.hasActiveChat});
factory _$RouterStateImpl.fromJson(Map<String, dynamic> json) => factory _$RouterStateImpl.fromJson(Map<String, dynamic> json) =>
_$$RouterStateImplFromJson(json); _$$RouterStateImplFromJson(json);
@override @override
final bool hasAnyAccount; final bool hasAnyAccount;
@override
final bool hasActiveChat;
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'RouterState(hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)'; return 'RouterState(hasAnyAccount: $hasAnyAccount)';
} }
@override @override
@ -129,8 +115,7 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties properties
..add(DiagnosticsProperty('type', 'RouterState')) ..add(DiagnosticsProperty('type', 'RouterState'))
..add(DiagnosticsProperty('hasAnyAccount', hasAnyAccount)) ..add(DiagnosticsProperty('hasAnyAccount', hasAnyAccount));
..add(DiagnosticsProperty('hasActiveChat', hasActiveChat));
} }
@override @override
@ -139,14 +124,12 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$RouterStateImpl && other is _$RouterStateImpl &&
(identical(other.hasAnyAccount, hasAnyAccount) || (identical(other.hasAnyAccount, hasAnyAccount) ||
other.hasAnyAccount == hasAnyAccount) && other.hasAnyAccount == hasAnyAccount));
(identical(other.hasActiveChat, hasActiveChat) ||
other.hasActiveChat == hasActiveChat));
} }
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
int get hashCode => Object.hash(runtimeType, hasAnyAccount, hasActiveChat); int get hashCode => Object.hash(runtimeType, hasAnyAccount);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@ -163,9 +146,8 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
} }
abstract class _RouterState implements RouterState { abstract class _RouterState implements RouterState {
const factory _RouterState( const factory _RouterState({required final bool hasAnyAccount}) =
{required final bool hasAnyAccount, _$RouterStateImpl;
required final bool hasActiveChat}) = _$RouterStateImpl;
factory _RouterState.fromJson(Map<String, dynamic> json) = factory _RouterState.fromJson(Map<String, dynamic> json) =
_$RouterStateImpl.fromJson; _$RouterStateImpl.fromJson;
@ -173,8 +155,6 @@ abstract class _RouterState implements RouterState {
@override @override
bool get hasAnyAccount; bool get hasAnyAccount;
@override @override
bool get hasActiveChat;
@override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith => _$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;

View file

@ -9,11 +9,9 @@ part of 'router_cubit.dart';
_$RouterStateImpl _$$RouterStateImplFromJson(Map<String, dynamic> json) => _$RouterStateImpl _$$RouterStateImplFromJson(Map<String, dynamic> json) =>
_$RouterStateImpl( _$RouterStateImpl(
hasAnyAccount: json['has_any_account'] as bool, hasAnyAccount: json['has_any_account'] as bool,
hasActiveChat: json['has_active_chat'] as bool,
); );
Map<String, dynamic> _$$RouterStateImplToJson(_$RouterStateImpl instance) => Map<String, dynamic> _$$RouterStateImplToJson(_$RouterStateImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'has_any_account': instance.hasAnyAccount, 'has_any_account': instance.hasAnyAccount,
'has_active_chat': instance.hasActiveChat,
}; };

View file

@ -89,11 +89,9 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
onError: errorScale.primaryText, onError: errorScale.primaryText,
// errorContainer: errorScale.hoverElementBackground, // errorContainer: errorScale.hoverElementBackground,
// onErrorContainer: errorScale.subtleText, // onErrorContainer: errorScale.subtleText,
background: grayScale.appBackground, // reviewed
onBackground: grayScale.appText, // reviewed
surface: primaryScale.primary, // reviewed surface: primaryScale.primary, // reviewed
onSurface: primaryScale.primaryText, // reviewed onSurface: primaryScale.primaryText, // reviewed
surfaceVariant: secondaryScale.primary, surfaceContainerHighest: secondaryScale.primary,
onSurfaceVariant: secondaryScale.primaryText, // ?? reviewed a little onSurfaceVariant: secondaryScale.primaryText, // ?? reviewed a little
outline: primaryScale.border, outline: primaryScale.border,
outlineVariant: secondaryScale.border, outlineVariant: secondaryScale.border,

View file

@ -5,7 +5,6 @@ import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:motion_toast/motion_toast.dart'; import 'package:motion_toast/motion_toast.dart';
import 'package:quickalert/quickalert.dart'; import 'package:quickalert/quickalert.dart';
@ -122,36 +121,45 @@ Future<void> showErrorModal(
} }
void showErrorToast(BuildContext context, String message) { void showErrorToast(BuildContext context, String message) {
MotionToast.error( final theme = Theme.of(context);
title: Text(translate('toast.error')), final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
MotionToast(
//title: Text(translate('toast.error')),
description: Text(message), description: Text(message),
constraints: BoxConstraints.loose(const Size(400, 100)),
contentPadding: const EdgeInsets.all(16),
primaryColor: scale.errorScale.elementBackground,
secondaryColor: scale.errorScale.calloutBackground,
borderRadius: 16,
toastDuration: const Duration(seconds: 4),
animationDuration: const Duration(milliseconds: 1000),
displayBorder: scaleConfig.useVisualIndicators,
icon: Icons.error,
).show(context); ).show(context);
} }
void showInfoToast(BuildContext context, String message) { void showInfoToast(BuildContext context, String message) {
MotionToast.info( final theme = Theme.of(context);
title: Text(translate('toast.info')), final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
MotionToast(
//title: Text(translate('toast.info')),
description: Text(message), description: Text(message),
constraints: BoxConstraints.loose(const Size(400, 100)),
contentPadding: const EdgeInsets.all(16),
primaryColor: scale.tertiaryScale.elementBackground,
secondaryColor: scale.tertiaryScale.calloutBackground,
borderRadius: 16,
toastDuration: const Duration(seconds: 2),
animationDuration: const Duration(milliseconds: 500),
displayBorder: scaleConfig.useVisualIndicators,
icon: Icons.info,
).show(context); ).show(context);
} }
// Widget insetBorder(
// {required BuildContext context,
// required bool enabled,
// required Color color,
// required Widget child}) {
// if (!enabled) {
// return child;
// }
// return Stack({
// children: [] {
// DecoratedBox(decoration: BoxDecoration()
// child,
// }
// })
// }
Widget styledTitleContainer({ Widget styledTitleContainer({
required BuildContext context, required BuildContext context,
required String title, required String title,
@ -230,3 +238,26 @@ Widget styledBottomSheet({
bool get isPlatformDark => bool get isPlatformDark =>
WidgetsBinding.instance.platformDispatcher.platformBrightness == WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.dark; Brightness.dark;
const grayColorFilter = ColorFilter.matrix(<double>[
0.2126,
0.7152,
0.0722,
0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0,
0,
0,
1,
0,
]);

View file

@ -0,0 +1,14 @@
import 'package:package_info_plus/package_info_plus.dart';
String packageInfoAppName = '';
String packageInfoPackageName = '';
String packageInfoVersion = '';
String packageInfoBuildNumber = '';
Future<void> initPackageInfo() async {
final packageInfo = await PackageInfo.fromPlatform();
packageInfoAppName = packageInfo.appName;
packageInfoPackageName = packageInfo.packageName;
packageInfoVersion = packageInfo.version;
packageInfoBuildNumber = packageInfo.buildNumber;
}

View file

@ -1,8 +1,10 @@
export 'animations.dart'; export 'animations.dart';
export 'enter_password.dart'; export 'enter_password.dart';
export 'enter_pin.dart'; export 'enter_pin.dart';
export 'loggy.dart'; export 'loggy.dart';
export 'misc.dart'; export 'misc.dart';
export 'package_info.dart';
export 'phono_byte.dart'; export 'phono_byte.dart';
export 'pop_control.dart'; export 'pop_control.dart';
export 'responsive.dart'; export 'responsive.dart';

View file

@ -1,5 +1,6 @@
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -11,7 +12,7 @@ import '../../theme/theme.dart';
import '../cubit/connection_state_cubit.dart'; import '../cubit/connection_state_cubit.dart';
class SignalStrengthMeterWidget extends StatelessWidget { class SignalStrengthMeterWidget extends StatelessWidget {
const SignalStrengthMeterWidget({super.key}); const SignalStrengthMeterWidget({super.key, this.color, this.inactiveColor});
@override @override
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
@ -33,32 +34,35 @@ class SignalStrengthMeterWidget extends StatelessWidget {
switch (connectionState.attachment.state) { switch (connectionState.attachment.state) {
case AttachmentState.detached: case AttachmentState.detached:
iconWidget = Icon(Icons.signal_cellular_nodata, iconWidget = Icon(Icons.signal_cellular_nodata,
size: iconSize, color: scale.primaryScale.primaryText); size: iconSize,
color: this.color ?? scale.primaryScale.primaryText);
return; return;
case AttachmentState.detaching: case AttachmentState.detaching:
iconWidget = Icon(Icons.signal_cellular_off, iconWidget = Icon(Icons.signal_cellular_off,
size: iconSize, color: scale.primaryScale.primaryText); size: iconSize,
color: this.color ?? scale.primaryScale.primaryText);
return; return;
case AttachmentState.attaching: case AttachmentState.attaching:
value = 0; value = 0;
color = scale.primaryScale.primaryText; color = this.color ?? scale.primaryScale.primaryText;
case AttachmentState.attachedWeak: case AttachmentState.attachedWeak:
value = 1; value = 1;
color = scale.primaryScale.primaryText; color = this.color ?? scale.primaryScale.primaryText;
case AttachmentState.attachedStrong: case AttachmentState.attachedStrong:
value = 2; value = 2;
color = scale.primaryScale.primaryText; color = this.color ?? scale.primaryScale.primaryText;
case AttachmentState.attachedGood: case AttachmentState.attachedGood:
value = 3; value = 3;
color = scale.primaryScale.primaryText; color = this.color ?? scale.primaryScale.primaryText;
case AttachmentState.fullyAttached: case AttachmentState.fullyAttached:
value = 4; value = 4;
color = scale.primaryScale.primaryText; color = this.color ?? scale.primaryScale.primaryText;
case AttachmentState.overAttached: case AttachmentState.overAttached:
value = 4; value = 4;
color = scale.primaryScale.primaryText; color = this.color ?? scale.primaryScale.primaryText;
} }
inactiveColor = scale.primaryScale.primaryText; inactiveColor =
this.inactiveColor ?? scale.primaryScale.primaryText;
iconWidget = SignalStrengthIndicator.bars( iconWidget = SignalStrengthIndicator.bars(
value: value, value: value,
@ -86,4 +90,16 @@ class SignalStrengthMeterWidget extends StatelessWidget {
child: iconWidget); child: iconWidget);
}); });
} }
////////////////////////////////////////////////////////////////////////////
final Color? color;
final Color? inactiveColor;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(ColorProperty('color', color))
..add(ColorProperty('inactiveColor', inactiveColor));
}
} }

View file

@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation import Foundation
import mobile_scanner import mobile_scanner
import package_info_plus
import pasteboard import pasteboard
import path_provider_foundation import path_provider_foundation
import screen_retriever import screen_retriever
@ -19,6 +20,7 @@ import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))

View file

@ -2,6 +2,8 @@ PODS:
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- mobile_scanner (5.1.1): - mobile_scanner (5.1.1):
- FlutterMacOS - FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- pasteboard (0.0.1): - pasteboard (0.0.1):
- FlutterMacOS - FlutterMacOS
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
@ -29,6 +31,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
@ -45,6 +48,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral :path: Flutter/ephemeral
mobile_scanner: mobile_scanner:
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
pasteboard: pasteboard:
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos :path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
path_provider_foundation: path_provider_foundation:
@ -69,6 +74,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
mobile_scanner: 1efac1e53c294b24e3bb55bcc7f4deee0233a86b mobile_scanner: 1efac1e53c294b24e3bb55bcc7f4deee0233a86b
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38

View file

@ -14,7 +14,7 @@ dependencies:
path: ../ path: ../
dev_dependencies: dev_dependencies:
async_tools: ^0.1.2 async_tools: ^0.1.3
integration_test: integration_test:
sdk: flutter sdk: flutter
lint_hard: ^4.0.0 lint_hard: ^4.0.0

View file

@ -23,7 +23,6 @@ message DHTData {
uint32 size = 4; uint32 size = 4;
} }
// DHTLog - represents a ring buffer of many elements with append/truncate semantics // DHTLog - represents a ring buffer of many elements with append/truncate semantics
// Header in subkey 0 of first key follows this structure // Header in subkey 0 of first key follows this structure
message DHTLog { message DHTLog {
@ -62,27 +61,6 @@ message DHTShortArray {
// calculated through iteration // calculated through iteration
} }
// Reference to data on the DHT
message DHTDataReference {
veilid.TypedKey dht_data = 1;
veilid.TypedKey hash = 2;
}
// Reference to data on the BlockStore
message BlockStoreDataReference {
veilid.TypedKey block = 1;
}
// DataReference
// Pointer to data somewhere in Veilid
// Abstraction over DHTData and BlockStore
message DataReference {
oneof kind {
DHTDataReference dht_data = 1;
BlockStoreDataReference block_store_data = 2;
}
}
// A pointer to an child DHT record // A pointer to an child DHT record
message OwnedDHTRecordPointer { message OwnedDHTRecordPointer {
// DHT Record key // DHT Record key

View file

@ -71,6 +71,12 @@ class _DHTLogSpine {
// Write new spine head record to the network // Write new spine head record to the network
await spine.operate((spine) async { await spine.operate((spine) async {
// Write first empty subkey
final subkeyData = _makeEmptySubkey();
final existingSubkeyData =
await spineRecord.tryWriteBytes(subkeyData, subkey: 1);
assert(existingSubkeyData == null, 'Should never conflict on create');
final success = await spine.writeSpineHead(); final success = await spine.writeSpineHead();
assert(success, 'false return should never happen on create'); assert(success, 'false return should never happen on create');
}); });
@ -603,7 +609,7 @@ class _DHTLogSpine {
// Don't watch for local changes because this class already handles // Don't watch for local changes because this class already handles
// notifying listeners and knows when it makes local changes // notifying listeners and knows when it makes local changes
_subscription ??= _subscription ??=
await _spineRecord.listen(localChanges: false, _onSpineChanged); await _spineRecord.listen(localChanges: true, _onSpineChanged);
} on Exception { } on Exception {
// If anything fails, try to cancel the watches // If anything fails, try to cancel the watches
await cancelWatch(); await cancelWatch();

View file

@ -12,14 +12,6 @@ class DefaultDHTRecordCubit<T> extends DHTRecordCubit<T> {
stateFunction: _makeStateFunction(decodeState), stateFunction: _makeStateFunction(decodeState),
watchFunction: _makeWatchFunction()); watchFunction: _makeWatchFunction());
// DefaultDHTRecordCubit.value({
// required super.record,
// required T Function(List<int> data) decodeState,
// }) : super.value(
// initialStateFunction: _makeInitialStateFunction(decodeState),
// stateFunction: _makeStateFunction(decodeState),
// watchFunction: _makeWatchFunction());
static InitialStateFunction<T> _makeInitialStateFunction<T>( static InitialStateFunction<T> _makeInitialStateFunction<T>(
T Function(List<int> data) decodeState) => T Function(List<int> data) decodeState) =>
(record) async { (record) async {

View file

@ -1,7 +1,5 @@
part of 'dht_record_pool.dart'; part of 'dht_record_pool.dart';
const _sfListen = 'listen';
@immutable @immutable
class DHTRecordWatchChange extends Equatable { class DHTRecordWatchChange extends Equatable {
const DHTRecordWatchChange( const DHTRecordWatchChange(
@ -41,7 +39,7 @@ enum DHTRecordRefreshMode {
class DHTRecord implements DHTDeleteable<DHTRecord> { class DHTRecord implements DHTDeleteable<DHTRecord> {
DHTRecord._( DHTRecord._(
{required VeilidRoutingContext routingContext, {required VeilidRoutingContext routingContext,
required SharedDHTRecordData sharedDHTRecordData, required _SharedDHTRecordData sharedDHTRecordData,
required int defaultSubkey, required int defaultSubkey,
required KeyPair? writer, required KeyPair? writer,
required VeilidCrypto crypto, required VeilidCrypto crypto,
@ -241,7 +239,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
// if so, shortcut and don't bother decrypting it // if so, shortcut and don't bother decrypting it
if (newValueData.data.equals(encryptedNewValue)) { if (newValueData.data.equals(encryptedNewValue)) {
if (isUpdated) { if (isUpdated) {
DHTRecordPool.instance.processLocalValueChange(key, newValue, subkey); DHTRecordPool.instance._processLocalValueChange(key, newValue, subkey);
} }
return null; return null;
} }
@ -251,7 +249,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
await (crypto ?? _crypto).decrypt(newValueData.data); await (crypto ?? _crypto).decrypt(newValueData.data);
if (isUpdated) { if (isUpdated) {
DHTRecordPool.instance DHTRecordPool.instance
.processLocalValueChange(key, decryptedNewValue, subkey); ._processLocalValueChange(key, decryptedNewValue, subkey);
} }
return decryptedNewValue; return decryptedNewValue;
} }
@ -298,7 +296,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
final isUpdated = newValueData.seq != lastSeq; final isUpdated = newValueData.seq != lastSeq;
if (isUpdated) { if (isUpdated) {
DHTRecordPool.instance.processLocalValueChange(key, newValue, subkey); DHTRecordPool.instance._processLocalValueChange(key, newValue, subkey);
} }
} }
@ -308,7 +306,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
/// Each attempt to write the value calls an update function with the /// Each attempt to write the value calls an update function with the
/// old value to determine what new value should be attempted for that write. /// old value to determine what new value should be attempted for that write.
Future<void> eventualUpdateBytes( Future<void> eventualUpdateBytes(
Future<Uint8List> Function(Uint8List? oldValue) update, Future<Uint8List?> Function(Uint8List? oldValue) update,
{int subkey = -1, {int subkey = -1,
VeilidCrypto? crypto, VeilidCrypto? crypto,
KeyPair? writer, KeyPair? writer,
@ -323,7 +321,10 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
do { do {
// Update the data // Update the data
final updatedValue = await update(oldValue); final updatedValue = await update(oldValue);
if (updatedValue == null) {
// If null is returned from the update, stop trying to do the update
break;
}
// Try to write it back to the network // Try to write it back to the network
oldValue = await tryWriteBytes(updatedValue, oldValue = await tryWriteBytes(updatedValue,
subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum);
@ -389,7 +390,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
/// Like 'eventualUpdateBytes' but with JSON marshal/unmarshal of the value /// Like 'eventualUpdateBytes' but with JSON marshal/unmarshal of the value
Future<void> eventualUpdateJson<T>( Future<void> eventualUpdateJson<T>(
T Function(dynamic) fromJson, Future<T> Function(T?) update, T Function(dynamic) fromJson, Future<T?> Function(T?) update,
{int subkey = -1, {int subkey = -1,
VeilidCrypto? crypto, VeilidCrypto? crypto,
KeyPair? writer, KeyPair? writer,
@ -399,7 +400,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
/// Like 'eventualUpdateBytes' but with protobuf marshal/unmarshal of the value /// Like 'eventualUpdateBytes' but with protobuf marshal/unmarshal of the value
Future<void> eventualUpdateProtobuf<T extends GeneratedMessage>( Future<void> eventualUpdateProtobuf<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, Future<T> Function(T?) update, T Function(List<int>) fromBuffer, Future<T?> Function(T?) update,
{int subkey = -1, {int subkey = -1,
VeilidCrypto? crypto, VeilidCrypto? crypto,
KeyPair? writer, KeyPair? writer,
@ -416,7 +417,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
// Set up watch requirements which will get picked up by the next tick // Set up watch requirements which will get picked up by the next tick
final oldWatchState = watchState; final oldWatchState = watchState;
watchState = watchState =
WatchState(subkeys: subkeys, expiration: expiration, count: count); _WatchState(subkeys: subkeys, expiration: expiration, count: count);
if (oldWatchState != watchState) { if (oldWatchState != watchState) {
_sharedDHTRecordData.needsWatchStateUpdate = true; _sharedDHTRecordData.needsWatchStateUpdate = true;
} }
@ -541,7 +542,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
final SharedDHTRecordData _sharedDHTRecordData; final _SharedDHTRecordData _sharedDHTRecordData;
final VeilidRoutingContext _routingContext; final VeilidRoutingContext _routingContext;
final int _defaultSubkey; final int _defaultSubkey;
final KeyPair? _writer; final KeyPair? _writer;
@ -551,5 +552,5 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
int _openCount; int _openCount;
StreamController<DHTRecordWatchChange>? _watchController; StreamController<DHTRecordWatchChange>? _watchController;
@internal @internal
WatchState? watchState; _WatchState? watchState;
} }

View file

@ -29,20 +29,6 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
}); });
} }
// DHTRecordCubit.value({
// required DHTRecord record,
// required InitialStateFunction<T> initialStateFunction,
// required StateFunction<T> stateFunction,
// required WatchFunction watchFunction,
// }) : _record = record,
// _stateFunction = stateFunction,
// _wantsCloseRecord = false,
// super(const AsyncValue.loading()) {
// Future.delayed(Duration.zero, () async {
// await _init(initialStateFunction, stateFunction, watchFunction);
// });
// }
Future<void> _init( Future<void> _init(
InitialStateFunction<T> initialStateFunction, InitialStateFunction<T> initialStateFunction,
StateFunction<T> stateFunction, StateFunction<T> stateFunction,

View file

@ -0,0 +1,77 @@
part of 'dht_record_pool.dart';
const int _watchBackoffMultiplier = 2;
const int _watchBackoffMax = 30;
const int? _defaultWatchDurationSecs = null; // 600
const int _watchRenewalNumerator = 4;
const int _watchRenewalDenominator = 5;
// DHT crypto domain
const String _cryptoDomainDHT = 'dht';
// Singlefuture keys
const _sfPollWatch = '_pollWatch';
const _sfListen = 'listen';
/// Watch state
@immutable
class _WatchState extends Equatable {
const _WatchState(
{required this.subkeys,
required this.expiration,
required this.count,
this.realExpiration,
this.renewalTime});
final List<ValueSubkeyRange>? subkeys;
final Timestamp? expiration;
final int? count;
final Timestamp? realExpiration;
final Timestamp? renewalTime;
@override
List<Object?> get props =>
[subkeys, expiration, count, realExpiration, renewalTime];
}
/// Data shared amongst all DHTRecord instances
class _SharedDHTRecordData {
_SharedDHTRecordData(
{required this.recordDescriptor,
required this.defaultWriter,
required this.defaultRoutingContext});
DHTRecordDescriptor recordDescriptor;
KeyPair? defaultWriter;
VeilidRoutingContext defaultRoutingContext;
bool needsWatchStateUpdate = false;
_WatchState? unionWatchState;
}
// Per opened record data
class _OpenedRecordInfo {
_OpenedRecordInfo(
{required DHTRecordDescriptor recordDescriptor,
required KeyPair? defaultWriter,
required VeilidRoutingContext defaultRoutingContext})
: shared = _SharedDHTRecordData(
recordDescriptor: recordDescriptor,
defaultWriter: defaultWriter,
defaultRoutingContext: defaultRoutingContext);
_SharedDHTRecordData shared;
Set<DHTRecord> records = {};
String get debugNames {
final r = records.toList()
..sort((a, b) => a.key.toString().compareTo(b.key.toString()));
return '[${r.map((x) => x.debugName).join(',')}]';
}
String get details {
final r = records.toList()
..sort((a, b) => a.key.toString().compareTo(b.key.toString()));
return '[${r.map((x) => "writer=${x._writer} "
"defaultSubkey=${x._defaultSubkey}").join(',')}]';
}
String get sharedDetails => shared.toString();
}

View file

@ -195,177 +195,6 @@ class DHTShortArray extends $pb.GeneratedMessage {
$core.List<$core.int> get seqs => $_getList(2); $core.List<$core.int> get seqs => $_getList(2);
} }
class DHTDataReference extends $pb.GeneratedMessage {
factory DHTDataReference() => create();
DHTDataReference._() : super();
factory DHTDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory DHTDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DHTDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create)
..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'hash', subBuilder: $0.TypedKey.create)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
DHTDataReference clone() => DHTDataReference()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
DHTDataReference copyWith(void Function(DHTDataReference) updates) => super.copyWith((message) => updates(message as DHTDataReference)) as DHTDataReference;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static DHTDataReference create() => DHTDataReference._();
DHTDataReference createEmptyInstance() => create();
static $pb.PbList<DHTDataReference> createRepeated() => $pb.PbList<DHTDataReference>();
@$core.pragma('dart2js:noInline')
static DHTDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DHTDataReference>(create);
static DHTDataReference? _defaultInstance;
@$pb.TagNumber(1)
$0.TypedKey get dhtData => $_getN(0);
@$pb.TagNumber(1)
set dhtData($0.TypedKey v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasDhtData() => $_has(0);
@$pb.TagNumber(1)
void clearDhtData() => clearField(1);
@$pb.TagNumber(1)
$0.TypedKey ensureDhtData() => $_ensure(0);
@$pb.TagNumber(2)
$0.TypedKey get hash => $_getN(1);
@$pb.TagNumber(2)
set hash($0.TypedKey v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasHash() => $_has(1);
@$pb.TagNumber(2)
void clearHash() => clearField(2);
@$pb.TagNumber(2)
$0.TypedKey ensureHash() => $_ensure(1);
}
class BlockStoreDataReference extends $pb.GeneratedMessage {
factory BlockStoreDataReference() => create();
BlockStoreDataReference._() : super();
factory BlockStoreDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory BlockStoreDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BlockStoreDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create)
..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'block', subBuilder: $0.TypedKey.create)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
BlockStoreDataReference clone() => BlockStoreDataReference()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
BlockStoreDataReference copyWith(void Function(BlockStoreDataReference) updates) => super.copyWith((message) => updates(message as BlockStoreDataReference)) as BlockStoreDataReference;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static BlockStoreDataReference create() => BlockStoreDataReference._();
BlockStoreDataReference createEmptyInstance() => create();
static $pb.PbList<BlockStoreDataReference> createRepeated() => $pb.PbList<BlockStoreDataReference>();
@$core.pragma('dart2js:noInline')
static BlockStoreDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BlockStoreDataReference>(create);
static BlockStoreDataReference? _defaultInstance;
@$pb.TagNumber(1)
$0.TypedKey get block => $_getN(0);
@$pb.TagNumber(1)
set block($0.TypedKey v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasBlock() => $_has(0);
@$pb.TagNumber(1)
void clearBlock() => clearField(1);
@$pb.TagNumber(1)
$0.TypedKey ensureBlock() => $_ensure(0);
}
enum DataReference_Kind {
dhtData,
blockStoreData,
notSet
}
class DataReference extends $pb.GeneratedMessage {
factory DataReference() => create();
DataReference._() : super();
factory DataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory DataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static const $core.Map<$core.int, DataReference_Kind> _DataReference_KindByTag = {
1 : DataReference_Kind.dhtData,
2 : DataReference_Kind.blockStoreData,
0 : DataReference_Kind.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create)
..oo(0, [1, 2])
..aOM<DHTDataReference>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: DHTDataReference.create)
..aOM<BlockStoreDataReference>(2, _omitFieldNames ? '' : 'blockStoreData', subBuilder: BlockStoreDataReference.create)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
DataReference clone() => DataReference()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
DataReference copyWith(void Function(DataReference) updates) => super.copyWith((message) => updates(message as DataReference)) as DataReference;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static DataReference create() => DataReference._();
DataReference createEmptyInstance() => create();
static $pb.PbList<DataReference> createRepeated() => $pb.PbList<DataReference>();
@$core.pragma('dart2js:noInline')
static DataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DataReference>(create);
static DataReference? _defaultInstance;
DataReference_Kind whichKind() => _DataReference_KindByTag[$_whichOneof(0)]!;
void clearKind() => clearField($_whichOneof(0));
@$pb.TagNumber(1)
DHTDataReference get dhtData => $_getN(0);
@$pb.TagNumber(1)
set dhtData(DHTDataReference v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasDhtData() => $_has(0);
@$pb.TagNumber(1)
void clearDhtData() => clearField(1);
@$pb.TagNumber(1)
DHTDataReference ensureDhtData() => $_ensure(0);
@$pb.TagNumber(2)
BlockStoreDataReference get blockStoreData => $_getN(1);
@$pb.TagNumber(2)
set blockStoreData(BlockStoreDataReference v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasBlockStoreData() => $_has(1);
@$pb.TagNumber(2)
void clearBlockStoreData() => clearField(2);
@$pb.TagNumber(2)
BlockStoreDataReference ensureBlockStoreData() => $_ensure(1);
}
class OwnedDHTRecordPointer extends $pb.GeneratedMessage { class OwnedDHTRecordPointer extends $pb.GeneratedMessage {
factory OwnedDHTRecordPointer() => create(); factory OwnedDHTRecordPointer() => create();
OwnedDHTRecordPointer._() : super(); OwnedDHTRecordPointer._() : super();

View file

@ -60,51 +60,6 @@ final $typed_data.Uint8List dHTShortArrayDescriptor = $convert.base64Decode(
'Cg1ESFRTaG9ydEFycmF5EiQKBGtleXMYASADKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGtleXMSFA' 'Cg1ESFRTaG9ydEFycmF5EiQKBGtleXMYASADKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGtleXMSFA'
'oFaW5kZXgYAiABKAxSBWluZGV4EhIKBHNlcXMYAyADKA1SBHNlcXM='); 'oFaW5kZXgYAiABKAxSBWluZGV4EhIKBHNlcXMYAyADKA1SBHNlcXM=');
@$core.Deprecated('Use dHTDataReferenceDescriptor instead')
const DHTDataReference$json = {
'1': 'DHTDataReference',
'2': [
{'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'dhtData'},
{'1': 'hash', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'hash'},
],
};
/// Descriptor for `DHTDataReference`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List dHTDataReferenceDescriptor = $convert.base64Decode(
'ChBESFREYXRhUmVmZXJlbmNlEisKCGRodF9kYXRhGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5Ug'
'dkaHREYXRhEiQKBGhhc2gYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGhhc2g=');
@$core.Deprecated('Use blockStoreDataReferenceDescriptor instead')
const BlockStoreDataReference$json = {
'1': 'BlockStoreDataReference',
'2': [
{'1': 'block', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'block'},
],
};
/// Descriptor for `BlockStoreDataReference`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List blockStoreDataReferenceDescriptor = $convert.base64Decode(
'ChdCbG9ja1N0b3JlRGF0YVJlZmVyZW5jZRImCgVibG9jaxgBIAEoCzIQLnZlaWxpZC5UeXBlZE'
'tleVIFYmxvY2s=');
@$core.Deprecated('Use dataReferenceDescriptor instead')
const DataReference$json = {
'1': 'DataReference',
'2': [
{'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.dht.DHTDataReference', '9': 0, '10': 'dhtData'},
{'1': 'block_store_data', '3': 2, '4': 1, '5': 11, '6': '.dht.BlockStoreDataReference', '9': 0, '10': 'blockStoreData'},
],
'8': [
{'1': 'kind'},
],
};
/// Descriptor for `DataReference`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List dataReferenceDescriptor = $convert.base64Decode(
'Cg1EYXRhUmVmZXJlbmNlEjIKCGRodF9kYXRhGAEgASgLMhUuZGh0LkRIVERhdGFSZWZlcmVuY2'
'VIAFIHZGh0RGF0YRJIChBibG9ja19zdG9yZV9kYXRhGAIgASgLMhwuZGh0LkJsb2NrU3RvcmVE'
'YXRhUmVmZXJlbmNlSABSDmJsb2NrU3RvcmVEYXRhQgYKBGtpbmQ=');
@$core.Deprecated('Use ownedDHTRecordPointerDescriptor instead') @$core.Deprecated('Use ownedDHTRecordPointerDescriptor instead')
const OwnedDHTRecordPointer$json = { const OwnedDHTRecordPointer$json = {
'1': 'OwnedDHTRecordPointer', '1': 'OwnedDHTRecordPointer',

View file

@ -12,16 +12,19 @@ Uint8List jsonEncodeBytes(Object? object,
Uint8List.fromList( Uint8List.fromList(
utf8.encode(jsonEncode(object, toEncodable: toEncodable))); utf8.encode(jsonEncode(object, toEncodable: toEncodable)));
Future<Uint8List> jsonUpdateBytes<T>(T Function(dynamic) fromJson, Future<Uint8List?> jsonUpdateBytes<T>(T Function(dynamic) fromJson,
Uint8List? oldBytes, Future<T> Function(T?) update) async { Uint8List? oldBytes, Future<T?> Function(T?) update) async {
final oldObj = final oldObj =
oldBytes == null ? null : fromJson(jsonDecode(utf8.decode(oldBytes))); oldBytes == null ? null : fromJson(jsonDecode(utf8.decode(oldBytes)));
final newObj = await update(oldObj); final newObj = await update(oldObj);
if (newObj == null) {
return null;
}
return jsonEncodeBytes(newObj); return jsonEncodeBytes(newObj);
} }
Future<Uint8List> Function(Uint8List?) jsonUpdate<T>( Future<Uint8List?> Function(Uint8List?) jsonUpdate<T>(
T Function(dynamic) fromJson, Future<T> Function(T?) update) => T Function(dynamic) fromJson, Future<T?> Function(T?) update) =>
(oldBytes) => jsonUpdateBytes(fromJson, oldBytes, update); (oldBytes) => jsonUpdateBytes(fromJson, oldBytes, update);
T Function(Object?) genericFromJson<T>( T Function(Object?) genericFromJson<T>(

View file

@ -8,8 +8,7 @@ import 'package:protobuf/protobuf.dart';
import 'table_db.dart'; import 'table_db.dart';
class PersistentQueue<T extends GeneratedMessage> class PersistentQueue<T extends GeneratedMessage>
/*extends Cubit<AsyncValue<IList<T>>>*/ with with TableDBBackedFromBuffer<IList<T>> {
TableDBBackedFromBuffer<IList<T>> {
// //
PersistentQueue( PersistentQueue(
{required String table, {required String table,

View file

@ -2,16 +2,19 @@ import 'dart:typed_data';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart';
Future<Uint8List> protobufUpdateBytes<T extends GeneratedMessage>( Future<Uint8List?> protobufUpdateBytes<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, T Function(List<int>) fromBuffer,
Uint8List? oldBytes, Uint8List? oldBytes,
Future<T> Function(T?) update) async { Future<T?> Function(T?) update) async {
final oldObj = oldBytes == null ? null : fromBuffer(oldBytes); final oldObj = oldBytes == null ? null : fromBuffer(oldBytes);
final newObj = await update(oldObj); final newObj = await update(oldObj);
if (newObj == null) {
return null;
}
return Uint8List.fromList(newObj.writeToBuffer()); return Uint8List.fromList(newObj.writeToBuffer());
} }
Future<Uint8List> Function(Uint8List?) Future<Uint8List?> Function(Uint8List?)
protobufUpdate<T extends GeneratedMessage>( protobufUpdate<T extends GeneratedMessage>(
T Function(List<int>) fromBuffer, Future<T> Function(T?) update) => T Function(List<int>) fromBuffer, Future<T?> Function(T?) update) =>
(oldBytes) => protobufUpdateBytes(fromBuffer, oldBytes, update); (oldBytes) => protobufUpdateBytes(fromBuffer, oldBytes, update);

View file

@ -39,7 +39,7 @@ packages:
path: "../../../dart_async_tools" path: "../../../dart_async_tools"
relative: true relative: true
source: path source: path
version: "0.1.1" version: "0.1.3"
bloc: bloc:
dependency: "direct main" dependency: "direct main"
description: description:
@ -54,7 +54,7 @@ packages:
path: "../../../bloc_advanced_tools" path: "../../../bloc_advanced_tools"
relative: true relative: true
source: path source: path
version: "0.1.1" version: "0.1.3"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:

View file

@ -7,9 +7,9 @@ environment:
sdk: '>=3.2.0 <4.0.0' sdk: '>=3.2.0 <4.0.0'
dependencies: dependencies:
async_tools: ^0.1.2 async_tools: ^0.1.3
bloc: ^8.1.4 bloc: ^8.1.4
bloc_advanced_tools: ^0.1.2 bloc_advanced_tools: ^0.1.3
charcode: ^1.3.1 charcode: ^1.3.1
collection: ^1.18.0 collection: ^1.18.0
equatable: ^2.0.5 equatable: ^2.0.5
@ -24,11 +24,11 @@ dependencies:
# veilid: ^0.0.1 # veilid: ^0.0.1
path: ../../../veilid/veilid-flutter path: ../../../veilid/veilid-flutter
# dependency_overrides: dependency_overrides:
# async_tools: async_tools:
# path: ../../../dart_async_tools path: ../../../dart_async_tools
# bloc_advanced_tools: bloc_advanced_tools:
# path: ../../../bloc_advanced_tools path: ../../../bloc_advanced_tools
dev_dependencies: dev_dependencies:
build_runner: ^2.4.10 build_runner: ^2.4.10

View file

@ -60,11 +60,10 @@ packages:
async_tools: async_tools:
dependency: "direct main" dependency: "direct main"
description: description:
name: async_tools path: "../dart_async_tools"
sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684" relative: true
url: "https://pub.dev" source: path
source: hosted version: "0.1.3"
version: "0.1.2"
awesome_extensions: awesome_extensions:
dependency: "direct main" dependency: "direct main"
description: description:
@ -100,11 +99,10 @@ packages:
bloc_advanced_tools: bloc_advanced_tools:
dependency: "direct main" dependency: "direct main"
description: description:
name: bloc_advanced_tools path: "../bloc_advanced_tools"
sha256: "0cf9b3a73a67addfe22ec3f97a1ac240f6ad53870d6b21a980260f390d7901cd" relative: true
url: "https://pub.dev" source: path
source: hosted version: "0.1.3"
version: "0.1.2"
blurry_modal_progress_hud: blurry_modal_progress_hud:
dependency: "direct main" dependency: "direct main"
description: description:
@ -585,6 +583,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_zoom_drawer:
dependency: "direct main"
description:
name: flutter_zoom_drawer
sha256: "5a3708548868463fb36e0e3171761ab7cb513df88d2f14053802812d2e855060"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
form_builder_validators: form_builder_validators:
dependency: "direct main" dependency: "direct main"
description: description:
@ -857,6 +863,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0
url: "https://pub.dev"
source: hosted
version: "8.0.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e
url: "https://pub.dev"
source: hosted
version: "3.0.0"
pasteboard: pasteboard:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -11,12 +11,12 @@ dependencies:
animated_theme_switcher: ^2.0.10 animated_theme_switcher: ^2.0.10
ansicolor: ^2.0.2 ansicolor: ^2.0.2
archive: ^3.6.1 archive: ^3.6.1
async_tools: ^0.1.2 async_tools: ^0.1.3
awesome_extensions: ^2.0.16 awesome_extensions: ^2.0.16
badges: ^3.1.2 badges: ^3.1.2
basic_utils: ^5.7.0 basic_utils: ^5.7.0
bloc: ^8.1.4 bloc: ^8.1.4
bloc_advanced_tools: ^0.1.2 bloc_advanced_tools: ^0.1.3
blurry_modal_progress_hud: ^1.1.1 blurry_modal_progress_hud: ^1.1.1
change_case: ^2.1.0 change_case: ^2.1.0
charcode: ^1.3.1 charcode: ^1.3.1
@ -45,6 +45,7 @@ dependencies:
flutter_spinkit: ^5.2.1 flutter_spinkit: ^5.2.1
flutter_svg: ^2.0.10+1 flutter_svg: ^2.0.10+1
flutter_translate: ^4.1.0 flutter_translate: ^4.1.0
flutter_zoom_drawer: ^3.2.0
form_builder_validators: ^10.0.1 form_builder_validators: ^10.0.1
freezed_annotation: ^2.4.1 freezed_annotation: ^2.4.1
go_router: ^14.1.4 go_router: ^14.1.4
@ -56,6 +57,7 @@ dependencies:
meta: ^1.12.0 meta: ^1.12.0
mobile_scanner: ^5.1.1 mobile_scanner: ^5.1.1
motion_toast: ^2.10.0 motion_toast: ^2.10.0
package_info_plus: ^8.0.0
pasteboard: ^0.2.0 pasteboard: ^0.2.0
path: ^1.9.0 path: ^1.9.0
path_provider: ^2.1.3 path_provider: ^2.1.3
@ -91,11 +93,11 @@ dependencies:
xterm: ^4.0.0 xterm: ^4.0.0
zxing2: ^0.2.3 zxing2: ^0.2.3
# dependency_overrides: dependency_overrides:
# async_tools: async_tools:
# path: ../dart_async_tools path: ../dart_async_tools
# bloc_advanced_tools: bloc_advanced_tools:
# path: ../bloc_advanced_tools path: ../bloc_advanced_tools
# flutter_chat_ui: # flutter_chat_ui:
# path: ../flutter_chat_ui # path: ../flutter_chat_ui