mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-08-11 07:30:17 -04:00
Merge branch 'recovery-key-ui' into 'main'
Multiple accounts support See merge request veilid/veilidchat!30
This commit is contained in:
commit
00fe682e0c
99 changed files with 5003 additions and 2538 deletions
|
@ -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": {
|
||||
|
|
40
lib/account_manager/cubits/account_info_cubit.dart
Normal file
40
lib/account_manager/cubits/account_info_cubit.dart
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
299
lib/account_manager/cubits/per_account_collection_cubit.dart
Normal file
299
lib/account_manager/cubits/per_account_collection_cubit.dart
Normal 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,
|
||||
));
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1 +1 @@
|
|||
export 'account_repository/account_repository.dart';
|
||||
export 'account_repository.dart';
|
||||
|
|
158
lib/account_manager/views/edit_account_page.dart
Normal file
158
lib/account_manager/views/edit_account_page.dart
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
114
lib/account_manager/views/profile_edit_form.dart
Normal file
114
lib/account_manager/views/profile_edit_form.dart
Normal 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,
|
||||
);
|
||||
}
|
63
lib/account_manager/views/show_recovery_key_page.dart
Normal file
63
lib/account_manager/views/show_recovery_key_page.dart
Normal 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));
|
||||
}
|
||||
}
|
|
@ -1,2 +1,4 @@
|
|||
export 'edit_account_page.dart';
|
||||
export 'new_account_page.dart';
|
||||
export 'profile_widget.dart';
|
||||
export 'show_recovery_key_page.dart';
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>>?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
export 'active_single_contact_chat_bloc_map_cubit.dart';
|
||||
export 'active_conversations_bloc_map_cubit.dart';
|
||||
export 'chat_list_cubit.dart';
|
||||
|
|
96
lib/chat_list/views/chat_list_widget.dart
Normal file
96
lib/chat_list/views/chat_list_widget.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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))));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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?>();
|
||||
}
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
export 'contact_list_cubit.dart';
|
||||
export 'conversation_cubit.dart';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
1
lib/conversation/conversation.dart
Normal file
1
lib/conversation/conversation.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'cubits/cubits.dart';
|
180
lib/conversation/cubits/active_conversations_bloc_map_cubit.dart
Normal file
180
lib/conversation/cubits/active_conversations_bloc_map_cubit.dart
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
3
lib/conversation/cubits/cubits.dart
Normal file
3
lib/conversation/cubits/cubits.dart
Normal 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';
|
37
lib/layout/home/active_account_page_controller_wrapper.dart
Normal file
37
lib/layout/home/active_account_page_controller_wrapper.dart
Normal 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;
|
||||
}
|
316
lib/layout/home/drawer_menu/drawer_menu.dart
Normal file
316
lib/layout/home/drawer_menu/drawer_menu.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
130
lib/layout/home/drawer_menu/menu_item_widget.dart
Normal file
130
lib/layout/home/drawer_menu/menu_item_widget.dart
Normal 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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
// });
|
|
@ -1,3 +1,2 @@
|
|||
export 'home_account_ready_chat.dart';
|
||||
export 'home_account_ready_main.dart';
|
||||
export 'home_account_ready_shell.dart';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
|
160
lib/layout/home/home_screen.dart
Normal file
160
lib/layout/home/home_screen.dart
Normal 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();
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
|
14
lib/tools/package_info.dart
Normal file
14
lib/tools/package_info.dart
Normal 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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
42
pubspec.lock
42
pubspec.lock
|
@ -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:
|
||||
|
|
16
pubspec.yaml
16
pubspec.yaml
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue