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": {
"title": "VeilidChat"
},
"app_bar": {
"settings_tooltip": "Settings"
"menu": {
"settings_tooltip": "Settings",
"add_account_tooltip": "Add Account"
},
"pager": {
"chats": "Chats",
@ -18,7 +19,7 @@
"lock_type_password": "password"
},
"new_account_page": {
"titlebar": "Create a new account",
"titlebar": "Create A New Account",
"header": "Account Profile",
"create": "Create",
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.",
@ -26,12 +27,37 @@
"name": "Name",
"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": {
"ok": "Ok",
"cancel": "Cancel",
"delete": "Delete",
"accept": "Accept",
"reject": "Reject",
"finish": "Finish",
"waiting_for_network": "Waiting For Network"
},
"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 'package:protobuf/protobuf.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../proto/proto.dart' as proto;
import '../account_manager.dart';
class AccountRecordCubit extends DefaultDHTRecordCubit<proto.Account> {
AccountRecordCubit({
required super.open,
}) : super(decodeState: proto.Account.fromBuffer);
typedef AccountRecordState = proto.Account;
/// The saved state of a VeilidChat Account on the DHT
/// 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
Future<void> close() async {
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:veilid_support/veilid_support.dart';
import '../repository/account_repository/account_repository.dart';
import '../repository/account_repository.dart';
class ActiveLocalAccountCubit extends Cubit<TypedKey?> {
ActiveLocalAccountCubit(AccountRepository accountRepository)

View file

@ -1,4 +1,7 @@
export 'account_info_cubit.dart';
export 'account_record_cubit.dart';
export 'active_local_account_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';

View file

@ -1,12 +1,17 @@
import 'dart:async';
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:veilid_support/veilid_support.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)
: _accountRepository = accountRepository,
super(accountRepository.getLocalAccounts()) {
@ -30,6 +35,14 @@ class LocalAccountsCubit extends Cubit<IList<LocalAccount>> {
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;
late final StreamSubscription<AccountRepositoryChange>
_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 '../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)
: _accountRepository = accountRepository,
super(accountRepository.getUserLogins()) {
@ -29,6 +31,7 @@ class UserLoginsCubit extends Cubit<IList<UserLogin>> {
await super.close();
await _accountRepositorySubscription.cancel();
}
////////////////////////////////////////////////////////////////////////////
final AccountRepository _accountRepository;
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 {
noAccount,
accountInvalid,
accountLocked,
accountReady,
accountUnlocked,
}
@immutable
class AccountInfo {
class AccountInfo extends Equatable {
const AccountInfo({
required this.status,
required this.active,
required this.activeAccountInfo,
required this.localAccount,
required this.userLogin,
});
final AccountInfoStatus status;
final bool active;
final ActiveAccountInfo? activeAccountInfo;
final LocalAccount localAccount;
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 'active_account_info.dart';
export 'encryption_key_type.dart';
export 'local_account/local_account.dart';
export 'new_profile_spec.dart';
export 'per_account_collection_state/per_account_collection_state.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:veilid_support/veilid_support.dart';
import '../../../../proto/proto.dart' as proto;
import '../../../tools/tools.dart';
import '../../models/models.dart';
import '../../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../models/models.dart';
const String veilidChatAccountKey = 'com.veilid.veilidchat';
@ -45,19 +45,6 @@ class AccountRepository {
valueToJson: (val) => val?.toJson(),
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 {
await _localAccounts.get();
await _userLogins.get();
@ -71,12 +58,10 @@ class AccountRepository {
}
//////////////////////////////////////////////////////////////
/// Streams
/// Public Interface
///
Stream<AccountRepositoryChange> get stream => _streamController.stream;
//////////////////////////////////////////////////////////////
/// Selectors
IList<LocalAccount> getLocalAccounts() => _localAccounts.value;
TypedKey? getActiveLocalAccount() => _activeLocalAccount.value;
IList<UserLogin> getUserLogins() => _userLogins.value;
@ -107,29 +92,11 @@ class AccountRepository {
return userLogins[idx];
}
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;
AccountInfo? getAccountInfo(TypedKey superIdentityRecordKey) {
// Get which local account we want to fetch the profile for
final localAccount = fetchLocalAccount(superIdentityRecordKey);
if (localAccount == null) {
// account does not exist
return AccountInfo(
status: AccountInfoStatus.noAccount,
active: active,
activeAccountInfo: null);
return null;
}
// See if we've logged into this account or if it is locked
@ -137,23 +104,20 @@ class AccountRepository {
if (userLogin == null) {
// Account was locked
return AccountInfo(
status: AccountInfoStatus.accountLocked,
active: active,
activeAccountInfo: null);
status: AccountInfoStatus.accountLocked,
localAccount: localAccount,
userLogin: null,
);
}
// Got account, decrypted and decoded
return AccountInfo(
status: AccountInfoStatus.accountReady,
active: active,
activeAccountInfo:
ActiveAccountInfo(localAccount: localAccount, userLogin: userLogin),
status: AccountInfoStatus.accountUnlocked,
localAccount: localAccount,
userLogin: userLogin,
);
}
//////////////////////////////////////////////////////////////
/// Mutators
/// Reorder accounts
Future<void> reorderAccount(int oldIndex, int newIndex) async {
final localAccounts = await _localAccounts.get();
@ -168,104 +132,39 @@ class AccountRepository {
/// Creates a new super identity, an identity instance, an account associated
/// with the identity instance, stores the account in the identity key and
/// 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');
final wsi = await WritableSuperIdentity.create();
try {
final localAccount = await _newLocalAccount(
superIdentity: wsi.superIdentity,
identitySecret: wsi.identitySecret,
newProfileSpec: newProfileSpec);
newProfile: newProfile);
// Log in the new account by default with no pin
final ok = await login(
localAccount.superIdentity.recordKey, EncryptionKeyType.none, '');
assert(ok, 'login with none should never fail');
return wsi.superSecret;
} on Exception catch (_) {
await wsi.delete();
rethrow;
}
}
/// 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 NewProfileSpec newProfileSpec,
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
String encryptionKey = ''}) async {
log.debug('Creating new local account');
Future<void> editAccountProfile(
TypedKey superIdentityRecordKey, proto.Profile newProfile) async {
log.debug('Editing profile for $superIdentityRecordKey');
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 = (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);
final newLocalAccounts = localAccounts.replaceFirstWhere(
(x) => x.superIdentity.recordKey == superIdentityRecordKey,
(localAccount) => localAccount!.copyWith(name: newProfile.name));
await _localAccounts.set(newLocalAccounts);
_streamController.add(AccountRepositoryChange.localAccounts);
// Return local account object
return localAccount;
}
/// Remove an account and wipe the messages for this account from this device
@ -307,6 +206,88 @@ class AccountRepository {
_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(
SuperIdentity superIdentity, SecretKey identitySecret) async {
// Verify identity secret works and return the valid cryptosystem
@ -399,16 +380,13 @@ class AccountRepository {
_streamController.add(AccountRepositoryChange.userLogins);
}
Future<DHTRecord> openAccountRecord(UserLogin userLogin) async {
final localAccount = fetchLocalAccount(userLogin.superIdentityRecordKey)!;
//////////////////////////////////////////////////////////////
/// Fields
// Record not yet open, do it
final pool = DHTRecordPool.instance;
final record = await pool.openRecordOwned(
userLogin.accountRecordInfo.accountRecord,
debugName: 'AccountRepository::openAccountRecord::AccountRecord',
parent: localAccount.superIdentity.currentInstance.recordKey);
static AccountRepository instance = AccountRepository._();
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: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:form_builder_validators/form_builder_validators.dart';
import 'package:go_router/go_router.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 NewAccountPage extends StatefulWidget {
const NewAccountPage({super.key});
@override
NewAccountPageState createState() => NewAccountPageState();
State createState() => _NewAccountPageState();
}
class NewAccountPageState extends State<NewAccountPage> {
final _formKey = GlobalKey<FormBuilderState>();
late bool isInAsyncCall = false;
static const String formFieldName = 'name';
static const String formFieldPronouns = 'pronouns';
class _NewAccountPageState extends State<NewAccountPage> {
bool _isInAsyncCall = false;
@override
void initState() {
@ -47,80 +44,35 @@ class NewAccountPageState extends State<NewAccountPage> {
false;
final canSubmit = networkReady;
return FormBuilder(
key: _formKey,
child: ListView(
children: [
Text(translate('new_account_page.header'))
.textStyle(context.headlineSmall)
.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(),
],
),
);
return EditProfileForm(
header: translate('new_account_page.header'),
instructions: translate('new_account_page.instructions'),
submitText: translate('new_account_page.create'),
submitDisabledText: translate('button.waiting_for_network'),
onSubmit: !canSubmit ? null : onSubmit);
}
@override
Widget build(BuildContext context) {
final displayModalHUD = isInAsyncCall;
final displayModalHUD = _isInAsyncCall;
return Scaffold(
// resizeToAvoidBottomInset: false,
appBar: DefaultAppBar(
title: Text(translate('new_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('app_bar.settings_tooltip'),
tooltip: translate('menu.settings_tooltip'),
onPressed: () async {
await GoRouterHelper(context).push('/settings');
})
@ -132,16 +84,33 @@ class NewAccountPageState extends State<NewAccountPage> {
FocusScope.of(context).unfocus();
try {
final name =
_formKey.currentState!.fields[formFieldName]!.value as String;
final pronouns = _formKey.currentState!.fields[formFieldPronouns]!
final name = formKey.currentState!
.fields[EditProfileForm.formFieldName]!.value as String;
final pronouns = formKey
.currentState!
.fields[EditProfileForm.formFieldPronouns]!
.value as String? ??
'';
final newProfileSpec =
NewProfileSpec(name: name, pronouns: pronouns);
final newProfile = proto.Profile()
..name = name
..pronouns = pronouns;
await AccountRepository.instance
.createWithNewSuperIdentity(newProfileSpec);
setState(() {
_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) {
if (context.mounted) {
await showErrorModal(context, translate('new_account_page.error'),
@ -152,10 +121,4 @@ class NewAccountPageState extends State<NewAccountPage> {
).paddingSymmetric(horizontal: 24, vertical: 8),
).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 '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 'account_manager/account_manager.dart';
import 'account_manager/cubits/active_local_account_cubit.dart';
import 'init.dart';
import 'layout/splash.dart';
import 'router/router.dart';
@ -129,7 +130,11 @@ class VeilidChatApp extends StatelessWidget {
BlocProvider<PreferencesCubit>(
create: (context) =>
PreferencesCubit(PreferencesRepository.instance),
)
),
BlocProvider<PerAccountCollectionBlocMapCubit>(
create: (context) => PerAccountCollectionBlocMapCubit(
accountRepository: AccountRepository.instance,
locator: context.read)),
],
child: BackgroundTicker(
child: _buildShortcuts(
@ -137,7 +142,7 @@ class VeilidChatApp extends StatelessWidget {
builder: (context) => MaterialApp.router(
debugShowCheckedModeBanner: false,
routerConfig:
context.watch<RouterCubit>().router(),
context.read<RouterCubit>().router(),
title: translate('app.title'),
theme: theme,
localizationsDelegates: [

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:typed_data';
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:fixnum/fixnum.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 '../../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 '../models/chat_component_state.dart';
import '../models/message_state.dart';
@ -23,18 +25,27 @@ const metadataKeyIdentityPublicKey = 'identityPublicKey';
const metadataKeyExpirationDuration = 'expiration';
const metadataKeyViewLimit = 'view_limit';
const metadataKeyAttachments = 'attachments';
const _sfChangedContacts = 'changedContacts';
class ChatComponentCubit extends Cubit<ChatComponentState> {
ChatComponentCubit._({
required AccountInfo accountInfo,
required AccountRecordCubit accountRecordCubit,
required ContactListCubit contactListCubit,
required List<ActiveConversationCubit> conversationCubits,
required SingleContactMessagesCubit messagesCubit,
required types.User localUser,
required IMap<TypedKey, types.User> remoteUsers,
}) : _messagesCubit = messagesCubit,
}) : _accountInfo = accountInfo,
_accountRecordCubit = accountRecordCubit,
_contactListCubit = contactListCubit,
_conversationCubits = conversationCubits,
_messagesCubit = messagesCubit,
super(ChatComponentState(
chatKey: GlobalKey<ChatState>(),
scrollController: AutoScrollController(),
localUser: localUser,
remoteUsers: remoteUsers,
localUser: null,
remoteUsers: const IMap.empty(),
historicalRemoteUsers: const IMap.empty(),
unknownUsers: const IMap.empty(),
messageWindow: const AsyncLoading(),
title: '',
)) {
@ -42,54 +53,49 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
_initWait.add(_init);
}
// ignore: prefer_constructors_over_static_methods
static ChatComponentCubit singleContact(
{required ActiveAccountInfo activeAccountInfo,
required proto.Account accountRecordInfo,
required ActiveConversationState activeConversationState,
required SingleContactMessagesCubit messagesCubit}) {
// Make local 'User'
final localUserIdentityKey = activeAccountInfo.identityTypedPublicKey;
final localUser = types.User(
id: localUserIdentityKey.toString(),
firstName: accountRecordInfo.profile.name,
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,
localUser: localUser,
remoteUsers: remoteUsers,
);
}
factory ChatComponentCubit.singleContact(
{required AccountInfo accountInfo,
required AccountRecordCubit accountRecordCubit,
required ContactListCubit contactListCubit,
required ActiveConversationCubit activeConversationCubit,
required SingleContactMessagesCubit messagesCubit}) =>
ChatComponentCubit._(
accountInfo: accountInfo,
accountRecordCubit: accountRecordCubit,
contactListCubit: contactListCubit,
conversationCubits: [activeConversationCubit],
messagesCubit: messagesCubit,
);
Future<void> _init() async {
_messagesSubscription = _messagesCubit.stream.listen((messagesState) {
emit(state.copyWith(
messageWindow: _convertMessages(messagesState),
));
});
emit(state.copyWith(
messageWindow: _convertMessages(_messagesCubit.state),
title: _getTitle(),
));
// Get local user info and account record cubit
_localUserIdentityKey = _accountInfo.identityTypedPublicKey;
// Subscribe to local user info
_accountRecordSubscription =
_accountRecordCubit.stream.listen(_onChangedAccountRecord);
_onChangedAccountRecord(_accountRecordCubit.state);
// 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
Future<void> close() async {
await _initWait();
await _contactListSubscription.cancel();
await _accountRecordSubscription.cancel();
await _messagesSubscription.cancel();
await _conversationSubscriptions.values.map((v) => v.cancel()).wait;
await super.close();
}
@ -153,23 +159,162 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
////////////////////////////////////////////////////////////////////////////
// Private Implementation
String _getTitle() {
if (state.remoteUsers.length == 1) {
final remoteUser = state.remoteUsers.values.first;
return remoteUser.firstName ?? '<unnamed>';
} else {
return '<group chat with ${state.remoteUsers.length} users>';
void _onChangedAccountRecord(AsyncValue<proto.Account> avAccount) {
// Update local 'User'
final account = avAccount.asData?.value;
if (account == null) {
emit(state.copyWith(localUser: null));
return;
}
final localUser = types.User(
id: _localUserIdentityKey.toString(),
firstName: account.profile.name,
metadata: {metadataKeyIdentityPublicKey: _localUserIdentityKey});
emit(state.copyWith(localUser: localUser));
}
types.Message? _messageStateToChatMessage(MessageState message) {
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});
}
}
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 author =
state.remoteUsers[authorIdentityPublicKey] ?? state.localUser;
late final types.User author;
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;
if (message.sendState != null) {
assert(author == state.localUser,
assert(author.id == _localUserIdentityKey.toString(),
'send state should only be on sent messages');
switch (message.sendState!) {
case MessageSendState.sending:
@ -192,7 +337,7 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
text: contextText.text,
showStatus: status != null,
status: status);
return textMessage;
return (currentState, textMessage);
case proto.Message_Kind.secret:
case proto.Message_Kind.delete:
case proto.Message_Kind.erase:
@ -201,17 +346,24 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
case proto.Message_Kind.membership:
case proto.Message_Kind.moderation:
case proto.Message_Kind.notSet:
return null;
return (currentState, null);
}
}
AsyncValue<WindowState<types.Message>> _convertMessages(
ChatComponentState _convertMessages(ChatComponentState currentState,
AsyncValue<WindowState<MessageState>> avMessagesState) {
// Clear out unknown users
currentState = state.copyWith(unknownUsers: const IMap.empty());
final asError = avMessagesState.asError;
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) {
return const AsyncValue.loading();
return currentState.copyWith(
unknownUsers: const IMap.empty(),
messageWindow: const AsyncValue.loading());
}
final messagesState = avMessagesState.asData!.value;
@ -219,7 +371,9 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
final chatMessages = <types.Message>[];
final tsSet = <String>{};
for (final message in messagesState.window) {
final chatMessage = _messageStateToChatMessage(message);
final (newState, chatMessage) =
_messageStateToChatMessage(currentState, message);
currentState = newState;
if (chatMessage == null) {
continue;
}
@ -232,12 +386,13 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
assert(false, 'should not have duplicate id');
}
}
return AsyncValue.data(WindowState<types.Message>(
window: chatMessages.toIList(),
length: messagesState.length,
windowTail: messagesState.windowTail,
windowCount: messagesState.windowCount,
follow: messagesState.follow));
return currentState.copyWith(
messageWindow: AsyncValue.data(WindowState<types.Message>(
window: chatMessages.toIList(),
length: messagesState.length,
windowTail: messagesState.windowTail,
windowCount: messagesState.windowCount,
follow: messagesState.follow)));
}
void _addTextMessage(
@ -265,7 +420,21 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
////////////////////////////////////////////////////////////////////////////
final _initWait = WaitSet<void>();
final AccountInfo _accountInfo;
final AccountRecordCubit _accountRecordCubit;
final ContactListCubit _contactListCubit;
final List<ActiveConversationCubit> _conversationCubits;
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<
BlocBusyState<
AsyncValue<IList<DHTShortArrayElementState<proto.Contact>>>>>
_contactListSubscription;
double scrollOffset = 0;
}

View file

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

View file

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

View file

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

View file

@ -21,8 +21,13 @@ mixin _$ChatComponentState {
throw _privateConstructorUsedError; // ScrollController for the chat
AutoScrollController get scrollController =>
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 =>
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
AsyncValue<WindowState<Message>> get messageWindow =>
throw _privateConstructorUsedError; // Title of the chat
@ -42,8 +47,10 @@ abstract class $ChatComponentStateCopyWith<$Res> {
$Res call(
{GlobalKey<ChatState> chatKey,
AutoScrollController scrollController,
User localUser,
User? localUser,
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
IMap<Typed<FixedEncodedString43>, User> unknownUsers,
AsyncValue<WindowState<Message>> messageWindow,
String title});
@ -65,8 +72,10 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState>
$Res call({
Object? chatKey = null,
Object? scrollController = null,
Object? localUser = null,
Object? localUser = freezed,
Object? remoteUsers = null,
Object? historicalRemoteUsers = null,
Object? unknownUsers = null,
Object? messageWindow = null,
Object? title = null,
}) {
@ -79,14 +88,22 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState>
? _value.scrollController
: scrollController // ignore: cast_nullable_to_non_nullable
as AutoScrollController,
localUser: null == localUser
localUser: freezed == localUser
? _value.localUser
: localUser // ignore: cast_nullable_to_non_nullable
as User,
as User?,
remoteUsers: null == remoteUsers
? _value.remoteUsers
: remoteUsers // ignore: cast_nullable_to_non_nullable
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
? _value.messageWindow
: messageWindow // ignore: cast_nullable_to_non_nullable
@ -119,8 +136,10 @@ abstract class _$$ChatComponentStateImplCopyWith<$Res>
$Res call(
{GlobalKey<ChatState> chatKey,
AutoScrollController scrollController,
User localUser,
User? localUser,
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
IMap<Typed<FixedEncodedString43>, User> unknownUsers,
AsyncValue<WindowState<Message>> messageWindow,
String title});
@ -141,8 +160,10 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res>
$Res call({
Object? chatKey = null,
Object? scrollController = null,
Object? localUser = null,
Object? localUser = freezed,
Object? remoteUsers = null,
Object? historicalRemoteUsers = null,
Object? unknownUsers = null,
Object? messageWindow = null,
Object? title = null,
}) {
@ -155,14 +176,22 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res>
? _value.scrollController
: scrollController // ignore: cast_nullable_to_non_nullable
as AutoScrollController,
localUser: null == localUser
localUser: freezed == localUser
? _value.localUser
: localUser // ignore: cast_nullable_to_non_nullable
as User,
as User?,
remoteUsers: null == remoteUsers
? _value.remoteUsers
: remoteUsers // ignore: cast_nullable_to_non_nullable
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
? _value.messageWindow
: messageWindow // ignore: cast_nullable_to_non_nullable
@ -183,6 +212,8 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
required this.scrollController,
required this.localUser,
required this.remoteUsers,
required this.historicalRemoteUsers,
required this.unknownUsers,
required this.messageWindow,
required this.title});
@ -194,10 +225,16 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
final AutoScrollController scrollController;
// Local user
@override
final User localUser;
// Remote users
final User? localUser;
// Active remote users
@override
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
@override
final AsyncValue<WindowState<Message>> messageWindow;
@ -207,7 +244,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
@override
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
@ -222,14 +259,26 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
other.localUser == localUser) &&
(identical(other.remoteUsers, remoteUsers) ||
other.remoteUsers == remoteUsers) &&
(identical(other.historicalRemoteUsers, historicalRemoteUsers) ||
other.historicalRemoteUsers == historicalRemoteUsers) &&
(identical(other.unknownUsers, unknownUsers) ||
other.unknownUsers == unknownUsers) &&
(identical(other.messageWindow, messageWindow) ||
other.messageWindow == messageWindow) &&
(identical(other.title, title) || other.title == title));
}
@override
int get hashCode => Object.hash(runtimeType, chatKey, scrollController,
localUser, remoteUsers, messageWindow, title);
int get hashCode => Object.hash(
runtimeType,
chatKey,
scrollController,
localUser,
remoteUsers,
historicalRemoteUsers,
unknownUsers,
messageWindow,
title);
@JsonKey(ignore: true)
@override
@ -243,8 +292,11 @@ abstract class _ChatComponentState implements ChatComponentState {
const factory _ChatComponentState(
{required final GlobalKey<ChatState> chatKey,
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>
historicalRemoteUsers,
required final IMap<Typed<FixedEncodedString43>, User> unknownUsers,
required final AsyncValue<WindowState<Message>> messageWindow,
required final String title}) = _$ChatComponentStateImpl;
@ -253,9 +305,13 @@ abstract class _ChatComponentState implements ChatComponentState {
@override // ScrollController for the chat
AutoScrollController get scrollController;
@override // Local user
User get localUser;
@override // Remote users
User? get localUser;
@override // Active remote users
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
AsyncValue<WindowState<Message>> get messageWindow;
@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 '../../account_manager/account_manager.dart';
import '../../chat_list/chat_list.dart';
import '../../contacts/contacts.dart';
import '../../conversation/conversation.dart';
import '../../theme/theme.dart';
import '../chat.dart';
@ -22,31 +23,29 @@ class ChatComponentWidget extends StatelessWidget {
static Widget builder(
{required TypedKey localConversationRecordKey, Key? key}) =>
Builder(builder: (context) {
// Get all watched dependendies
final activeAccountInfo = context.watch<ActiveAccountInfo>();
final accountRecordInfo =
context.watch<AccountRecordCubit>().state.asData?.value;
if (accountRecordInfo == null) {
return debugPage('should always have an account record here');
}
// Get the account info
final accountInfo = context.watch<AccountInfoCubit>().state;
final avconversation = context.select<ActiveConversationsBlocMapCubit,
AsyncValue<ActiveConversationState>?>(
(x) => x.state[localConversationRecordKey]);
if (avconversation == null) {
// Get the account record cubit
final accountRecordCubit = context.read<AccountRecordCubit>();
// 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();
}
final activeConversationState = avconversation.asData?.value;
if (activeConversationState == null) {
return avconversation.buildNotData();
}
// Get the messages cubit
final messagesCubit = context.select<
ActiveSingleContactChatBlocMapCubit,
SingleContactMessagesCubit?>(
(x) => x.tryOperate(localConversationRecordKey,
(x) => x.tryOperateSync(localConversationRecordKey,
closure: (cubit) => cubit));
if (messagesCubit == null) {
return waitingPage();
@ -54,10 +53,12 @@ class ChatComponentWidget extends StatelessWidget {
// Make chat component state
return BlocProvider(
key: key,
create: (context) => ChatComponentCubit.singleContact(
activeAccountInfo: activeAccountInfo,
accountRecordInfo: accountRecordInfo,
activeConversationState: activeConversationState,
accountInfo: accountInfo,
accountRecordCubit: accountRecordCubit,
contactListCubit: contactListCubit,
activeConversationCubit: activeConversationCubit,
messagesCubit: messagesCubit,
),
child: ChatComponentWidget._(key: key));
@ -159,6 +160,11 @@ class ChatComponentWidget extends StatelessWidget {
final chatComponentCubit = context.watch<ChatComponentCubit>();
final chatComponentState = chatComponentCubit.state;
final localUser = chatComponentState.localUser;
if (localUser == null) {
return waitingPage();
}
final messageWindow = chatComponentState.messageWindow.asData?.value;
if (messageWindow == null) {
return chatComponentState.messageWindow.buildNotData();
@ -281,7 +287,7 @@ class ChatComponentWidget extends StatelessWidget {
_handleSendPressed(chatComponentCubit, pt),
//showUserAvatars: false,
//showUserNames: true,
user: chatComponentState.localUser,
user: localUser,
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 '../../chat/chat.dart';
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>
with StateMapFollowable<ChatListCubitState, TypedKey, proto.Chat> {
ChatListCubit({
required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
required this.activeChatCubit,
}) : super(
open: () => _open(activeAccountInfo, account),
required AccountInfo accountInfo,
required OwnedDHTRecordPointer chatListRecordPointer,
required ActiveChatCubit activeChatCubit,
}) : _activeChatCubit = activeChatCubit,
super(
open: () => _open(accountInfo, chatListRecordPointer),
decodeElement: proto.Chat.fromBuffer);
static Future<DHTShortArray> _open(
ActiveAccountInfo activeAccountInfo, proto.Account account) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final chatListRecordKey = account.chatList.toVeilid();
final dhtRecord = await DHTShortArray.openOwned(chatListRecordKey,
debugName: 'ChatListCubit::_open::ChatList', parent: accountRecordKey);
static Future<DHTShortArray> _open(AccountInfo accountInfo,
OwnedDHTRecordPointer chatListRecordPointer) async {
final dhtRecord = await DHTShortArray.openOwned(chatListRecordPointer,
debugName: 'ChatListCubit::_open::ChatList',
parent: accountInfo.accountRecordKey);
return dhtRecord;
}
Future<proto.ChatSettings> getDefaultChatSettings(
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()
..title = '${contact.editedProfile.name}$pronouns'
..title = '${contact.displayName}$pronouns'
..description = ''
..defaultExpiration = Int64.ZERO;
}
@ -57,12 +53,24 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
// Make local copy so we don't share the buffer
final localConversationRecordKey =
contact.localConversationRecordKey.toVeilid();
final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
final remoteConversationRecordKey =
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
// if this fails, don't keep retrying, user can try again later
await operateWrite((writer) async {
await operateWriteEventual((writer) async {
// See if we have added this chat already
for (var i = 0; i < writer.length; i++) {
final cbuf = await writer.get(i);
@ -70,19 +78,27 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
throw Exception('Failed to get chat');
}
final c = proto.Chat.fromBuffer(cbuf);
if (c.localConversationRecordKey ==
contact.localConversationRecordKey) {
// Nothing to do here
return;
switch (c.whichKind()) {
case proto.Chat_Kind.direct:
if (c.direct.localConversationRecordKey ==
contact.localConversationRecordKey) {
// Nothing to do here
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
await writer.add(chat.writeToBuffer());
});
@ -91,41 +107,23 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
/// Delete a chat
Future<void> deleteChat(
{required TypedKey localConversationRecordKey}) async {
final localConversationRecordKeyProto =
localConversationRecordKey.toProto();
// Remove Chat from account's list
// if this fails, don't keep retrying, user can try again later
final deletedItem =
// Ensure followers get their changes before we return
await syncFollowers(() => operateWrite((writer) async {
if (activeChatCubit.state == localConversationRecordKey) {
activeChatCubit.setActiveChat(null);
}
for (var i = 0; i < writer.length; i++) {
final c = await writer.getProtobuf(proto.Chat.fromBuffer, i);
if (c == null) {
throw Exception('Failed to get chat');
}
if (c.localConversationRecordKey ==
localConversationRecordKeyProto) {
// Found the right chat
await writer.remove(i);
return c;
}
}
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);
await operateWriteEventual((writer) async {
if (_activeChatCubit.state == localConversationRecordKey) {
_activeChatCubit.setActiveChat(null);
}
}
for (var i = 0; i < writer.length; i++) {
final c = await writer.getProtobuf(proto.Chat.fromBuffer, i);
if (c == null) {
throw Exception('Failed to get chat');
}
if (c.localConversationRecordKey == localConversationRecordKey) {
await writer.remove(i);
return;
}
}
});
}
/// StateMapFollowable /////////////////////////
@ -136,9 +134,11 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
return IMap();
}
return IMap.fromIterable(stateValue,
keyMapper: (e) => e.value.localConversationRecordKey.toVeilid(),
keyMapper: (e) => e.value.localConversationRecordKey,
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';

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();
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(
key: ObjectKey(_contact),
disabled: _disabled,
selected: selected,
tileScale: ScaleKind.secondary,
title: _contact.editedProfile.name,
subtitle: _contact.editedProfile.pronouns,
title: title,
subtitle: subtitle,
icon: Icons.chat,
onTap: () {
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_list_widget.dart';
export 'empty_chat_list_widget.dart';

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart';
import '../../conversation/conversation.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../models/accepted_contact.dart';
@ -24,25 +24,27 @@ class InvitationStatus extends Equatable {
class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
proto.SignedContactResponse?> {
WaitingInvitationCubit(ContactRequestInboxCubit super.input,
{required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
required proto.ContactInvitationRecord contactInvitationRecord})
: super(
WaitingInvitationCubit(
ContactRequestInboxCubit super.input, {
required AccountInfo accountInfo,
required AccountRecordCubit accountRecordCubit,
required proto.ContactInvitationRecord contactInvitationRecord,
}) : super(
transform: (signedContactResponse) => _transform(
signedContactResponse,
activeAccountInfo: activeAccountInfo,
account: account,
accountInfo: accountInfo,
accountRecordCubit: accountRecordCubit,
contactInvitationRecord: contactInvitationRecord));
static Future<AsyncValue<InvitationStatus>> _transform(
proto.SignedContactResponse? signedContactResponse,
{required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
{required AccountInfo accountInfo,
required AccountRecordCubit accountRecordCubit,
required proto.ContactInvitationRecord contactInvitationRecord}) async {
if (signedContactResponse == null) {
return const AsyncValue.loading();
}
final contactResponseBytes =
Uint8List.fromList(signedContactResponse.contactResponse);
final contactResponse =
@ -57,8 +59,11 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
// Verify
final idcs = await contactSuperIdentity.currentInstance.cryptoSystem;
final signature = signedContactResponse.identitySignature.toVeilid();
await idcs.verify(contactSuperIdentity.currentInstance.publicKey,
contactResponseBytes, signature);
if (!await idcs.verify(contactSuperIdentity.currentInstance.publicKey,
contactResponseBytes, signature)) {
// Could not verify signature of contact response
return AsyncValue.error('Invalid signature on contact response.');
}
// Check for rejection
if (!contactResponse.accept) {
@ -71,7 +76,7 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
contactResponse.remoteConversationRecordKey.toVeilid();
final conversation = ConversationCubit(
activeAccountInfo: activeAccountInfo,
accountInfo: accountInfo,
remoteIdentityPublicKey:
contactSuperIdentity.currentInstance.typedPublicKey,
remoteConversationRecordKey: remoteConversationRecordKey);
@ -98,16 +103,13 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
final localConversationRecordKey =
contactInvitationRecord.localConversationRecordKey.toVeilid();
return conversation.initLocalConversation(
profile: accountRecordCubit.state.asData!.value.profile,
existingConversationRecordKey: localConversationRecordKey,
profile: account.profile,
// ignore: prefer_expression_function_bodies
callback: (localConversation) async {
return AsyncValue.data(InvitationStatus(
acceptedContact: AcceptedContact(
remoteProfile: remoteProfile,
remoteIdentity: contactSuperIdentity,
remoteConversationRecordKey: remoteConversationRecordKey,
localConversationRecordKey: localConversationRecordKey)));
});
callback: (localConversation) async => AsyncValue.data(InvitationStatus(
acceptedContact: AcceptedContact(
remoteProfile: remoteProfile,
remoteIdentity: contactSuperIdentity,
remoteConversationRecordKey: remoteConversationRecordKey,
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 '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import 'cubits.dart';
@ -18,7 +19,27 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
StateMapFollower<DHTShortArrayBusyState<proto.ContactInvitationRecord>,
TypedKey, proto.ContactInvitationRecord> {
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(
{required proto.ContactInvitationRecord
@ -27,22 +48,66 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(),
WaitingInvitationCubit(
ContactRequestInboxCubit(
activeAccountInfo: activeAccountInfo,
accountInfo: _accountInfo,
contactInvitationRecord: contactInvitationRecord),
activeAccountInfo: activeAccountInfo,
account: account,
accountInfo: _accountInfo,
accountRecordCubit: _accountRecordCubit,
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 /////////////////////////
@override
Future<void> removeFromState(TypedKey key) => remove(key);
@override
Future<void> updateState(TypedKey key, proto.ContactInvitationRecord value) =>
_addWaitingInvitation(contactInvitationRecord: value);
Future<void> updateState(
TypedKey key,
proto.ContactInvitationRecord? oldValue,
proto.ContactInvitationRecord newValue) async {
await _addWaitingInvitation(contactInvitationRecord: newValue);
}
////
final ActiveAccountInfo activeAccountInfo;
final proto.Account account;
final AccountInfo _accountInfo;
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 '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart';
import '../../conversation/conversation.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import 'models.dart';
@ -13,14 +13,12 @@ import 'models.dart';
class ValidContactInvitation {
@internal
ValidContactInvitation(
{required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
{required AccountInfo accountInfo,
required TypedKey contactRequestInboxKey,
required proto.ContactRequestPrivate contactRequestPrivate,
required SuperIdentity contactSuperIdentity,
required KeyPair writer})
: _activeAccountInfo = activeAccountInfo,
_account = account,
: _accountInfo = accountInfo,
_contactRequestInboxKey = contactRequestInboxKey,
_contactRequestPrivate = contactRequestPrivate,
_contactSuperIdentity = contactSuperIdentity,
@ -28,44 +26,40 @@ class ValidContactInvitation {
proto.Profile get remoteProfile => _contactRequestPrivate.profile;
Future<AcceptedContact?> accept() async {
Future<AcceptedContact?> accept(proto.Profile profile) async {
final pool = DHTRecordPool.instance;
try {
// Ensure we don't delete this if we're trying to chat to self
// The initiating side will delete the records in deleteInvitation()
final isSelf = _contactSuperIdentity.currentInstance.publicKey ==
_activeAccountInfo.identityPublicKey;
final accountRecordKey = _activeAccountInfo.accountRecordKey;
_accountInfo.identityPublicKey;
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
debugName: 'ValidContactInvitation::accept::'
'ContactRequestInbox',
parent: accountRecordKey))
parent: pool.getParentRecordKey(_contactRequestInboxKey) ??
_accountInfo.accountRecordKey))
// ignore: prefer_expression_function_bodies
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
// Create local conversation key for this
// contact and send via contact response
final conversation = ConversationCubit(
activeAccountInfo: _activeAccountInfo,
accountInfo: _accountInfo,
remoteIdentityPublicKey:
_contactSuperIdentity.currentInstance.typedPublicKey);
return conversation.initLocalConversation(
profile: _account.profile,
profile: profile,
callback: (localConversation) async {
final contactResponse = proto.ContactResponse()
..accept = true
..remoteConversationRecordKey = localConversation.key.toProto()
..superIdentityRecordKey =
_activeAccountInfo.superIdentityRecordKey.toProto();
_accountInfo.superIdentityRecordKey.toProto();
final contactResponseBytes = contactResponse.writeToBuffer();
final cs = await pool.veilid
.getCryptoSystem(_contactRequestInboxKey.kind);
final identitySignature = await cs.sign(
_activeAccountInfo.identityWriter.key,
_activeAccountInfo.identityWriter.secret,
contactResponseBytes);
final cs = await _accountInfo.identityCryptoSystem;
final identitySignature = await cs.signWithKeyPair(
_accountInfo.identityWriter, contactResponseBytes);
final signedContactResponse = proto.SignedContactResponse()
..contactResponse = contactResponseBytes
@ -95,27 +89,22 @@ class ValidContactInvitation {
// Ensure we don't delete this if we're trying to chat to self
final isSelf = _contactSuperIdentity.currentInstance.publicKey ==
_activeAccountInfo.identityPublicKey;
final accountRecordKey = _activeAccountInfo.accountRecordKey;
_accountInfo.identityPublicKey;
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
debugName: 'ValidContactInvitation::reject::'
'ContactRequestInbox',
parent: accountRecordKey))
parent: _accountInfo.accountRecordKey))
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
final cs =
await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind);
final contactResponse = proto.ContactResponse()
..accept = false
..superIdentityRecordKey =
_activeAccountInfo.superIdentityRecordKey.toProto();
_accountInfo.superIdentityRecordKey.toProto();
final contactResponseBytes = contactResponse.writeToBuffer();
final identitySignature = await cs.sign(
_activeAccountInfo.identityWriter.key,
_activeAccountInfo.identityWriter.secret,
contactResponseBytes);
final cs = await _accountInfo.identityCryptoSystem;
final identitySignature = await cs.signWithKeyPair(
_accountInfo.identityWriter, contactResponseBytes);
final signedContactResponse = proto.SignedContactResponse()
..contactResponse = contactResponseBytes
@ -129,8 +118,7 @@ class ValidContactInvitation {
}
//
final ActiveAccountInfo _activeAccountInfo;
final proto.Account _account;
final AccountInfo _accountInfo;
final TypedKey _contactRequestInboxKey;
final SuperIdentity _contactSuperIdentity;
final KeyPair _writer;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -11,18 +10,9 @@ import '../contacts.dart';
class ContactItemWidget extends StatelessWidget {
const ContactItemWidget(
{required this.contact, required this.disabled, super.key});
final proto.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));
}
{required proto.Contact contact, required bool disabled, super.key})
: _disabled = disabled,
_contact = contact;
@override
// ignore: prefer_expression_function_bodies
@ -30,26 +20,44 @@ class ContactItemWidget extends StatelessWidget {
BuildContext context,
) {
final localConversationRecordKey =
contact.localConversationRecordKey.toVeilid();
_contact.localConversationRecordKey.toVeilid();
const selected = false; // xxx: eventually when we have selectable contacts:
// 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(
key: ObjectKey(contact),
key: ObjectKey(_contact),
disabled: tileDisabled,
selected: selected,
tileScale: ScaleKind.primary,
title: contact.editedProfile.name,
subtitle: contact.editedProfile.pronouns,
title: title,
subtitle: subtitle,
icon: Icons.person,
onTap: () async {
// Start a chat
final chatListCubit = context.read<ChatListCubit>();
await chatListCubit.getOrCreateChatSingleContact(contact: contact);
await chatListCubit.getOrCreateChatSingleContact(contact: _contact);
// Click over to chats
if (context.mounted) {
await MainPager.of(context)
@ -66,14 +74,20 @@ class ContactItemWidget extends StatelessWidget {
final contactListCubit = context.read<ContactListCubit>();
final chatListCubit = context.read<ChatListCubit>();
// Delete the contact itself
await contactListCubit.deleteContact(
localConversationRecordKey: localConversationRecordKey);
// Remove any chats for this contact
await chatListCubit.deleteChat(
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();
return contactList
.where((element) =>
element.editedProfile.name
element.nickname
.toLowerCase()
.contains(lowerValue) ||
element.editedProfile.pronouns
element.profile.name
.toLowerCase()
.contains(lowerValue) ||
element.profile.pronouns
.toLowerCase()
.contains(lowerValue))
.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:flutter_bloc/flutter_bloc.dart';
import 'package:meta/meta.dart';
import 'package:protobuf/protobuf.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
const _sfUpdateAccountChange = 'updateAccountChange';
@immutable
class ConversationState extends Equatable {
@ -27,32 +29,35 @@ class ConversationState extends Equatable {
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>> {
ConversationCubit(
{required ActiveAccountInfo activeAccountInfo,
{required AccountInfo accountInfo,
required TypedKey remoteIdentityPublicKey,
TypedKey? localConversationRecordKey,
TypedKey? remoteConversationRecordKey})
: _activeAccountInfo = activeAccountInfo,
: _accountInfo = accountInfo,
_localConversationRecordKey = localConversationRecordKey,
_remoteIdentityPublicKey = remoteIdentityPublicKey,
_remoteConversationRecordKey = remoteConversationRecordKey,
super(const AsyncValue.loading()) {
_identityWriter = _accountInfo.identityWriter;
if (_localConversationRecordKey != null) {
_initWait.add(() async {
await _setLocalConversation(() async {
final accountRecordKey = _activeAccountInfo
.userLogin.accountRecordInfo.accountRecord.recordKey;
// Open local record key if it is specified
final pool = DHTRecordPool.instance;
final crypto = await _cachedConversationCrypto();
final writer = _activeAccountInfo.identityWriter;
final writer = _identityWriter;
final record = await pool.openRecordWrite(
_localConversationRecordKey!, writer,
debugName: 'ConversationCubit::LocalConversation',
parent: accountRecordKey,
parent: accountInfo.accountRecordKey,
crypto: crypto);
return record;
});
});
@ -61,15 +66,13 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
if (_remoteConversationRecordKey != null) {
_initWait.add(() async {
await _setRemoteConversation(() async {
final accountRecordKey = _activeAccountInfo
.userLogin.accountRecordInfo.accountRecord.recordKey;
// Open remote record key if it is specified
final pool = DHTRecordPool.instance;
final crypto = await _cachedConversationCrypto();
final record = await pool.openRecordRead(_remoteConversationRecordKey,
debugName: 'ConversationCubit::RemoteConversation',
parent: accountRecordKey,
parent: pool.getParentRecordKey(_remoteConversationRecordKey) ??
accountInfo.accountRecordKey,
crypto: crypto);
return record;
});
@ -80,6 +83,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
@override
Future<void> close() async {
await _initWait();
await _accountSubscription?.cancel();
await _localSubscription?.cancel();
await _remoteSubscription?.cancel();
await _localConversationCubit?.close();
@ -88,6 +92,130 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
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) {
final newState = avconv.when(
data: (conv) {
@ -140,6 +268,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
open: open, decodeState: proto.Conversation.fromBuffer);
_localSubscription =
_localConversationCubit!.stream.listen(_updateLocalConversationState);
_updateLocalConversationState(_localConversationCubit!.state);
}
// Open remote converation key
@ -150,146 +279,16 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
open: open, decodeState: proto.Conversation.fromBuffer);
_remoteSubscription =
_remoteConversationCubit!.stream.listen(_updateRemoteConversationState);
}
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;
_updateRemoteConversationState(_remoteConversationCubit!.state);
}
// Initialize local messages
Future<T> _initLocalMessages<T>({
required ActiveAccountInfo activeAccountInfo,
required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationKey,
required FutureOr<T> Function(DHTLog) callback,
}) async {
final crypto =
await activeAccountInfo.makeConversationCrypto(remoteIdentityPublicKey);
final writer = activeAccountInfo.identityWriter;
final crypto = await _cachedConversationCrypto();
final writer = _identityWriter;
return (await DHTLog.create(
debugName: 'ConversationCubit::initLocalMessages::LocalMessages',
@ -299,47 +298,23 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
.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 {
var conversationCrypto = _conversationCrypto;
if (conversationCrypto != null) {
return conversationCrypto;
}
conversationCrypto = await _activeAccountInfo
.makeConversationCrypto(_remoteIdentityPublicKey);
conversationCrypto =
await _accountInfo.makeConversationCrypto(_remoteIdentityPublicKey);
_conversationCrypto = conversationCrypto;
return conversationCrypto;
}
final ActiveAccountInfo _activeAccountInfo;
////////////////////////////////////////////////////////////////////////////
// Fields
TypedKey get remoteIdentityPublicKey => _remoteIdentityPublicKey;
final AccountInfo _accountInfo;
late final KeyPair _identityWriter;
final TypedKey _remoteIdentityPublicKey;
TypedKey? _localConversationRecordKey;
final TypedKey? _remoteConversationRecordKey;
@ -347,9 +322,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit;
StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription;
StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription;
StreamSubscription<AsyncValue<proto.Account>>? _accountSubscription;
ConversationState _incrementalState = const ConversationState(
localConversation: null, remoteConversation: null);
//
VeilidCrypto? _conversationCrypto;
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_locked.dart';
export 'home_account_missing.dart';
export 'home_account_ready/home_account_ready.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
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_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_bloc/flutter_bloc.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 '../../../chat/chat.dart';
import '../../../proto/proto.dart' as proto;
import '../../../theme/theme.dart';
import '../../../tools/tools.dart';
import 'main_pager/main_pager.dart';
@ -29,14 +30,15 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
}
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 scale = theme.extension<ScaleScheme>()!;
return Column(children: <Widget>[
Row(children: [
IconButton(
icon: const Icon(Icons.settings),
icon: const Icon(Icons.menu),
color: scale.secondaryScale.borderText,
constraints: const BoxConstraints.expand(height: 64, width: 64),
style: ButtonStyle(
@ -44,13 +46,13 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
WidgetStateProperty.all(scale.primaryScale.hoverBorder),
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16))))),
tooltip: translate('app_bar.settings_tooltip'),
tooltip: translate('menu.settings_tooltip'),
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),
asyncValueBuilder(account,
(_, account) => ProfileWidget(profile: account.profile))
.expanded(),
ProfileWidget(profile: profile).expanded(),
]).paddingAll(8),
const MainPager().expanded()
]);
@ -70,8 +72,8 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
return const NoConversationWidget();
}
return ChatComponentWidget.builder(
localConversationRecordKey: activeChatLocalConversationKey,
);
localConversationRecordKey: activeChatLocalConversationKey,
key: ValueKey(activeChatLocalConversationKey));
}
// 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
Widget build(BuildContext context) {
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_translate/flutter_translate.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 '../../../../chat/chat.dart';
@ -117,7 +118,7 @@ class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
style: TextStyle(fontSize: 24),
),
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_translate/flutter_translate.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:stack_trace/stack_trace.dart';
import 'app.dart';
@ -45,6 +46,9 @@ void main() async {
fallbackLocale: 'en_US', supportedLocales: ['en_US']);
await initializeDateFormatting();
// Get package info
await initPackageInfo();
// Run the app
// Hot reloads will only restart this part, not Veilid
runApp(LocalizedApp(localizationDelegate,

View file

@ -29,3 +29,21 @@ extension MessageExt on proto.Message {
static int compareTimestamp(proto.Message a, proto.Message b) =>
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';
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 {
media,
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)
..aOS(1, _omitFieldNames ? '' : 'mime')
..aOS(2, _omitFieldNames ? '' : 'name')
..aOM<$1.DataReference>(3, _omitFieldNames ? '' : 'content', subBuilder: $1.DataReference.create)
..aOM<DataReference>(3, _omitFieldNames ? '' : 'content', subBuilder: DataReference.create)
..hasRequiredFields = false
;
@ -142,15 +313,15 @@ class AttachmentMedia extends $pb.GeneratedMessage {
void clearName() => clearField(2);
@$pb.TagNumber(3)
$1.DataReference get content => $_getN(2);
DataReference get content => $_getN(2);
@$pb.TagNumber(3)
set content($1.DataReference v) { setField(3, v); }
set content(DataReference v) { setField(3, v); }
@$pb.TagNumber(3)
$core.bool hasContent() => $_has(2);
@$pb.TagNumber(3)
void clearContent() => clearField(3);
@$pb.TagNumber(3)
$1.DataReference ensureContent() => $_ensure(2);
DataReference ensureContent() => $_ensure(2);
}
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)
..aOS(1, _omitFieldNames ? '' : 'title')
..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)
..hasRequiredFields = false
;
@ -321,15 +492,15 @@ class ChatSettings extends $pb.GeneratedMessage {
void clearDescription() => clearField(2);
@$pb.TagNumber(3)
$1.DataReference get icon => $_getN(2);
DataReference get icon => $_getN(2);
@$pb.TagNumber(3)
set icon($1.DataReference v) { setField(3, v); }
set icon(DataReference v) { setField(3, v); }
@$pb.TagNumber(3)
$core.bool hasIcon() => $_has(2);
@$pb.TagNumber(3)
void clearIcon() => clearField(3);
@$pb.TagNumber(3)
$1.DataReference ensureIcon() => $_ensure(2);
DataReference ensureIcon() => $_ensure(2);
@$pb.TagNumber(4)
$fixnum.Int64 get defaultExpiration => $_getI64(3);
@ -1084,16 +1255,15 @@ class Conversation extends $pb.GeneratedMessage {
$0.TypedKey ensureMessages() => $_ensure(2);
}
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);
class ChatMember extends $pb.GeneratedMessage {
factory ChatMember() => create();
ChatMember._() : super();
factory ChatMember.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(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)
..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatMember', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'remoteIdentityPublicKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
..hasRequiredFields = false
;
@ -1101,22 +1271,79 @@ class Chat extends $pb.GeneratedMessage {
'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);
ChatMember clone() => ChatMember()..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;
ChatMember copyWith(void Function(ChatMember) updates) => super.copyWith((message) => updates(message as ChatMember)) as ChatMember;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Chat create() => Chat._();
Chat createEmptyInstance() => create();
static $pb.PbList<Chat> createRepeated() => $pb.PbList<Chat>();
static ChatMember create() => ChatMember._();
ChatMember createEmptyInstance() => create();
static $pb.PbList<ChatMember> createRepeated() => $pb.PbList<ChatMember>();
@$core.pragma('dart2js:noInline')
static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Chat>(create);
static Chat? _defaultInstance;
static ChatMember getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ChatMember>(create);
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)
ChatSettings get settings => $_getN(0);
@ -1141,15 +1368,15 @@ class Chat extends $pb.GeneratedMessage {
$0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1);
@$pb.TagNumber(3)
$0.TypedKey get remoteConversationRecordKey => $_getN(2);
ChatMember get remoteMember => $_getN(2);
@$pb.TagNumber(3)
set remoteConversationRecordKey($0.TypedKey v) { setField(3, v); }
set remoteMember(ChatMember v) { setField(3, v); }
@$pb.TagNumber(3)
$core.bool hasRemoteConversationRecordKey() => $_has(2);
$core.bool hasRemoteMember() => $_has(2);
@$pb.TagNumber(3)
void clearRemoteConversationRecordKey() => clearField(3);
void clearRemoteMember() => clearField(3);
@$pb.TagNumber(3)
$0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2);
ChatMember ensureRemoteMember() => $_ensure(2);
}
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)
..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
..pc<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKeys', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create)
..aOM<Membership>(2, _omitFieldNames ? '' : 'membership', subBuilder: Membership.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
;
@ -1198,18 +1427,111 @@ class GroupChat extends $pb.GeneratedMessage {
ChatSettings ensureSettings() => $_ensure(0);
@$pb.TagNumber(2)
$0.TypedKey get localConversationRecordKey => $_getN(1);
Membership get membership => $_getN(1);
@$pb.TagNumber(2)
set localConversationRecordKey($0.TypedKey v) { setField(2, v); }
set membership(Membership v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasLocalConversationRecordKey() => $_has(1);
$core.bool hasMembership() => $_has(1);
@$pb.TagNumber(2)
void clearLocalConversationRecordKey() => clearField(2);
void clearMembership() => clearField(2);
@$pb.TagNumber(2)
$0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1);
Membership ensureMembership() => $_ensure(1);
@$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 {
@ -1224,7 +1546,8 @@ class Profile extends $pb.GeneratedMessage {
..aOS(3, _omitFieldNames ? '' : 'about')
..aOS(4, _omitFieldNames ? '' : 'status')
..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
;
@ -1295,15 +1618,24 @@ class Profile extends $pb.GeneratedMessage {
void clearAvailability() => clearField(5);
@$pb.TagNumber(6)
$0.TypedKey get avatar => $_getN(5);
DataReference get avatar => $_getN(5);
@$pb.TagNumber(6)
set avatar($0.TypedKey v) { setField(6, v); }
set avatar(DataReference v) { setField(6, v); }
@$pb.TagNumber(6)
$core.bool hasAvatar() => $_has(5);
@$pb.TagNumber(6)
void clearAvatar() => clearField(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 {
@ -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);
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)
..aOM<Profile>(2, _omitFieldNames ? '' : 'remoteProfile', subBuilder: Profile.create)
..aOS(1, _omitFieldNames ? '' : 'nickname')
..aOM<Profile>(2, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create)
..aOS(3, _omitFieldNames ? '' : 'superIdentityJson')
..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
..aOB(7, _omitFieldNames ? '' : 'showAvailability')
..aOS(8, _omitFieldNames ? '' : 'notes')
..hasRequiredFields = false
;
@ -1457,26 +1790,24 @@ class Contact extends $pb.GeneratedMessage {
static Contact? _defaultInstance;
@$pb.TagNumber(1)
Profile get editedProfile => $_getN(0);
$core.String get nickname => $_getSZ(0);
@$pb.TagNumber(1)
set editedProfile(Profile v) { setField(1, v); }
set nickname($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasEditedProfile() => $_has(0);
$core.bool hasNickname() => $_has(0);
@$pb.TagNumber(1)
void clearEditedProfile() => clearField(1);
@$pb.TagNumber(1)
Profile ensureEditedProfile() => $_ensure(0);
void clearNickname() => clearField(1);
@$pb.TagNumber(2)
Profile get remoteProfile => $_getN(1);
Profile get profile => $_getN(1);
@$pb.TagNumber(2)
set remoteProfile(Profile v) { setField(2, v); }
set profile(Profile v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasRemoteProfile() => $_has(1);
$core.bool hasProfile() => $_has(1);
@$pb.TagNumber(2)
void clearRemoteProfile() => clearField(2);
void clearProfile() => clearField(2);
@$pb.TagNumber(2)
Profile ensureRemoteProfile() => $_ensure(1);
Profile ensureProfile() => $_ensure(1);
@$pb.TagNumber(3)
$core.String get superIdentityJson => $_getSZ(2);
@ -1528,6 +1859,15 @@ class Contact extends $pb.GeneratedMessage {
$core.bool hasShowAvailability() => $_has(6);
@$pb.TagNumber(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 {

View file

@ -65,6 +65,51 @@ final $typed_data.Uint8List scopeDescriptor = $convert.base64Decode(
'CgVTY29wZRIMCghXQVRDSEVSUxAAEg0KCU1PREVSQVRFRBABEgsKB1RBTEtFUlMQAhIOCgpNT0'
'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')
const Attachment$json = {
'1': 'Attachment',
@ -89,14 +134,14 @@ const AttachmentMedia$json = {
'2': [
{'1': 'mime', '3': 1, '4': 1, '5': 9, '10': 'mime'},
{'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`.
final $typed_data.Uint8List attachmentMediaDescriptor = $convert.base64Decode(
'Cg9BdHRhY2htZW50TWVkaWESEgoEbWltZRgBIAEoCVIEbWltZRISCgRuYW1lGAIgASgJUgRuYW'
'1lEiwKB2NvbnRlbnQYAyABKAsyEi5kaHQuRGF0YVJlZmVyZW5jZVIHY29udGVudA==');
'1lEjMKB2NvbnRlbnQYAyABKAsyGS52ZWlsaWRjaGF0LkRhdGFSZWZlcmVuY2VSB2NvbnRlbnQ=');
@$core.Deprecated('Use permissionsDescriptor instead')
const Permissions$json = {
@ -140,7 +185,7 @@ const ChatSettings$json = {
'2': [
{'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'},
{'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'},
],
'8': [
@ -151,9 +196,9 @@ const ChatSettings$json = {
/// Descriptor for `ChatSettings`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatSettingsDescriptor = $convert.base64Decode(
'CgxDaGF0U2V0dGluZ3MSFAoFdGl0bGUYASABKAlSBXRpdGxlEiAKC2Rlc2NyaXB0aW9uGAIgAS'
'gJUgtkZXNjcmlwdGlvbhIrCgRpY29uGAMgASgLMhIuZGh0LkRhdGFSZWZlcmVuY2VIAFIEaWNv'
'bogBARItChJkZWZhdWx0X2V4cGlyYXRpb24YBCABKARSEWRlZmF1bHRFeHBpcmF0aW9uQgcKBV'
'9pY29u');
'gJUgtkZXNjcmlwdGlvbhIyCgRpY29uGAMgASgLMhkudmVpbGlkY2hhdC5EYXRhUmVmZXJlbmNl'
'SABSBGljb26IAQESLQoSZGVmYXVsdF9leHBpcmF0aW9uGAQgASgEUhFkZWZhdWx0RXhwaXJhdG'
'lvbkIHCgVfaWNvbg==');
@$core.Deprecated('Use messageDescriptor instead')
const Message$json = {
@ -320,41 +365,76 @@ final $typed_data.Uint8List conversationDescriptor = $convert.base64Decode(
'JvZmlsZRIuChNzdXBlcl9pZGVudGl0eV9qc29uGAIgASgJUhFzdXBlcklkZW50aXR5SnNvbhIs'
'CghtZXNzYWdlcxgDIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIIbWVzc2FnZXM=');
@$core.Deprecated('Use chatDescriptor instead')
const Chat$json = {
'1': 'Chat',
@$core.Deprecated('Use chatMemberDescriptor instead')
const ChatMember$json = {
'1': 'ChatMember',
'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_conversation_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
{'1': 'remote_identity_public_key', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteIdentityPublicKey'},
{'1': 'remote_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
],
};
/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatDescriptor = $convert.base64Decode(
'CgRDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3NSCHNldH'
'RpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVpbGlkLlR5'
'cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRJVCh5yZW1vdGVfY29udmVyc2F0aW'
'9uX3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdGlv'
'blJlY29yZEtleQ==');
/// Descriptor for `ChatMember`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatMemberDescriptor = $convert.base64Decode(
'CgpDaGF0TWVtYmVyEk0KGnJlbW90ZV9pZGVudGl0eV9wdWJsaWNfa2V5GAEgASgLMhAudmVpbG'
'lkLlR5cGVkS2V5UhdyZW1vdGVJZGVudGl0eVB1YmxpY0tleRJVCh5yZW1vdGVfY29udmVyc2F0'
'aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdG'
'lvblJlY29yZEtleQ==');
@$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')
const GroupChat$json = {
'1': 'GroupChat',
'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_conversation_record_keys', '3': 3, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKeys'},
{'1': 'membership', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Membership', '10': 'membership'},
{'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`.
final $typed_data.Uint8List groupChatDescriptor = $convert.base64Decode(
'CglHcm91cENoYXQSNAoIc2V0dGluZ3MYASABKAsyGC52ZWlsaWRjaGF0LkNoYXRTZXR0aW5nc1'
'IIc2V0dGluZ3MSUwodbG9jYWxfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWls'
'aWQuVHlwZWRLZXlSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5ElcKH3JlbW90ZV9jb252ZX'
'JzYXRpb25fcmVjb3JkX2tleXMYAyADKAsyEC52ZWlsaWQuVHlwZWRLZXlSHHJlbW90ZUNvbnZl'
'cnNhdGlvblJlY29yZEtleXM=');
'IIc2V0dGluZ3MSNgoKbWVtYmVyc2hpcBgCIAEoCzIWLnZlaWxpZGNoYXQuTWVtYmVyc2hpcFIK'
'bWVtYmVyc2hpcBI5CgtwZXJtaXNzaW9ucxgDIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbn'
'NSC3Blcm1pc3Npb25zElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAQgASgLMhAu'
'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')
const Profile$json = {
@ -365,7 +445,8 @@ const Profile$json = {
{'1': 'about', '3': 3, '4': 1, '5': 9, '10': 'about'},
{'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'},
{'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': [
{'1': '_avatar'},
@ -376,9 +457,9 @@ const Profile$json = {
final $typed_data.Uint8List profileDescriptor = $convert.base64Decode(
'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW'
'5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp'
'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ei'
'0KBmF2YXRhchgGIAEoCzIQLnZlaWxpZC5UeXBlZEtleUgAUgZhdmF0YXKIAQFCCQoHX2F2YXRh'
'cg==');
'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ej'
'YKBmF2YXRhchgGIAEoCzIZLnZlaWxpZGNoYXQuRGF0YVJlZmVyZW5jZUgAUgZhdmF0YXKIAQES'
'HAoJdGltZXN0YW1wGAcgASgEUgl0aW1lc3RhbXBCCQoHX2F2YXRhcg==');
@$core.Deprecated('Use accountDescriptor instead')
const Account$json = {
@ -409,27 +490,27 @@ final $typed_data.Uint8List accountDescriptor = $convert.base64Decode(
const Contact$json = {
'1': 'Contact',
'2': [
{'1': 'edited_profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'editedProfile'},
{'1': 'remote_profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'remoteProfile'},
{'1': 'nickname', '3': 1, '4': 1, '5': 9, '10': 'nickname'},
{'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': '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': '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': 'notes', '3': 8, '4': 1, '5': 9, '10': 'notes'},
],
};
/// Descriptor for `Contact`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List contactDescriptor = $convert.base64Decode(
'CgdDb250YWN0EjoKDmVkaXRlZF9wcm9maWxlGAEgASgLMhMudmVpbGlkY2hhdC5Qcm9maWxlUg'
'1lZGl0ZWRQcm9maWxlEjoKDnJlbW90ZV9wcm9maWxlGAIgASgLMhMudmVpbGlkY2hhdC5Qcm9m'
'aWxlUg1yZW1vdGVQcm9maWxlEi4KE3N1cGVyX2lkZW50aXR5X2pzb24YAyABKAlSEXN1cGVySW'
'RlbnRpdHlKc29uEkAKE2lkZW50aXR5X3B1YmxpY19rZXkYBCABKAsyEC52ZWlsaWQuVHlwZWRL'
'ZXlSEWlkZW50aXR5UHVibGljS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleR'
'gFIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5ElMK'
'HWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAYgASgLMhAudmVpbGlkLlR5cGVkS2V5Uh'
'psb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRIrChFzaG93X2F2YWlsYWJpbGl0eRgHIAEoCFIQ'
'c2hvd0F2YWlsYWJpbGl0eQ==');
'CgdDb250YWN0EhoKCG5pY2tuYW1lGAEgASgJUghuaWNrbmFtZRItCgdwcm9maWxlGAIgASgLMh'
'MudmVpbGlkY2hhdC5Qcm9maWxlUgdwcm9maWxlEi4KE3N1cGVyX2lkZW50aXR5X2pzb24YAyAB'
'KAlSEXN1cGVySWRlbnRpdHlKc29uEkAKE2lkZW50aXR5X3B1YmxpY19rZXkYBCABKAsyEC52ZW'
'lsaWQuVHlwZWRLZXlSEWlkZW50aXR5UHVibGljS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25f'
'cmVjb3JkX2tleRgFIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUm'
'Vjb3JkS2V5ElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAYgASgLMhAudmVpbGlk'
'LlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRIrChFzaG93X2F2YWlsYWJpbG'
'l0eRgHIAEoCFIQc2hvd0F2YWlsYWJpbGl0eRIUCgVub3RlcxgIIAEoCVIFbm90ZXM=');
@$core.Deprecated('Use contactInvitationDescriptor instead')
const ContactInvitation$json = {

View file

@ -47,6 +47,31 @@ enum Scope {
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
////////////////////////////////////////////////////////////////////////////////////
@ -67,10 +92,9 @@ message AttachmentMedia {
// Title or filename
string name = 2;
// Pointer to the data content
dht.DataReference content = 3;
DataReference content = 3;
}
////////////////////////////////////////////////////////////////////////////////////
// Chat room controls
////////////////////////////////////////////////////////////////////////////////////
@ -106,7 +130,7 @@ message ChatSettings {
// Description for the chat
string description = 2;
// Icon for the chat
optional dht.DataReference icon = 3;
optional DataReference icon = 3;
// Default message expiration duration (in us)
uint64 default_expiration = 4;
}
@ -243,15 +267,23 @@ message Conversation {
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
message Chat {
message DirectChat {
// Settings
ChatSettings settings = 1;
// Conversation key for this user
veilid.TypedKey local_conversation_record_key = 2;
// Conversation key for the other party
veilid.TypedKey remote_conversation_record_key = 3;
ChatMember remote_member = 3;
}
// A group chat
@ -259,10 +291,22 @@ message Chat {
message GroupChat {
// Settings
ChatSettings settings = 1;
// Membership
Membership membership = 2;
// Permissions
Permissions permissions = 3;
// 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
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;
// Availability
Availability availability = 5;
// Avatar DHTData
optional veilid.TypedKey avatar = 6;
// Avatar
optional DataReference avatar = 6;
// Timestamp of last change
uint64 timestamp = 7;
}
// A record of an individual account
@ -323,10 +369,10 @@ message Account {
//
// Stored in ContactList DHTList
message Contact {
// Friend's profile as locally edited
Profile edited_profile = 1;
// Friend's nickname
string nickname = 1;
// 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
string super_identity_json = 3;
// 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;
// Show availability to this contact
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:go_router/go_router.dart';
import 'package:stream_transform/stream_transform.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart';
import '../../layout/layout.dart';
import '../../proto/proto.dart' as proto;
import '../../settings/settings.dart';
import '../../tools/tools.dart';
import '../../veilid_processor/views/developer.dart';
@ -18,13 +20,12 @@ part 'router_cubit.freezed.dart';
part 'router_cubit.g.dart';
final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'rootNavKey');
final _homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'homeNavKey');
@freezed
class RouterState with _$RouterState {
const factory RouterState(
{required bool hasAnyAccount,
required bool hasActiveChat}) = _RouterState;
const factory RouterState({
required bool hasAnyAccount,
}) = _RouterState;
factory RouterState.fromJson(dynamic json) =>
_$RouterStateFromJson(json as Map<String, dynamic>);
@ -34,7 +35,6 @@ class RouterCubit extends Cubit<RouterState> {
RouterCubit(AccountRepository accountRepository)
: super(RouterState(
hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty,
hasActiveChat: false,
)) {
// Subscribe to repository streams
_accountRepositorySubscription = accountRepository.stream.listen((event) {
@ -50,10 +50,6 @@ class RouterCubit extends Cubit<RouterState> {
});
}
void setHasActiveChat(bool active) {
emit(state.copyWith(hasActiveChat: active));
}
@override
Future<void> close() async {
await _accountRepositorySubscription.cancel();
@ -62,27 +58,29 @@ class RouterCubit extends Cubit<RouterState> {
/// Our application routes
List<RouteBase> get routes => [
ShellRoute(
navigatorKey: _homeNavKey,
builder: (context, state, child) => HomeShell(
accountReadyBuilder: Builder(
builder: (context) =>
HomeAccountReadyShell(context: context, child: child))),
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeAccountReadyMain(),
),
GoRoute(
path: '/chat',
builder: (context, state) => const HomeAccountReadyChat(),
),
],
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/edit_account',
builder: (context, state) {
final extra = state.extra! as List<Object?>;
return EditAccountPage(
superIdentityRecordKey: extra[0]! as TypedKey,
existingProfile: extra[1]! as proto.Profile,
);
},
),
GoRoute(
path: '/new_account',
builder: (context, state) => const NewAccountPage(),
),
GoRoute(
path: '/new_account/recovery_key',
builder: (context, state) =>
ShowRecoveryKeyPage(secretKey: state.extra! as SecretKey),
),
GoRoute(
path: '/settings',
builder: (context, state) => const SettingsPage(),
@ -98,37 +96,14 @@ class RouterCubit extends Cubit<RouterState> {
// No matter where we are, if there's not
switch (goRouterState.matchedLocation) {
case '/new_account':
return state.hasAnyAccount ? '/' : null;
case '/':
if (!state.hasAnyAccount) {
return '/new_account';
}
if (responsiveVisibility(
context: context,
tablet: false,
tabletLandscape: false,
desktop: false)) {
if (state.hasActiveChat) {
return '/chat';
}
}
return null;
case '/chat':
if (!state.hasAnyAccount) {
return '/new_account';
}
if (responsiveVisibility(
context: context,
tablet: false,
tabletLandscape: false,
desktop: false)) {
if (!state.hasActiveChat) {
return '/';
}
} else {
return '/';
}
case '/new_account':
return null;
case '/new_account/recovery_key':
return null;
case '/settings':
return null;

View file

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

View file

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

View file

@ -89,11 +89,9 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
onError: errorScale.primaryText,
// errorContainer: errorScale.hoverElementBackground,
// onErrorContainer: errorScale.subtleText,
background: grayScale.appBackground, // reviewed
onBackground: grayScale.appText, // reviewed
surface: primaryScale.primary, // reviewed
onSurface: primaryScale.primaryText, // reviewed
surfaceVariant: secondaryScale.primary,
surfaceContainerHighest: secondaryScale.primary,
onSurfaceVariant: secondaryScale.primaryText, // ?? reviewed a little
outline: primaryScale.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_bloc/flutter_bloc.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:motion_toast/motion_toast.dart';
import 'package:quickalert/quickalert.dart';
@ -122,36 +121,45 @@ Future<void> showErrorModal(
}
void showErrorToast(BuildContext context, String message) {
MotionToast.error(
title: Text(translate('toast.error')),
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
MotionToast(
//title: Text(translate('toast.error')),
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);
}
void showInfoToast(BuildContext context, String message) {
MotionToast.info(
title: Text(translate('toast.info')),
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
MotionToast(
//title: Text(translate('toast.info')),
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);
}
// 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({
required BuildContext context,
required String title,
@ -230,3 +238,26 @@ Widget styledBottomSheet({
bool get isPlatformDark =>
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
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 'enter_password.dart';
export 'enter_pin.dart';
export 'loggy.dart';
export 'misc.dart';
export 'package_info.dart';
export 'phono_byte.dart';
export 'pop_control.dart';
export 'responsive.dart';

View file

@ -1,5 +1,6 @@
import 'package:async_tools/async_tools.dart';
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:go_router/go_router.dart';
@ -11,7 +12,7 @@ import '../../theme/theme.dart';
import '../cubit/connection_state_cubit.dart';
class SignalStrengthMeterWidget extends StatelessWidget {
const SignalStrengthMeterWidget({super.key});
const SignalStrengthMeterWidget({super.key, this.color, this.inactiveColor});
@override
// ignore: prefer_expression_function_bodies
@ -33,32 +34,35 @@ class SignalStrengthMeterWidget extends StatelessWidget {
switch (connectionState.attachment.state) {
case AttachmentState.detached:
iconWidget = Icon(Icons.signal_cellular_nodata,
size: iconSize, color: scale.primaryScale.primaryText);
size: iconSize,
color: this.color ?? scale.primaryScale.primaryText);
return;
case AttachmentState.detaching:
iconWidget = Icon(Icons.signal_cellular_off,
size: iconSize, color: scale.primaryScale.primaryText);
size: iconSize,
color: this.color ?? scale.primaryScale.primaryText);
return;
case AttachmentState.attaching:
value = 0;
color = scale.primaryScale.primaryText;
color = this.color ?? scale.primaryScale.primaryText;
case AttachmentState.attachedWeak:
value = 1;
color = scale.primaryScale.primaryText;
color = this.color ?? scale.primaryScale.primaryText;
case AttachmentState.attachedStrong:
value = 2;
color = scale.primaryScale.primaryText;
color = this.color ?? scale.primaryScale.primaryText;
case AttachmentState.attachedGood:
value = 3;
color = scale.primaryScale.primaryText;
color = this.color ?? scale.primaryScale.primaryText;
case AttachmentState.fullyAttached:
value = 4;
color = scale.primaryScale.primaryText;
color = this.color ?? scale.primaryScale.primaryText;
case AttachmentState.overAttached:
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(
value: value,
@ -86,4 +90,16 @@ class SignalStrengthMeterWidget extends StatelessWidget {
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 mobile_scanner
import package_info_plus
import pasteboard
import path_provider_foundation
import screen_retriever
@ -19,6 +20,7 @@ import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))

View file

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

View file

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

View file

@ -23,7 +23,6 @@ message DHTData {
uint32 size = 4;
}
// DHTLog - represents a ring buffer of many elements with append/truncate semantics
// Header in subkey 0 of first key follows this structure
message DHTLog {
@ -62,27 +61,6 @@ message DHTShortArray {
// 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
message OwnedDHTRecordPointer {
// DHT Record key

View file

@ -71,6 +71,12 @@ class _DHTLogSpine {
// Write new spine head record to the network
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();
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
// notifying listeners and knows when it makes local changes
_subscription ??=
await _spineRecord.listen(localChanges: false, _onSpineChanged);
await _spineRecord.listen(localChanges: true, _onSpineChanged);
} on Exception {
// If anything fails, try to cancel the watches
await cancelWatch();

View file

@ -12,14 +12,6 @@ class DefaultDHTRecordCubit<T> extends DHTRecordCubit<T> {
stateFunction: _makeStateFunction(decodeState),
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>(
T Function(List<int> data) decodeState) =>
(record) async {

View file

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

View file

@ -60,51 +60,6 @@ final $typed_data.Uint8List dHTShortArrayDescriptor = $convert.base64Decode(
'Cg1ESFRTaG9ydEFycmF5EiQKBGtleXMYASADKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGtleXMSFA'
'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')
const OwnedDHTRecordPointer$json = {
'1': 'OwnedDHTRecordPointer',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -60,11 +60,10 @@ packages:
async_tools:
dependency: "direct main"
description:
name: async_tools
sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
path: "../dart_async_tools"
relative: true
source: path
version: "0.1.3"
awesome_extensions:
dependency: "direct main"
description:
@ -100,11 +99,10 @@ packages:
bloc_advanced_tools:
dependency: "direct main"
description:
name: bloc_advanced_tools
sha256: "0cf9b3a73a67addfe22ec3f97a1ac240f6ad53870d6b21a980260f390d7901cd"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
path: "../bloc_advanced_tools"
relative: true
source: path
version: "0.1.3"
blurry_modal_progress_hud:
dependency: "direct main"
description:
@ -585,6 +583,14 @@ packages:
description: flutter
source: sdk
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:
dependency: "direct main"
description:
@ -857,6 +863,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:

View file

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