mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-12-20 18:45:19 -05: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": {
|
"app": {
|
||||||
"title": "VeilidChat"
|
"title": "VeilidChat"
|
||||||
},
|
},
|
||||||
"app_bar": {
|
"menu": {
|
||||||
"settings_tooltip": "Settings"
|
"settings_tooltip": "Settings",
|
||||||
|
"add_account_tooltip": "Add Account"
|
||||||
},
|
},
|
||||||
"pager": {
|
"pager": {
|
||||||
"chats": "Chats",
|
"chats": "Chats",
|
||||||
|
|
@ -18,7 +19,7 @@
|
||||||
"lock_type_password": "password"
|
"lock_type_password": "password"
|
||||||
},
|
},
|
||||||
"new_account_page": {
|
"new_account_page": {
|
||||||
"titlebar": "Create a new account",
|
"titlebar": "Create A New Account",
|
||||||
"header": "Account Profile",
|
"header": "Account Profile",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.",
|
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.",
|
||||||
|
|
@ -26,12 +27,37 @@
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"pronouns": "Pronouns"
|
"pronouns": "Pronouns"
|
||||||
},
|
},
|
||||||
|
"edit_account_page": {
|
||||||
|
"titlebar": "Edit Account",
|
||||||
|
"header": "Account Profile",
|
||||||
|
"update": "Update",
|
||||||
|
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.",
|
||||||
|
"error": "Account modification error",
|
||||||
|
"name": "Name",
|
||||||
|
"pronouns": "Pronouns",
|
||||||
|
"remove_account": "Remove Account",
|
||||||
|
"delete_identity": "Delete Identity",
|
||||||
|
"remove_account_confirm": "Confirm Account Removal?",
|
||||||
|
"remove_account_description": "Remove account from this device only",
|
||||||
|
"delete_identity_description": "Delete identity and all messages completely",
|
||||||
|
"delete_identity_confirm_message": "This action is PERMANENT, and your identity will no longer be recoverable with the recovery key. This will not remove your messages you have sent from other people's devices.",
|
||||||
|
"confirm_are_you_sure": "Are you sure you want to do this?"
|
||||||
|
},
|
||||||
|
"show_recovery_key_page": {
|
||||||
|
"titlebar": "Save Recovery Key",
|
||||||
|
"instructions": "You must save this recovery key somewhere safe. This key is the ONLY way to recover your VeilidChat account in the event of a forgotton password or a lost, stolen, or compromised device.",
|
||||||
|
"instructions_options": "Here are some options for your recovery key:",
|
||||||
|
"instructions_print": "Print the recovery key and keep it somewhere safe",
|
||||||
|
"instructions_write": "View the recovery key and write it down on paper",
|
||||||
|
"instructions_send": "Send the recovery key to another app to save it"
|
||||||
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"ok": "Ok",
|
"ok": "Ok",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"accept": "Accept",
|
"accept": "Accept",
|
||||||
"reject": "Reject",
|
"reject": "Reject",
|
||||||
|
"finish": "Finish",
|
||||||
"waiting_for_network": "Waiting For Network"
|
"waiting_for_network": "Waiting For Network"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
|
|
|
||||||
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 'dart:async';
|
||||||
|
|
||||||
|
import 'package:protobuf/protobuf.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
|
import '../account_manager.dart';
|
||||||
|
|
||||||
class AccountRecordCubit extends DefaultDHTRecordCubit<proto.Account> {
|
typedef AccountRecordState = proto.Account;
|
||||||
AccountRecordCubit({
|
|
||||||
required super.open,
|
/// The saved state of a VeilidChat Account on the DHT
|
||||||
}) : super(decodeState: proto.Account.fromBuffer);
|
/// Used to synchronize status, profile, and options for a specific account
|
||||||
|
/// across multiple clients. This DHT record is the 'source of truth' for an
|
||||||
|
/// account and is privately encrypted with an owned record from the 'userLogin'
|
||||||
|
/// tabledb-local storage, encrypted by the unlock code for the account.
|
||||||
|
class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
|
||||||
|
AccountRecordCubit(
|
||||||
|
{required LocalAccount localAccount, required UserLogin userLogin})
|
||||||
|
: super(
|
||||||
|
decodeState: proto.Account.fromBuffer,
|
||||||
|
open: () => _open(localAccount, userLogin));
|
||||||
|
|
||||||
|
static Future<DHTRecord> _open(
|
||||||
|
LocalAccount localAccount, UserLogin userLogin) async {
|
||||||
|
// Record not yet open, do it
|
||||||
|
final pool = DHTRecordPool.instance;
|
||||||
|
final record = await pool.openRecordOwned(
|
||||||
|
userLogin.accountRecordInfo.accountRecord,
|
||||||
|
debugName: 'AccountRecordCubit::_open::AccountRecord',
|
||||||
|
parent: localAccount.superIdentity.currentInstance.recordKey);
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Public Interface
|
||||||
|
|
||||||
|
Future<void> updateProfile(proto.Profile profile) async {
|
||||||
|
await record.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async {
|
||||||
|
if (old == null || old.profile == profile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return old.deepCopy()..profile = profile;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import 'dart:async';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../repository/account_repository/account_repository.dart';
|
import '../repository/account_repository.dart';
|
||||||
|
|
||||||
class ActiveLocalAccountCubit extends Cubit<TypedKey?> {
|
class ActiveLocalAccountCubit extends Cubit<TypedKey?> {
|
||||||
ActiveLocalAccountCubit(AccountRepository accountRepository)
|
ActiveLocalAccountCubit(AccountRepository accountRepository)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
export 'account_info_cubit.dart';
|
||||||
export 'account_record_cubit.dart';
|
export 'account_record_cubit.dart';
|
||||||
export 'active_local_account_cubit.dart';
|
export 'active_local_account_cubit.dart';
|
||||||
export 'local_accounts_cubit.dart';
|
export 'local_accounts_cubit.dart';
|
||||||
|
export 'per_account_collection_bloc_map_cubit.dart';
|
||||||
|
export 'per_account_collection_cubit.dart';
|
||||||
export 'user_logins_cubit.dart';
|
export 'user_logins_cubit.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../models/models.dart';
|
import '../models/models.dart';
|
||||||
import '../repository/account_repository/account_repository.dart';
|
import '../repository/account_repository.dart';
|
||||||
|
|
||||||
class LocalAccountsCubit extends Cubit<IList<LocalAccount>> {
|
typedef LocalAccountsState = IList<LocalAccount>;
|
||||||
|
|
||||||
|
class LocalAccountsCubit extends Cubit<LocalAccountsState>
|
||||||
|
with StateMapFollowable<LocalAccountsState, TypedKey, LocalAccount> {
|
||||||
LocalAccountsCubit(AccountRepository accountRepository)
|
LocalAccountsCubit(AccountRepository accountRepository)
|
||||||
: _accountRepository = accountRepository,
|
: _accountRepository = accountRepository,
|
||||||
super(accountRepository.getLocalAccounts()) {
|
super(accountRepository.getLocalAccounts()) {
|
||||||
|
|
@ -30,6 +35,14 @@ class LocalAccountsCubit extends Cubit<IList<LocalAccount>> {
|
||||||
await _accountRepositorySubscription.cancel();
|
await _accountRepositorySubscription.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// StateMapFollowable /////////////////////////
|
||||||
|
@override
|
||||||
|
IMap<TypedKey, LocalAccount> getStateMap(LocalAccountsState state) {
|
||||||
|
final stateValue = state;
|
||||||
|
return IMap.fromIterable(stateValue,
|
||||||
|
keyMapper: (e) => e.superIdentity.recordKey, valueMapper: (e) => e);
|
||||||
|
}
|
||||||
|
|
||||||
final AccountRepository _accountRepository;
|
final AccountRepository _accountRepository;
|
||||||
late final StreamSubscription<AccountRepositoryChange>
|
late final StreamSubscription<AccountRepositoryChange>
|
||||||
_accountRepositorySubscription;
|
_accountRepositorySubscription;
|
||||||
|
|
|
||||||
|
|
@ -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 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
|
|
||||||
import '../models/models.dart';
|
import '../models/models.dart';
|
||||||
import '../repository/account_repository/account_repository.dart';
|
import '../repository/account_repository.dart';
|
||||||
|
|
||||||
class UserLoginsCubit extends Cubit<IList<UserLogin>> {
|
typedef UserLoginsState = IList<UserLogin>;
|
||||||
|
|
||||||
|
class UserLoginsCubit extends Cubit<UserLoginsState> {
|
||||||
UserLoginsCubit(AccountRepository accountRepository)
|
UserLoginsCubit(AccountRepository accountRepository)
|
||||||
: _accountRepository = accountRepository,
|
: _accountRepository = accountRepository,
|
||||||
super(accountRepository.getUserLogins()) {
|
super(accountRepository.getUserLogins()) {
|
||||||
|
|
@ -29,6 +31,7 @@ class UserLoginsCubit extends Cubit<IList<UserLogin>> {
|
||||||
await super.close();
|
await super.close();
|
||||||
await _accountRepositorySubscription.cancel();
|
await _accountRepositorySubscription.cancel();
|
||||||
}
|
}
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
final AccountRepository _accountRepository;
|
final AccountRepository _accountRepository;
|
||||||
late final StreamSubscription<AccountRepositoryChange>
|
late final StreamSubscription<AccountRepositoryChange>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,62 @@
|
||||||
import 'package:meta/meta.dart';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'active_account_info.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
|
import '../account_manager.dart';
|
||||||
|
|
||||||
enum AccountInfoStatus {
|
enum AccountInfoStatus {
|
||||||
noAccount,
|
|
||||||
accountInvalid,
|
accountInvalid,
|
||||||
accountLocked,
|
accountLocked,
|
||||||
accountReady,
|
accountUnlocked,
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class AccountInfo {
|
class AccountInfo extends Equatable {
|
||||||
const AccountInfo({
|
const AccountInfo({
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.active,
|
required this.localAccount,
|
||||||
required this.activeAccountInfo,
|
required this.userLogin,
|
||||||
});
|
});
|
||||||
|
|
||||||
final AccountInfoStatus status;
|
final AccountInfoStatus status;
|
||||||
final bool active;
|
final LocalAccount localAccount;
|
||||||
final ActiveAccountInfo? activeAccountInfo;
|
final UserLogin? userLogin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
status,
|
||||||
|
localAccount,
|
||||||
|
userLogin,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AccountInfoExt on AccountInfo {
|
||||||
|
TypedKey get superIdentityRecordKey => localAccount.superIdentity.recordKey;
|
||||||
|
TypedKey get accountRecordKey =>
|
||||||
|
userLogin!.accountRecordInfo.accountRecord.recordKey;
|
||||||
|
TypedKey get identityTypedPublicKey =>
|
||||||
|
localAccount.superIdentity.currentInstance.typedPublicKey;
|
||||||
|
PublicKey get identityPublicKey =>
|
||||||
|
localAccount.superIdentity.currentInstance.publicKey;
|
||||||
|
SecretKey get identitySecretKey => userLogin!.identitySecret.value;
|
||||||
|
KeyPair get identityWriter =>
|
||||||
|
KeyPair(key: identityPublicKey, secret: identitySecretKey);
|
||||||
|
Future<VeilidCryptoSystem> get identityCryptoSystem =>
|
||||||
|
localAccount.superIdentity.currentInstance.cryptoSystem;
|
||||||
|
|
||||||
|
Future<VeilidCrypto> makeConversationCrypto(
|
||||||
|
TypedKey remoteIdentityPublicKey) async {
|
||||||
|
final identitySecret = userLogin!.identitySecret;
|
||||||
|
final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind);
|
||||||
|
final sharedSecret = await cs.generateSharedSecret(
|
||||||
|
remoteIdentityPublicKey.value,
|
||||||
|
identitySecret.value,
|
||||||
|
utf8.encode('VeilidChat Conversation'));
|
||||||
|
|
||||||
|
final messagesCrypto = await VeilidCryptoPrivate.fromSharedSecret(
|
||||||
|
identitySecret.kind, sharedSecret);
|
||||||
|
return messagesCrypto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 'account_info.dart';
|
||||||
export 'active_account_info.dart';
|
|
||||||
export 'encryption_key_type.dart';
|
export 'encryption_key_type.dart';
|
||||||
export 'local_account/local_account.dart';
|
export 'local_account/local_account.dart';
|
||||||
export 'new_profile_spec.dart';
|
export 'new_profile_spec.dart';
|
||||||
|
export 'per_account_collection_state/per_account_collection_state.dart';
|
||||||
export 'user_login/user_login.dart';
|
export 'user_login/user_login.dart';
|
||||||
|
|
|
||||||
|
|
@ -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:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../../../proto/proto.dart' as proto;
|
import '../../../proto/proto.dart' as proto;
|
||||||
import '../../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
import '../../models/models.dart';
|
import '../models/models.dart';
|
||||||
|
|
||||||
const String veilidChatAccountKey = 'com.veilid.veilidchat';
|
const String veilidChatAccountKey = 'com.veilid.veilidchat';
|
||||||
|
|
||||||
|
|
@ -45,19 +45,6 @@ class AccountRepository {
|
||||||
valueToJson: (val) => val?.toJson(),
|
valueToJson: (val) => val?.toJson(),
|
||||||
makeInitialValue: () => null);
|
makeInitialValue: () => null);
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////
|
|
||||||
/// Fields
|
|
||||||
|
|
||||||
final TableDBValue<IList<LocalAccount>> _localAccounts;
|
|
||||||
final TableDBValue<IList<UserLogin>> _userLogins;
|
|
||||||
final TableDBValue<TypedKey?> _activeLocalAccount;
|
|
||||||
final StreamController<AccountRepositoryChange> _streamController;
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////
|
|
||||||
/// Singleton initialization
|
|
||||||
|
|
||||||
static AccountRepository instance = AccountRepository._();
|
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
await _localAccounts.get();
|
await _localAccounts.get();
|
||||||
await _userLogins.get();
|
await _userLogins.get();
|
||||||
|
|
@ -71,12 +58,10 @@ class AccountRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
/// Streams
|
/// Public Interface
|
||||||
|
///
|
||||||
Stream<AccountRepositoryChange> get stream => _streamController.stream;
|
Stream<AccountRepositoryChange> get stream => _streamController.stream;
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////
|
|
||||||
/// Selectors
|
|
||||||
IList<LocalAccount> getLocalAccounts() => _localAccounts.value;
|
IList<LocalAccount> getLocalAccounts() => _localAccounts.value;
|
||||||
TypedKey? getActiveLocalAccount() => _activeLocalAccount.value;
|
TypedKey? getActiveLocalAccount() => _activeLocalAccount.value;
|
||||||
IList<UserLogin> getUserLogins() => _userLogins.value;
|
IList<UserLogin> getUserLogins() => _userLogins.value;
|
||||||
|
|
@ -107,29 +92,11 @@ class AccountRepository {
|
||||||
return userLogins[idx];
|
return userLogins[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountInfo getAccountInfo(TypedKey? superIdentityRecordKey) {
|
AccountInfo? getAccountInfo(TypedKey superIdentityRecordKey) {
|
||||||
// Get active account if we have one
|
|
||||||
final activeLocalAccount = getActiveLocalAccount();
|
|
||||||
if (superIdentityRecordKey == null) {
|
|
||||||
if (activeLocalAccount == null) {
|
|
||||||
// No user logged in
|
|
||||||
return const AccountInfo(
|
|
||||||
status: AccountInfoStatus.noAccount,
|
|
||||||
active: false,
|
|
||||||
activeAccountInfo: null);
|
|
||||||
}
|
|
||||||
superIdentityRecordKey = activeLocalAccount;
|
|
||||||
}
|
|
||||||
final active = superIdentityRecordKey == activeLocalAccount;
|
|
||||||
|
|
||||||
// Get which local account we want to fetch the profile for
|
// Get which local account we want to fetch the profile for
|
||||||
final localAccount = fetchLocalAccount(superIdentityRecordKey);
|
final localAccount = fetchLocalAccount(superIdentityRecordKey);
|
||||||
if (localAccount == null) {
|
if (localAccount == null) {
|
||||||
// account does not exist
|
return null;
|
||||||
return AccountInfo(
|
|
||||||
status: AccountInfoStatus.noAccount,
|
|
||||||
active: active,
|
|
||||||
activeAccountInfo: null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// See if we've logged into this account or if it is locked
|
// See if we've logged into this account or if it is locked
|
||||||
|
|
@ -137,23 +104,20 @@ class AccountRepository {
|
||||||
if (userLogin == null) {
|
if (userLogin == null) {
|
||||||
// Account was locked
|
// Account was locked
|
||||||
return AccountInfo(
|
return AccountInfo(
|
||||||
status: AccountInfoStatus.accountLocked,
|
status: AccountInfoStatus.accountLocked,
|
||||||
active: active,
|
localAccount: localAccount,
|
||||||
activeAccountInfo: null);
|
userLogin: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Got account, decrypted and decoded
|
// Got account, decrypted and decoded
|
||||||
return AccountInfo(
|
return AccountInfo(
|
||||||
status: AccountInfoStatus.accountReady,
|
status: AccountInfoStatus.accountUnlocked,
|
||||||
active: active,
|
localAccount: localAccount,
|
||||||
activeAccountInfo:
|
userLogin: userLogin,
|
||||||
ActiveAccountInfo(localAccount: localAccount, userLogin: userLogin),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////
|
|
||||||
/// Mutators
|
|
||||||
|
|
||||||
/// Reorder accounts
|
/// Reorder accounts
|
||||||
Future<void> reorderAccount(int oldIndex, int newIndex) async {
|
Future<void> reorderAccount(int oldIndex, int newIndex) async {
|
||||||
final localAccounts = await _localAccounts.get();
|
final localAccounts = await _localAccounts.get();
|
||||||
|
|
@ -168,104 +132,39 @@ class AccountRepository {
|
||||||
/// Creates a new super identity, an identity instance, an account associated
|
/// Creates a new super identity, an identity instance, an account associated
|
||||||
/// with the identity instance, stores the account in the identity key and
|
/// with the identity instance, stores the account in the identity key and
|
||||||
/// then logs into that account with no password set at this time
|
/// then logs into that account with no password set at this time
|
||||||
Future<void> createWithNewSuperIdentity(NewProfileSpec newProfileSpec) async {
|
Future<SecretKey> createWithNewSuperIdentity(proto.Profile newProfile) async {
|
||||||
log.debug('Creating super identity');
|
log.debug('Creating super identity');
|
||||||
final wsi = await WritableSuperIdentity.create();
|
final wsi = await WritableSuperIdentity.create();
|
||||||
try {
|
try {
|
||||||
final localAccount = await _newLocalAccount(
|
final localAccount = await _newLocalAccount(
|
||||||
superIdentity: wsi.superIdentity,
|
superIdentity: wsi.superIdentity,
|
||||||
identitySecret: wsi.identitySecret,
|
identitySecret: wsi.identitySecret,
|
||||||
newProfileSpec: newProfileSpec);
|
newProfile: newProfile);
|
||||||
|
|
||||||
// Log in the new account by default with no pin
|
// Log in the new account by default with no pin
|
||||||
final ok = await login(
|
final ok = await login(
|
||||||
localAccount.superIdentity.recordKey, EncryptionKeyType.none, '');
|
localAccount.superIdentity.recordKey, EncryptionKeyType.none, '');
|
||||||
assert(ok, 'login with none should never fail');
|
assert(ok, 'login with none should never fail');
|
||||||
|
|
||||||
|
return wsi.superSecret;
|
||||||
} on Exception catch (_) {
|
} on Exception catch (_) {
|
||||||
await wsi.delete();
|
await wsi.delete();
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new Account associated with the current instance of the identity
|
Future<void> editAccountProfile(
|
||||||
/// Adds a logged-out LocalAccount to track its existence on this device
|
TypedKey superIdentityRecordKey, proto.Profile newProfile) async {
|
||||||
Future<LocalAccount> _newLocalAccount(
|
log.debug('Editing profile for $superIdentityRecordKey');
|
||||||
{required SuperIdentity superIdentity,
|
|
||||||
required SecretKey identitySecret,
|
|
||||||
required NewProfileSpec newProfileSpec,
|
|
||||||
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
|
|
||||||
String encryptionKey = ''}) async {
|
|
||||||
log.debug('Creating new local account');
|
|
||||||
|
|
||||||
final localAccounts = await _localAccounts.get();
|
final localAccounts = await _localAccounts.get();
|
||||||
|
|
||||||
// Add account with profile to DHT
|
final newLocalAccounts = localAccounts.replaceFirstWhere(
|
||||||
await superIdentity.currentInstance.addAccount(
|
(x) => x.superIdentity.recordKey == superIdentityRecordKey,
|
||||||
superRecordKey: superIdentity.recordKey,
|
(localAccount) => localAccount!.copyWith(name: newProfile.name));
|
||||||
secretKey: identitySecret,
|
|
||||||
accountKey: veilidChatAccountKey,
|
|
||||||
createAccountCallback: (parent) async {
|
|
||||||
// Make empty contact list
|
|
||||||
log.debug('Creating contacts list');
|
|
||||||
final contactList = await (await DHTShortArray.create(
|
|
||||||
debugName: 'AccountRepository::_newLocalAccount::Contacts',
|
|
||||||
parent: parent))
|
|
||||||
.scope((r) async => r.recordPointer);
|
|
||||||
|
|
||||||
// Make empty contact invitation record list
|
|
||||||
log.debug('Creating contact invitation records list');
|
|
||||||
final contactInvitationRecords = await (await DHTShortArray.create(
|
|
||||||
debugName:
|
|
||||||
'AccountRepository::_newLocalAccount::ContactInvitations',
|
|
||||||
parent: parent))
|
|
||||||
.scope((r) async => r.recordPointer);
|
|
||||||
|
|
||||||
// Make empty chat record list
|
|
||||||
log.debug('Creating chat records list');
|
|
||||||
final chatRecords = await (await DHTShortArray.create(
|
|
||||||
debugName: 'AccountRepository::_newLocalAccount::Chats',
|
|
||||||
parent: parent))
|
|
||||||
.scope((r) async => r.recordPointer);
|
|
||||||
|
|
||||||
// Make account object
|
|
||||||
final account = proto.Account()
|
|
||||||
..profile = (proto.Profile()
|
|
||||||
..name = newProfileSpec.name
|
|
||||||
..pronouns = newProfileSpec.pronouns)
|
|
||||||
..contactList = contactList.toProto()
|
|
||||||
..contactInvitationRecords = contactInvitationRecords.toProto()
|
|
||||||
..chatList = chatRecords.toProto();
|
|
||||||
return account.writeToBuffer();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Encrypt identitySecret with key
|
|
||||||
final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes(
|
|
||||||
secret: identitySecret,
|
|
||||||
cryptoKind: superIdentity.currentInstance.recordKey.kind,
|
|
||||||
encryptionKey: encryptionKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create local account object
|
|
||||||
// Does not contain the account key or its secret
|
|
||||||
// as that is not to be persisted, and only pulled from the identity key
|
|
||||||
// and optionally decrypted with the unlock password
|
|
||||||
final localAccount = LocalAccount(
|
|
||||||
superIdentity: superIdentity,
|
|
||||||
identitySecretBytes: identitySecretBytes,
|
|
||||||
encryptionKeyType: encryptionKeyType,
|
|
||||||
biometricsEnabled: false,
|
|
||||||
hiddenAccount: false,
|
|
||||||
name: newProfileSpec.name,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add local account object to internal store
|
|
||||||
final newLocalAccounts = localAccounts.add(localAccount);
|
|
||||||
|
|
||||||
await _localAccounts.set(newLocalAccounts);
|
await _localAccounts.set(newLocalAccounts);
|
||||||
_streamController.add(AccountRepositoryChange.localAccounts);
|
_streamController.add(AccountRepositoryChange.localAccounts);
|
||||||
|
|
||||||
// Return local account object
|
|
||||||
return localAccount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove an account and wipe the messages for this account from this device
|
/// Remove an account and wipe the messages for this account from this device
|
||||||
|
|
@ -307,6 +206,88 @@ class AccountRepository {
|
||||||
_streamController.add(AccountRepositoryChange.activeLocalAccount);
|
_streamController.add(AccountRepositoryChange.activeLocalAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
/// Internal Implementation
|
||||||
|
|
||||||
|
/// Creates a new Account associated with the current instance of the identity
|
||||||
|
/// Adds a logged-out LocalAccount to track its existence on this device
|
||||||
|
Future<LocalAccount> _newLocalAccount(
|
||||||
|
{required SuperIdentity superIdentity,
|
||||||
|
required SecretKey identitySecret,
|
||||||
|
required proto.Profile newProfile,
|
||||||
|
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
|
||||||
|
String encryptionKey = ''}) async {
|
||||||
|
log.debug('Creating new local account');
|
||||||
|
|
||||||
|
final localAccounts = await _localAccounts.get();
|
||||||
|
|
||||||
|
// Add account with profile to DHT
|
||||||
|
await superIdentity.currentInstance.addAccount(
|
||||||
|
superRecordKey: superIdentity.recordKey,
|
||||||
|
secretKey: identitySecret,
|
||||||
|
accountKey: veilidChatAccountKey,
|
||||||
|
createAccountCallback: (parent) async {
|
||||||
|
// Make empty contact list
|
||||||
|
log.debug('Creating contacts list');
|
||||||
|
final contactList = await (await DHTShortArray.create(
|
||||||
|
debugName: 'AccountRepository::_newLocalAccount::Contacts',
|
||||||
|
parent: parent))
|
||||||
|
.scope((r) async => r.recordPointer);
|
||||||
|
|
||||||
|
// Make empty contact invitation record list
|
||||||
|
log.debug('Creating contact invitation records list');
|
||||||
|
final contactInvitationRecords = await (await DHTShortArray.create(
|
||||||
|
debugName:
|
||||||
|
'AccountRepository::_newLocalAccount::ContactInvitations',
|
||||||
|
parent: parent))
|
||||||
|
.scope((r) async => r.recordPointer);
|
||||||
|
|
||||||
|
// Make empty chat record list
|
||||||
|
log.debug('Creating chat records list');
|
||||||
|
final chatRecords = await (await DHTShortArray.create(
|
||||||
|
debugName: 'AccountRepository::_newLocalAccount::Chats',
|
||||||
|
parent: parent))
|
||||||
|
.scope((r) async => r.recordPointer);
|
||||||
|
|
||||||
|
// Make account object
|
||||||
|
final account = proto.Account()
|
||||||
|
..profile = newProfile
|
||||||
|
..contactList = contactList.toProto()
|
||||||
|
..contactInvitationRecords = contactInvitationRecords.toProto()
|
||||||
|
..chatList = chatRecords.toProto();
|
||||||
|
return account.writeToBuffer();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Encrypt identitySecret with key
|
||||||
|
final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes(
|
||||||
|
secret: identitySecret,
|
||||||
|
cryptoKind: superIdentity.currentInstance.recordKey.kind,
|
||||||
|
encryptionKey: encryptionKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create local account object
|
||||||
|
// Does not contain the account key or its secret
|
||||||
|
// as that is not to be persisted, and only pulled from the identity key
|
||||||
|
// and optionally decrypted with the unlock password
|
||||||
|
final localAccount = LocalAccount(
|
||||||
|
superIdentity: superIdentity,
|
||||||
|
identitySecretBytes: identitySecretBytes,
|
||||||
|
encryptionKeyType: encryptionKeyType,
|
||||||
|
biometricsEnabled: false,
|
||||||
|
hiddenAccount: false,
|
||||||
|
name: newProfile.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add local account object to internal store
|
||||||
|
final newLocalAccounts = localAccounts.add(localAccount);
|
||||||
|
|
||||||
|
await _localAccounts.set(newLocalAccounts);
|
||||||
|
_streamController.add(AccountRepositoryChange.localAccounts);
|
||||||
|
|
||||||
|
// Return local account object
|
||||||
|
return localAccount;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> _decryptedLogin(
|
Future<bool> _decryptedLogin(
|
||||||
SuperIdentity superIdentity, SecretKey identitySecret) async {
|
SuperIdentity superIdentity, SecretKey identitySecret) async {
|
||||||
// Verify identity secret works and return the valid cryptosystem
|
// Verify identity secret works and return the valid cryptosystem
|
||||||
|
|
@ -399,16 +380,13 @@ class AccountRepository {
|
||||||
_streamController.add(AccountRepositoryChange.userLogins);
|
_streamController.add(AccountRepositoryChange.userLogins);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DHTRecord> openAccountRecord(UserLogin userLogin) async {
|
//////////////////////////////////////////////////////////////
|
||||||
final localAccount = fetchLocalAccount(userLogin.superIdentityRecordKey)!;
|
/// Fields
|
||||||
|
|
||||||
// Record not yet open, do it
|
static AccountRepository instance = AccountRepository._();
|
||||||
final pool = DHTRecordPool.instance;
|
|
||||||
final record = await pool.openRecordOwned(
|
|
||||||
userLogin.accountRecordInfo.accountRecord,
|
|
||||||
debugName: 'AccountRepository::openAccountRecord::AccountRecord',
|
|
||||||
parent: localAccount.superIdentity.currentInstance.recordKey);
|
|
||||||
|
|
||||||
return record;
|
final TableDBValue<IList<LocalAccount>> _localAccounts;
|
||||||
}
|
final TableDBValue<IList<UserLogin>> _userLogins;
|
||||||
|
final TableDBValue<TypedKey?> _activeLocalAccount;
|
||||||
|
final StreamController<AccountRepositoryChange> _streamController;
|
||||||
}
|
}
|
||||||
|
|
@ -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:awesome_extensions/awesome_extensions.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../layout/default_app_bar.dart';
|
import '../../layout/default_app_bar.dart';
|
||||||
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../theme/theme.dart';
|
import '../../theme/theme.dart';
|
||||||
import '../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
import '../../veilid_processor/veilid_processor.dart';
|
import '../../veilid_processor/veilid_processor.dart';
|
||||||
import '../account_manager.dart';
|
import '../account_manager.dart';
|
||||||
|
import 'profile_edit_form.dart';
|
||||||
|
|
||||||
class NewAccountPage extends StatefulWidget {
|
class NewAccountPage extends StatefulWidget {
|
||||||
const NewAccountPage({super.key});
|
const NewAccountPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
NewAccountPageState createState() => NewAccountPageState();
|
State createState() => _NewAccountPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class NewAccountPageState extends State<NewAccountPage> {
|
class _NewAccountPageState extends State<NewAccountPage> {
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
bool _isInAsyncCall = false;
|
||||||
late bool isInAsyncCall = false;
|
|
||||||
static const String formFieldName = 'name';
|
|
||||||
static const String formFieldPronouns = 'pronouns';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -47,80 +44,35 @@ class NewAccountPageState extends State<NewAccountPage> {
|
||||||
false;
|
false;
|
||||||
final canSubmit = networkReady;
|
final canSubmit = networkReady;
|
||||||
|
|
||||||
return FormBuilder(
|
return EditProfileForm(
|
||||||
key: _formKey,
|
header: translate('new_account_page.header'),
|
||||||
child: ListView(
|
instructions: translate('new_account_page.instructions'),
|
||||||
children: [
|
submitText: translate('new_account_page.create'),
|
||||||
Text(translate('new_account_page.header'))
|
submitDisabledText: translate('button.waiting_for_network'),
|
||||||
.textStyle(context.headlineSmall)
|
onSubmit: !canSubmit ? null : onSubmit);
|
||||||
.paddingSymmetric(vertical: 16),
|
|
||||||
FormBuilderTextField(
|
|
||||||
autofocus: true,
|
|
||||||
name: formFieldName,
|
|
||||||
decoration:
|
|
||||||
InputDecoration(labelText: translate('account.form_name')),
|
|
||||||
maxLength: 64,
|
|
||||||
// The validator receives the text that the user has entered.
|
|
||||||
validator: FormBuilderValidators.compose([
|
|
||||||
FormBuilderValidators.required(),
|
|
||||||
]),
|
|
||||||
textInputAction: TextInputAction.next,
|
|
||||||
),
|
|
||||||
FormBuilderTextField(
|
|
||||||
name: formFieldPronouns,
|
|
||||||
maxLength: 64,
|
|
||||||
decoration:
|
|
||||||
InputDecoration(labelText: translate('account.form_pronouns')),
|
|
||||||
textInputAction: TextInputAction.next,
|
|
||||||
),
|
|
||||||
Row(children: [
|
|
||||||
const Spacer(),
|
|
||||||
Text(translate('new_account_page.instructions'))
|
|
||||||
.toCenter()
|
|
||||||
.flexible(flex: 6),
|
|
||||||
const Spacer(),
|
|
||||||
]).paddingSymmetric(vertical: 4),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: !canSubmit
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
|
||||||
setState(() {
|
|
||||||
isInAsyncCall = true;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await onSubmit(_formKey);
|
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
isInAsyncCall = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(translate(!networkReady
|
|
||||||
? 'button.waiting_for_network'
|
|
||||||
: 'new_account_page.create')),
|
|
||||||
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final displayModalHUD = isInAsyncCall;
|
final displayModalHUD = _isInAsyncCall;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// resizeToAvoidBottomInset: false,
|
// resizeToAvoidBottomInset: false,
|
||||||
appBar: DefaultAppBar(
|
appBar: DefaultAppBar(
|
||||||
title: Text(translate('new_account_page.titlebar')),
|
title: Text(translate('new_account_page.titlebar')),
|
||||||
|
leading: Navigator.canPop(context)
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
actions: [
|
actions: [
|
||||||
const SignalStrengthMeterWidget(),
|
const SignalStrengthMeterWidget(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.settings),
|
icon: const Icon(Icons.settings),
|
||||||
tooltip: translate('app_bar.settings_tooltip'),
|
tooltip: translate('menu.settings_tooltip'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await GoRouterHelper(context).push('/settings');
|
await GoRouterHelper(context).push('/settings');
|
||||||
})
|
})
|
||||||
|
|
@ -132,16 +84,33 @@ class NewAccountPageState extends State<NewAccountPage> {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final name =
|
final name = formKey.currentState!
|
||||||
_formKey.currentState!.fields[formFieldName]!.value as String;
|
.fields[EditProfileForm.formFieldName]!.value as String;
|
||||||
final pronouns = _formKey.currentState!.fields[formFieldPronouns]!
|
final pronouns = formKey
|
||||||
|
.currentState!
|
||||||
|
.fields[EditProfileForm.formFieldPronouns]!
|
||||||
.value as String? ??
|
.value as String? ??
|
||||||
'';
|
'';
|
||||||
final newProfileSpec =
|
final newProfile = proto.Profile()
|
||||||
NewProfileSpec(name: name, pronouns: pronouns);
|
..name = name
|
||||||
|
..pronouns = pronouns;
|
||||||
|
|
||||||
await AccountRepository.instance
|
setState(() {
|
||||||
.createWithNewSuperIdentity(newProfileSpec);
|
_isInAsyncCall = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final superSecret = await AccountRepository.instance
|
||||||
|
.createWithNewSuperIdentity(newProfile);
|
||||||
|
GoRouterHelper(context).pushReplacement(
|
||||||
|
'/new_account/recovery_key',
|
||||||
|
extra: superSecret);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isInAsyncCall = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
await showErrorModal(context, translate('new_account_page.error'),
|
await showErrorModal(context, translate('new_account_page.error'),
|
||||||
|
|
@ -152,10 +121,4 @@ class NewAccountPageState extends State<NewAccountPage> {
|
||||||
).paddingSymmetric(horizontal: 24, vertical: 8),
|
).paddingSymmetric(horizontal: 24, vertical: 8),
|
||||||
).withModalHUD(context, displayModalHUD);
|
).withModalHUD(context, displayModalHUD);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
||||||
super.debugFillProperties(properties);
|
|
||||||
properties.add(DiagnosticsProperty<bool>('isInAsyncCall', isInAsyncCall));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 'new_account_page.dart';
|
||||||
export 'profile_widget.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 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import 'account_manager/account_manager.dart';
|
import 'account_manager/account_manager.dart';
|
||||||
|
import 'account_manager/cubits/active_local_account_cubit.dart';
|
||||||
import 'init.dart';
|
import 'init.dart';
|
||||||
import 'layout/splash.dart';
|
import 'layout/splash.dart';
|
||||||
import 'router/router.dart';
|
import 'router/router.dart';
|
||||||
|
|
@ -129,7 +130,11 @@ class VeilidChatApp extends StatelessWidget {
|
||||||
BlocProvider<PreferencesCubit>(
|
BlocProvider<PreferencesCubit>(
|
||||||
create: (context) =>
|
create: (context) =>
|
||||||
PreferencesCubit(PreferencesRepository.instance),
|
PreferencesCubit(PreferencesRepository.instance),
|
||||||
)
|
),
|
||||||
|
BlocProvider<PerAccountCollectionBlocMapCubit>(
|
||||||
|
create: (context) => PerAccountCollectionBlocMapCubit(
|
||||||
|
accountRepository: AccountRepository.instance,
|
||||||
|
locator: context.read)),
|
||||||
],
|
],
|
||||||
child: BackgroundTicker(
|
child: BackgroundTicker(
|
||||||
child: _buildShortcuts(
|
child: _buildShortcuts(
|
||||||
|
|
@ -137,7 +142,7 @@ class VeilidChatApp extends StatelessWidget {
|
||||||
builder: (context) => MaterialApp.router(
|
builder: (context) => MaterialApp.router(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
routerConfig:
|
routerConfig:
|
||||||
context.watch<RouterCubit>().router(),
|
context.read<RouterCubit>().router(),
|
||||||
title: translate('app.title'),
|
title: translate('app.title'),
|
||||||
theme: theme,
|
theme: theme,
|
||||||
localizationsDelegates: [
|
localizationsDelegates: [
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:async_tools/async_tools.dart';
|
import 'package:async_tools/async_tools.dart';
|
||||||
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
@ -12,7 +13,8 @@ import 'package:scroll_to_index/scroll_to_index.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../account_manager/account_manager.dart';
|
import '../../account_manager/account_manager.dart';
|
||||||
import '../../chat_list/chat_list.dart';
|
import '../../contacts/contacts.dart';
|
||||||
|
import '../../conversation/conversation.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../models/chat_component_state.dart';
|
import '../models/chat_component_state.dart';
|
||||||
import '../models/message_state.dart';
|
import '../models/message_state.dart';
|
||||||
|
|
@ -23,18 +25,27 @@ const metadataKeyIdentityPublicKey = 'identityPublicKey';
|
||||||
const metadataKeyExpirationDuration = 'expiration';
|
const metadataKeyExpirationDuration = 'expiration';
|
||||||
const metadataKeyViewLimit = 'view_limit';
|
const metadataKeyViewLimit = 'view_limit';
|
||||||
const metadataKeyAttachments = 'attachments';
|
const metadataKeyAttachments = 'attachments';
|
||||||
|
const _sfChangedContacts = 'changedContacts';
|
||||||
|
|
||||||
class ChatComponentCubit extends Cubit<ChatComponentState> {
|
class ChatComponentCubit extends Cubit<ChatComponentState> {
|
||||||
ChatComponentCubit._({
|
ChatComponentCubit._({
|
||||||
|
required AccountInfo accountInfo,
|
||||||
|
required AccountRecordCubit accountRecordCubit,
|
||||||
|
required ContactListCubit contactListCubit,
|
||||||
|
required List<ActiveConversationCubit> conversationCubits,
|
||||||
required SingleContactMessagesCubit messagesCubit,
|
required SingleContactMessagesCubit messagesCubit,
|
||||||
required types.User localUser,
|
}) : _accountInfo = accountInfo,
|
||||||
required IMap<TypedKey, types.User> remoteUsers,
|
_accountRecordCubit = accountRecordCubit,
|
||||||
}) : _messagesCubit = messagesCubit,
|
_contactListCubit = contactListCubit,
|
||||||
|
_conversationCubits = conversationCubits,
|
||||||
|
_messagesCubit = messagesCubit,
|
||||||
super(ChatComponentState(
|
super(ChatComponentState(
|
||||||
chatKey: GlobalKey<ChatState>(),
|
chatKey: GlobalKey<ChatState>(),
|
||||||
scrollController: AutoScrollController(),
|
scrollController: AutoScrollController(),
|
||||||
localUser: localUser,
|
localUser: null,
|
||||||
remoteUsers: remoteUsers,
|
remoteUsers: const IMap.empty(),
|
||||||
|
historicalRemoteUsers: const IMap.empty(),
|
||||||
|
unknownUsers: const IMap.empty(),
|
||||||
messageWindow: const AsyncLoading(),
|
messageWindow: const AsyncLoading(),
|
||||||
title: '',
|
title: '',
|
||||||
)) {
|
)) {
|
||||||
|
|
@ -42,54 +53,49 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
||||||
_initWait.add(_init);
|
_initWait.add(_init);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore: prefer_constructors_over_static_methods
|
factory ChatComponentCubit.singleContact(
|
||||||
static ChatComponentCubit singleContact(
|
{required AccountInfo accountInfo,
|
||||||
{required ActiveAccountInfo activeAccountInfo,
|
required AccountRecordCubit accountRecordCubit,
|
||||||
required proto.Account accountRecordInfo,
|
required ContactListCubit contactListCubit,
|
||||||
required ActiveConversationState activeConversationState,
|
required ActiveConversationCubit activeConversationCubit,
|
||||||
required SingleContactMessagesCubit messagesCubit}) {
|
required SingleContactMessagesCubit messagesCubit}) =>
|
||||||
// Make local 'User'
|
ChatComponentCubit._(
|
||||||
final localUserIdentityKey = activeAccountInfo.identityTypedPublicKey;
|
accountInfo: accountInfo,
|
||||||
final localUser = types.User(
|
accountRecordCubit: accountRecordCubit,
|
||||||
id: localUserIdentityKey.toString(),
|
contactListCubit: contactListCubit,
|
||||||
firstName: accountRecordInfo.profile.name,
|
conversationCubits: [activeConversationCubit],
|
||||||
metadata: {metadataKeyIdentityPublicKey: localUserIdentityKey});
|
messagesCubit: messagesCubit,
|
||||||
// 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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
_messagesSubscription = _messagesCubit.stream.listen((messagesState) {
|
// Get local user info and account record cubit
|
||||||
emit(state.copyWith(
|
_localUserIdentityKey = _accountInfo.identityTypedPublicKey;
|
||||||
messageWindow: _convertMessages(messagesState),
|
|
||||||
));
|
// Subscribe to local user info
|
||||||
});
|
_accountRecordSubscription =
|
||||||
emit(state.copyWith(
|
_accountRecordCubit.stream.listen(_onChangedAccountRecord);
|
||||||
messageWindow: _convertMessages(_messagesCubit.state),
|
_onChangedAccountRecord(_accountRecordCubit.state);
|
||||||
title: _getTitle(),
|
|
||||||
));
|
// Subscribe to remote user info
|
||||||
|
await _updateConversationSubscriptions();
|
||||||
|
|
||||||
|
// Subscribe to messages
|
||||||
|
_messagesSubscription = _messagesCubit.stream.listen(_onChangedMessages);
|
||||||
|
_onChangedMessages(_messagesCubit.state);
|
||||||
|
|
||||||
|
// Subscribe to contact list changes
|
||||||
|
_contactListSubscription =
|
||||||
|
_contactListCubit.stream.listen(_onChangedContacts);
|
||||||
|
_onChangedContacts(_contactListCubit.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await _initWait();
|
await _initWait();
|
||||||
|
await _contactListSubscription.cancel();
|
||||||
|
await _accountRecordSubscription.cancel();
|
||||||
await _messagesSubscription.cancel();
|
await _messagesSubscription.cancel();
|
||||||
|
await _conversationSubscriptions.values.map((v) => v.cancel()).wait;
|
||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,23 +159,162 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Private Implementation
|
// Private Implementation
|
||||||
|
|
||||||
String _getTitle() {
|
void _onChangedAccountRecord(AsyncValue<proto.Account> avAccount) {
|
||||||
if (state.remoteUsers.length == 1) {
|
// Update local 'User'
|
||||||
final remoteUser = state.remoteUsers.values.first;
|
final account = avAccount.asData?.value;
|
||||||
return remoteUser.firstName ?? '<unnamed>';
|
if (account == null) {
|
||||||
} else {
|
emit(state.copyWith(localUser: null));
|
||||||
return '<group chat with ${state.remoteUsers.length} users>';
|
return;
|
||||||
}
|
}
|
||||||
|
final localUser = types.User(
|
||||||
|
id: _localUserIdentityKey.toString(),
|
||||||
|
firstName: account.profile.name,
|
||||||
|
metadata: {metadataKeyIdentityPublicKey: _localUserIdentityKey});
|
||||||
|
emit(state.copyWith(localUser: localUser));
|
||||||
}
|
}
|
||||||
|
|
||||||
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 authorIdentityPublicKey = message.content.author.toVeilid();
|
||||||
final author =
|
late final types.User author;
|
||||||
state.remoteUsers[authorIdentityPublicKey] ?? state.localUser;
|
if (authorIdentityPublicKey == _localUserIdentityKey &&
|
||||||
|
currentState.localUser != null) {
|
||||||
|
author = currentState.localUser!;
|
||||||
|
} else {
|
||||||
|
final remoteUser = currentState.remoteUsers[authorIdentityPublicKey];
|
||||||
|
if (remoteUser != null) {
|
||||||
|
author = remoteUser;
|
||||||
|
} else {
|
||||||
|
final historicalRemoteUser =
|
||||||
|
currentState.historicalRemoteUsers[authorIdentityPublicKey];
|
||||||
|
if (historicalRemoteUser != null) {
|
||||||
|
author = historicalRemoteUser;
|
||||||
|
} else {
|
||||||
|
final unknownRemoteUser =
|
||||||
|
currentState.unknownUsers[authorIdentityPublicKey];
|
||||||
|
if (unknownRemoteUser != null) {
|
||||||
|
author = unknownRemoteUser;
|
||||||
|
} else {
|
||||||
|
final unknownUser = _convertUnknownUser(authorIdentityPublicKey);
|
||||||
|
currentState = currentState.copyWith(
|
||||||
|
unknownUsers: currentState.unknownUsers
|
||||||
|
.add(authorIdentityPublicKey, unknownUser));
|
||||||
|
author = unknownUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
types.Status? status;
|
types.Status? status;
|
||||||
if (message.sendState != null) {
|
if (message.sendState != null) {
|
||||||
assert(author == state.localUser,
|
assert(author.id == _localUserIdentityKey.toString(),
|
||||||
'send state should only be on sent messages');
|
'send state should only be on sent messages');
|
||||||
switch (message.sendState!) {
|
switch (message.sendState!) {
|
||||||
case MessageSendState.sending:
|
case MessageSendState.sending:
|
||||||
|
|
@ -192,7 +337,7 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
||||||
text: contextText.text,
|
text: contextText.text,
|
||||||
showStatus: status != null,
|
showStatus: status != null,
|
||||||
status: status);
|
status: status);
|
||||||
return textMessage;
|
return (currentState, textMessage);
|
||||||
case proto.Message_Kind.secret:
|
case proto.Message_Kind.secret:
|
||||||
case proto.Message_Kind.delete:
|
case proto.Message_Kind.delete:
|
||||||
case proto.Message_Kind.erase:
|
case proto.Message_Kind.erase:
|
||||||
|
|
@ -201,17 +346,24 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
||||||
case proto.Message_Kind.membership:
|
case proto.Message_Kind.membership:
|
||||||
case proto.Message_Kind.moderation:
|
case proto.Message_Kind.moderation:
|
||||||
case proto.Message_Kind.notSet:
|
case proto.Message_Kind.notSet:
|
||||||
return null;
|
return (currentState, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncValue<WindowState<types.Message>> _convertMessages(
|
ChatComponentState _convertMessages(ChatComponentState currentState,
|
||||||
AsyncValue<WindowState<MessageState>> avMessagesState) {
|
AsyncValue<WindowState<MessageState>> avMessagesState) {
|
||||||
|
// Clear out unknown users
|
||||||
|
currentState = state.copyWith(unknownUsers: const IMap.empty());
|
||||||
|
|
||||||
final asError = avMessagesState.asError;
|
final asError = avMessagesState.asError;
|
||||||
if (asError != null) {
|
if (asError != null) {
|
||||||
return AsyncValue.error(asError.error, asError.stackTrace);
|
return currentState.copyWith(
|
||||||
|
unknownUsers: const IMap.empty(),
|
||||||
|
messageWindow: AsyncValue.error(asError.error, asError.stackTrace));
|
||||||
} else if (avMessagesState.asLoading != null) {
|
} else if (avMessagesState.asLoading != null) {
|
||||||
return const AsyncValue.loading();
|
return currentState.copyWith(
|
||||||
|
unknownUsers: const IMap.empty(),
|
||||||
|
messageWindow: const AsyncValue.loading());
|
||||||
}
|
}
|
||||||
final messagesState = avMessagesState.asData!.value;
|
final messagesState = avMessagesState.asData!.value;
|
||||||
|
|
||||||
|
|
@ -219,7 +371,9 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
||||||
final chatMessages = <types.Message>[];
|
final chatMessages = <types.Message>[];
|
||||||
final tsSet = <String>{};
|
final tsSet = <String>{};
|
||||||
for (final message in messagesState.window) {
|
for (final message in messagesState.window) {
|
||||||
final chatMessage = _messageStateToChatMessage(message);
|
final (newState, chatMessage) =
|
||||||
|
_messageStateToChatMessage(currentState, message);
|
||||||
|
currentState = newState;
|
||||||
if (chatMessage == null) {
|
if (chatMessage == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -232,12 +386,13 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
||||||
assert(false, 'should not have duplicate id');
|
assert(false, 'should not have duplicate id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return AsyncValue.data(WindowState<types.Message>(
|
return currentState.copyWith(
|
||||||
window: chatMessages.toIList(),
|
messageWindow: AsyncValue.data(WindowState<types.Message>(
|
||||||
length: messagesState.length,
|
window: chatMessages.toIList(),
|
||||||
windowTail: messagesState.windowTail,
|
length: messagesState.length,
|
||||||
windowCount: messagesState.windowCount,
|
windowTail: messagesState.windowTail,
|
||||||
follow: messagesState.follow));
|
windowCount: messagesState.windowCount,
|
||||||
|
follow: messagesState.follow)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addTextMessage(
|
void _addTextMessage(
|
||||||
|
|
@ -265,7 +420,21 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
final _initWait = WaitSet<void>();
|
final _initWait = WaitSet<void>();
|
||||||
|
final AccountInfo _accountInfo;
|
||||||
|
final AccountRecordCubit _accountRecordCubit;
|
||||||
|
final ContactListCubit _contactListCubit;
|
||||||
|
final List<ActiveConversationCubit> _conversationCubits;
|
||||||
final SingleContactMessagesCubit _messagesCubit;
|
final SingleContactMessagesCubit _messagesCubit;
|
||||||
|
|
||||||
|
late final TypedKey _localUserIdentityKey;
|
||||||
|
late final StreamSubscription<AsyncValue<proto.Account>>
|
||||||
|
_accountRecordSubscription;
|
||||||
|
final Map<TypedKey, StreamSubscription<AsyncValue<ActiveConversationState>>>
|
||||||
|
_conversationSubscriptions = {};
|
||||||
late StreamSubscription<SingleContactMessagesState> _messagesSubscription;
|
late StreamSubscription<SingleContactMessagesState> _messagesSubscription;
|
||||||
|
late StreamSubscription<
|
||||||
|
BlocBusyState<
|
||||||
|
AsyncValue<IList<DHTShortArrayElementState<proto.Contact>>>>>
|
||||||
|
_contactListSubscription;
|
||||||
double scrollOffset = 0;
|
double scrollOffset = 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../../proto/proto.dart' as proto;
|
import '../../../proto/proto.dart' as proto;
|
||||||
|
|
||||||
|
import '../../../tools/tools.dart';
|
||||||
import 'author_input_source.dart';
|
import 'author_input_source.dart';
|
||||||
import 'message_integrity.dart';
|
import 'message_integrity.dart';
|
||||||
import 'output_position.dart';
|
import 'output_position.dart';
|
||||||
|
|
@ -84,6 +85,8 @@ class AuthorInputQueue {
|
||||||
if (_lastMessage != null) {
|
if (_lastMessage != null) {
|
||||||
// Ensure the timestamp is not moving backward
|
// Ensure the timestamp is not moving backward
|
||||||
if (nextMessage.value.timestamp < _lastMessage!.timestamp) {
|
if (nextMessage.value.timestamp < _lastMessage!.timestamp) {
|
||||||
|
log.warning('timestamp backward: ${nextMessage.value.timestamp}'
|
||||||
|
' < ${_lastMessage!.timestamp}');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,11 +94,14 @@ class AuthorInputQueue {
|
||||||
// Verify the id chain for the message
|
// Verify the id chain for the message
|
||||||
final matchId = await _messageIntegrity.generateMessageId(_lastMessage);
|
final matchId = await _messageIntegrity.generateMessageId(_lastMessage);
|
||||||
if (matchId.compare(nextMessage.value.idBytes) != 0) {
|
if (matchId.compare(nextMessage.value.idBytes) != 0) {
|
||||||
|
log.warning(
|
||||||
|
'id chain invalid: $matchId != ${nextMessage.value.idBytes}');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the signature for the message
|
// Verify the signature for the message
|
||||||
if (!await _messageIntegrity.verifyMessage(nextMessage.value)) {
|
if (!await _messageIntegrity.verifyMessage(nextMessage.value)) {
|
||||||
|
log.warning('invalid message signature: ${nextMessage.value}');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,13 +50,13 @@ typedef SingleContactMessagesState = AsyncValue<WindowState<MessageState>>;
|
||||||
// Builds the reconciled chat record from the local and remote conversation keys
|
// Builds the reconciled chat record from the local and remote conversation keys
|
||||||
class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
SingleContactMessagesCubit({
|
SingleContactMessagesCubit({
|
||||||
required ActiveAccountInfo activeAccountInfo,
|
required AccountInfo accountInfo,
|
||||||
required TypedKey remoteIdentityPublicKey,
|
required TypedKey remoteIdentityPublicKey,
|
||||||
required TypedKey localConversationRecordKey,
|
required TypedKey localConversationRecordKey,
|
||||||
required TypedKey localMessagesRecordKey,
|
required TypedKey localMessagesRecordKey,
|
||||||
required TypedKey remoteConversationRecordKey,
|
required TypedKey remoteConversationRecordKey,
|
||||||
required TypedKey remoteMessagesRecordKey,
|
required TypedKey remoteMessagesRecordKey,
|
||||||
}) : _activeAccountInfo = activeAccountInfo,
|
}) : _accountInfo = accountInfo,
|
||||||
_remoteIdentityPublicKey = remoteIdentityPublicKey,
|
_remoteIdentityPublicKey = remoteIdentityPublicKey,
|
||||||
_localConversationRecordKey = localConversationRecordKey,
|
_localConversationRecordKey = localConversationRecordKey,
|
||||||
_localMessagesRecordKey = localMessagesRecordKey,
|
_localMessagesRecordKey = localMessagesRecordKey,
|
||||||
|
|
@ -81,6 +81,16 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
await _sentMessagesCubit?.close();
|
await _sentMessagesCubit?.close();
|
||||||
await _rcvdMessagesCubit?.close();
|
await _rcvdMessagesCubit?.close();
|
||||||
await _reconciledMessagesCubit?.close();
|
await _reconciledMessagesCubit?.close();
|
||||||
|
|
||||||
|
// If the local conversation record is gone, then delete the reconciled
|
||||||
|
// messages table as well
|
||||||
|
final conversationDead = await DHTRecordPool.instance
|
||||||
|
.isDeletedRecordKey(_localConversationRecordKey);
|
||||||
|
if (conversationDead) {
|
||||||
|
await SingleContactMessagesCubit.cleanupAndDeleteMessages(
|
||||||
|
localConversationRecordKey: _localConversationRecordKey);
|
||||||
|
}
|
||||||
|
|
||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,15 +121,15 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
|
|
||||||
// Make crypto
|
// Make crypto
|
||||||
Future<void> _initCrypto() async {
|
Future<void> _initCrypto() async {
|
||||||
_conversationCrypto = await _activeAccountInfo
|
_conversationCrypto =
|
||||||
.makeConversationCrypto(_remoteIdentityPublicKey);
|
await _accountInfo.makeConversationCrypto(_remoteIdentityPublicKey);
|
||||||
_senderMessageIntegrity = await MessageIntegrity.create(
|
_senderMessageIntegrity = await MessageIntegrity.create(
|
||||||
author: _activeAccountInfo.identityTypedPublicKey);
|
author: _accountInfo.identityTypedPublicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open local messages key
|
// Open local messages key
|
||||||
Future<void> _initSentMessagesCubit() async {
|
Future<void> _initSentMessagesCubit() async {
|
||||||
final writer = _activeAccountInfo.identityWriter;
|
final writer = _accountInfo.identityWriter;
|
||||||
|
|
||||||
_sentMessagesCubit = DHTLogCubit(
|
_sentMessagesCubit = DHTLogCubit(
|
||||||
open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer,
|
open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer,
|
||||||
|
|
@ -149,7 +159,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
|
|
||||||
Future<VeilidCrypto> _makeLocalMessagesCrypto() async =>
|
Future<VeilidCrypto> _makeLocalMessagesCrypto() async =>
|
||||||
VeilidCryptoPrivate.fromTypedKey(
|
VeilidCryptoPrivate.fromTypedKey(
|
||||||
_activeAccountInfo.userLogin.identitySecret, 'tabledb');
|
_accountInfo.userLogin!.identitySecret, 'tabledb');
|
||||||
|
|
||||||
// Open reconciled chat record key
|
// Open reconciled chat record key
|
||||||
Future<void> _initReconciledMessagesCubit() async {
|
Future<void> _initReconciledMessagesCubit() async {
|
||||||
|
|
@ -240,8 +250,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_reconciliation.reconcileMessages(_activeAccountInfo.identityTypedPublicKey,
|
_reconciliation.reconcileMessages(
|
||||||
sentMessages, _sentMessagesCubit!);
|
_accountInfo.identityTypedPublicKey, sentMessages, _sentMessagesCubit!);
|
||||||
|
|
||||||
// Update the view
|
// Update the view
|
||||||
_renderState();
|
_renderState();
|
||||||
|
|
@ -278,7 +288,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
|
|
||||||
// Now sign it
|
// Now sign it
|
||||||
await _senderMessageIntegrity.signMessage(
|
await _senderMessageIntegrity.signMessage(
|
||||||
message, _activeAccountInfo.identitySecretKey);
|
message, _accountInfo.identitySecretKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Async process to send messages in the background
|
// Async process to send messages in the background
|
||||||
|
|
@ -291,8 +301,14 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
previousMessage = message;
|
previousMessage = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// _sendingMessages = messages;
|
||||||
|
|
||||||
|
// _renderState();
|
||||||
|
|
||||||
await _sentMessagesCubit!.operateAppendEventual((writer) =>
|
await _sentMessagesCubit!.operateAppendEventual((writer) =>
|
||||||
writer.addAll(messages.map((m) => m.writeToBuffer()).toList()));
|
writer.addAll(messages.map((m) => m.writeToBuffer()).toList()));
|
||||||
|
|
||||||
|
// _sendingMessages = const IList.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Produce a state for this cubit from the input cubits and queues
|
// Produce a state for this cubit from the input cubits and queues
|
||||||
|
|
@ -302,8 +318,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
_reconciledMessagesCubit?.state.state.asData?.value;
|
_reconciledMessagesCubit?.state.state.asData?.value;
|
||||||
// Get all sent messages
|
// Get all sent messages
|
||||||
final sentMessages = _sentMessagesCubit?.state.state.asData?.value;
|
final sentMessages = _sentMessagesCubit?.state.state.asData?.value;
|
||||||
// Get all items in the unsent queue
|
//Get all items in the unsent queue
|
||||||
// final unsentMessages = _unsentMessagesQueue.queue;
|
//final unsentMessages = _unsentMessagesQueue.queue;
|
||||||
|
|
||||||
// If we aren't ready to render a state, say we're loading
|
// If we aren't ready to render a state, say we're loading
|
||||||
if (reconciledMessages == null || sentMessages == null) {
|
if (reconciledMessages == null || sentMessages == null) {
|
||||||
|
|
@ -315,7 +331,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
// final reconciledMessagesMap =
|
// final reconciledMessagesMap =
|
||||||
// IMap<String, proto.ReconciledMessage>.fromValues(
|
// IMap<String, proto.ReconciledMessage>.fromValues(
|
||||||
// keyMapper: (x) => x.content.authorUniqueIdString,
|
// keyMapper: (x) => x.content.authorUniqueIdString,
|
||||||
// values: reconciledMessages.elements,
|
// values: reconciledMessages.windowElements,
|
||||||
// );
|
// );
|
||||||
final sentMessagesMap =
|
final sentMessagesMap =
|
||||||
IMap<String, OnlineElementState<proto.Message>>.fromValues(
|
IMap<String, OnlineElementState<proto.Message>>.fromValues(
|
||||||
|
|
@ -328,10 +344,10 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
// );
|
// );
|
||||||
|
|
||||||
final renderedElements = <RenderStateElement>[];
|
final renderedElements = <RenderStateElement>[];
|
||||||
|
final renderedIds = <String>{};
|
||||||
for (final m in reconciledMessages.windowElements) {
|
for (final m in reconciledMessages.windowElements) {
|
||||||
final isLocal = m.content.author.toVeilid() ==
|
final isLocal =
|
||||||
_activeAccountInfo.identityTypedPublicKey;
|
m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey;
|
||||||
final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime);
|
final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime);
|
||||||
final sm =
|
final sm =
|
||||||
isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null;
|
isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null;
|
||||||
|
|
@ -345,8 +361,23 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
sent: sent,
|
sent: sent,
|
||||||
sentOffline: sentOffline,
|
sentOffline: sentOffline,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
renderedIds.add(m.content.authorUniqueIdString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render in-flight messages at the bottom
|
||||||
|
// for (final m in _sendingMessages) {
|
||||||
|
// if (renderedIds.contains(m.authorUniqueIdString)) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// renderedElements.add(RenderStateElement(
|
||||||
|
// message: m,
|
||||||
|
// isLocal: true,
|
||||||
|
// sent: true,
|
||||||
|
// sentOffline: true,
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
|
||||||
// Render the state
|
// Render the state
|
||||||
final messages = renderedElements
|
final messages = renderedElements
|
||||||
.map((x) => MessageState(
|
.map((x) => MessageState(
|
||||||
|
|
@ -369,7 +400,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
// Add common fields
|
// Add common fields
|
||||||
// id and signature will get set by _processMessageToSend
|
// id and signature will get set by _processMessageToSend
|
||||||
message
|
message
|
||||||
..author = _activeAccountInfo.identityTypedPublicKey.toProto()
|
..author = _accountInfo.identityTypedPublicKey.toProto()
|
||||||
..timestamp = Veilid.instance.now().toInt64();
|
..timestamp = Veilid.instance.now().toInt64();
|
||||||
|
|
||||||
// Put in the queue
|
// Put in the queue
|
||||||
|
|
@ -402,7 +433,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
/////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
final WaitSet<void> _initWait = WaitSet();
|
final WaitSet<void> _initWait = WaitSet();
|
||||||
final ActiveAccountInfo _activeAccountInfo;
|
late final AccountInfo _accountInfo;
|
||||||
final TypedKey _remoteIdentityPublicKey;
|
final TypedKey _remoteIdentityPublicKey;
|
||||||
final TypedKey _localConversationRecordKey;
|
final TypedKey _localConversationRecordKey;
|
||||||
final TypedKey _localMessagesRecordKey;
|
final TypedKey _localMessagesRecordKey;
|
||||||
|
|
@ -419,7 +450,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
||||||
late final MessageReconciliation _reconciliation;
|
late final MessageReconciliation _reconciliation;
|
||||||
|
|
||||||
late final PersistentQueue<proto.Message> _unsentMessagesQueue;
|
late final PersistentQueue<proto.Message> _unsentMessagesQueue;
|
||||||
|
// IList<proto.Message> _sendingMessages = const IList.empty();
|
||||||
StreamSubscription<DHTLogBusyState<proto.Message>>? _sentSubscription;
|
StreamSubscription<DHTLogBusyState<proto.Message>>? _sentSubscription;
|
||||||
StreamSubscription<DHTLogBusyState<proto.Message>>? _rcvdSubscription;
|
StreamSubscription<DHTLogBusyState<proto.Message>>? _rcvdSubscription;
|
||||||
StreamSubscription<TableDBArrayProtobufBusyState<proto.ReconciledMessage>>?
|
StreamSubscription<TableDBArrayProtobufBusyState<proto.ReconciledMessage>>?
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,13 @@ class ChatComponentState with _$ChatComponentState {
|
||||||
// ScrollController for the chat
|
// ScrollController for the chat
|
||||||
required AutoScrollController scrollController,
|
required AutoScrollController scrollController,
|
||||||
// Local user
|
// Local user
|
||||||
required User localUser,
|
required User? localUser,
|
||||||
// Remote users
|
// Active remote users
|
||||||
required IMap<TypedKey, User> remoteUsers,
|
required IMap<TypedKey, User> remoteUsers,
|
||||||
|
// Historical remote users
|
||||||
|
required IMap<TypedKey, User> historicalRemoteUsers,
|
||||||
|
// Unknown users
|
||||||
|
required IMap<TypedKey, User> unknownUsers,
|
||||||
// Messages state
|
// Messages state
|
||||||
required AsyncValue<WindowState<Message>> messageWindow,
|
required AsyncValue<WindowState<Message>> messageWindow,
|
||||||
// Title of the chat
|
// Title of the chat
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,13 @@ mixin _$ChatComponentState {
|
||||||
throw _privateConstructorUsedError; // ScrollController for the chat
|
throw _privateConstructorUsedError; // ScrollController for the chat
|
||||||
AutoScrollController get scrollController =>
|
AutoScrollController get scrollController =>
|
||||||
throw _privateConstructorUsedError; // Local user
|
throw _privateConstructorUsedError; // Local user
|
||||||
User get localUser => throw _privateConstructorUsedError; // Remote users
|
User? get localUser =>
|
||||||
|
throw _privateConstructorUsedError; // Active remote users
|
||||||
IMap<Typed<FixedEncodedString43>, User> get remoteUsers =>
|
IMap<Typed<FixedEncodedString43>, User> get remoteUsers =>
|
||||||
|
throw _privateConstructorUsedError; // Historical remote users
|
||||||
|
IMap<Typed<FixedEncodedString43>, User> get historicalRemoteUsers =>
|
||||||
|
throw _privateConstructorUsedError; // Unknown users
|
||||||
|
IMap<Typed<FixedEncodedString43>, User> get unknownUsers =>
|
||||||
throw _privateConstructorUsedError; // Messages state
|
throw _privateConstructorUsedError; // Messages state
|
||||||
AsyncValue<WindowState<Message>> get messageWindow =>
|
AsyncValue<WindowState<Message>> get messageWindow =>
|
||||||
throw _privateConstructorUsedError; // Title of the chat
|
throw _privateConstructorUsedError; // Title of the chat
|
||||||
|
|
@ -42,8 +47,10 @@ abstract class $ChatComponentStateCopyWith<$Res> {
|
||||||
$Res call(
|
$Res call(
|
||||||
{GlobalKey<ChatState> chatKey,
|
{GlobalKey<ChatState> chatKey,
|
||||||
AutoScrollController scrollController,
|
AutoScrollController scrollController,
|
||||||
User localUser,
|
User? localUser,
|
||||||
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
|
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
|
||||||
|
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
|
||||||
|
IMap<Typed<FixedEncodedString43>, User> unknownUsers,
|
||||||
AsyncValue<WindowState<Message>> messageWindow,
|
AsyncValue<WindowState<Message>> messageWindow,
|
||||||
String title});
|
String title});
|
||||||
|
|
||||||
|
|
@ -65,8 +72,10 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState>
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? chatKey = null,
|
Object? chatKey = null,
|
||||||
Object? scrollController = null,
|
Object? scrollController = null,
|
||||||
Object? localUser = null,
|
Object? localUser = freezed,
|
||||||
Object? remoteUsers = null,
|
Object? remoteUsers = null,
|
||||||
|
Object? historicalRemoteUsers = null,
|
||||||
|
Object? unknownUsers = null,
|
||||||
Object? messageWindow = null,
|
Object? messageWindow = null,
|
||||||
Object? title = null,
|
Object? title = null,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -79,14 +88,22 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState>
|
||||||
? _value.scrollController
|
? _value.scrollController
|
||||||
: scrollController // ignore: cast_nullable_to_non_nullable
|
: scrollController // ignore: cast_nullable_to_non_nullable
|
||||||
as AutoScrollController,
|
as AutoScrollController,
|
||||||
localUser: null == localUser
|
localUser: freezed == localUser
|
||||||
? _value.localUser
|
? _value.localUser
|
||||||
: localUser // ignore: cast_nullable_to_non_nullable
|
: localUser // ignore: cast_nullable_to_non_nullable
|
||||||
as User,
|
as User?,
|
||||||
remoteUsers: null == remoteUsers
|
remoteUsers: null == remoteUsers
|
||||||
? _value.remoteUsers
|
? _value.remoteUsers
|
||||||
: remoteUsers // ignore: cast_nullable_to_non_nullable
|
: remoteUsers // ignore: cast_nullable_to_non_nullable
|
||||||
as IMap<Typed<FixedEncodedString43>, User>,
|
as IMap<Typed<FixedEncodedString43>, User>,
|
||||||
|
historicalRemoteUsers: null == historicalRemoteUsers
|
||||||
|
? _value.historicalRemoteUsers
|
||||||
|
: historicalRemoteUsers // ignore: cast_nullable_to_non_nullable
|
||||||
|
as IMap<Typed<FixedEncodedString43>, User>,
|
||||||
|
unknownUsers: null == unknownUsers
|
||||||
|
? _value.unknownUsers
|
||||||
|
: unknownUsers // ignore: cast_nullable_to_non_nullable
|
||||||
|
as IMap<Typed<FixedEncodedString43>, User>,
|
||||||
messageWindow: null == messageWindow
|
messageWindow: null == messageWindow
|
||||||
? _value.messageWindow
|
? _value.messageWindow
|
||||||
: messageWindow // ignore: cast_nullable_to_non_nullable
|
: messageWindow // ignore: cast_nullable_to_non_nullable
|
||||||
|
|
@ -119,8 +136,10 @@ abstract class _$$ChatComponentStateImplCopyWith<$Res>
|
||||||
$Res call(
|
$Res call(
|
||||||
{GlobalKey<ChatState> chatKey,
|
{GlobalKey<ChatState> chatKey,
|
||||||
AutoScrollController scrollController,
|
AutoScrollController scrollController,
|
||||||
User localUser,
|
User? localUser,
|
||||||
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
|
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
|
||||||
|
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
|
||||||
|
IMap<Typed<FixedEncodedString43>, User> unknownUsers,
|
||||||
AsyncValue<WindowState<Message>> messageWindow,
|
AsyncValue<WindowState<Message>> messageWindow,
|
||||||
String title});
|
String title});
|
||||||
|
|
||||||
|
|
@ -141,8 +160,10 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res>
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? chatKey = null,
|
Object? chatKey = null,
|
||||||
Object? scrollController = null,
|
Object? scrollController = null,
|
||||||
Object? localUser = null,
|
Object? localUser = freezed,
|
||||||
Object? remoteUsers = null,
|
Object? remoteUsers = null,
|
||||||
|
Object? historicalRemoteUsers = null,
|
||||||
|
Object? unknownUsers = null,
|
||||||
Object? messageWindow = null,
|
Object? messageWindow = null,
|
||||||
Object? title = null,
|
Object? title = null,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -155,14 +176,22 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res>
|
||||||
? _value.scrollController
|
? _value.scrollController
|
||||||
: scrollController // ignore: cast_nullable_to_non_nullable
|
: scrollController // ignore: cast_nullable_to_non_nullable
|
||||||
as AutoScrollController,
|
as AutoScrollController,
|
||||||
localUser: null == localUser
|
localUser: freezed == localUser
|
||||||
? _value.localUser
|
? _value.localUser
|
||||||
: localUser // ignore: cast_nullable_to_non_nullable
|
: localUser // ignore: cast_nullable_to_non_nullable
|
||||||
as User,
|
as User?,
|
||||||
remoteUsers: null == remoteUsers
|
remoteUsers: null == remoteUsers
|
||||||
? _value.remoteUsers
|
? _value.remoteUsers
|
||||||
: remoteUsers // ignore: cast_nullable_to_non_nullable
|
: remoteUsers // ignore: cast_nullable_to_non_nullable
|
||||||
as IMap<Typed<FixedEncodedString43>, User>,
|
as IMap<Typed<FixedEncodedString43>, User>,
|
||||||
|
historicalRemoteUsers: null == historicalRemoteUsers
|
||||||
|
? _value.historicalRemoteUsers
|
||||||
|
: historicalRemoteUsers // ignore: cast_nullable_to_non_nullable
|
||||||
|
as IMap<Typed<FixedEncodedString43>, User>,
|
||||||
|
unknownUsers: null == unknownUsers
|
||||||
|
? _value.unknownUsers
|
||||||
|
: unknownUsers // ignore: cast_nullable_to_non_nullable
|
||||||
|
as IMap<Typed<FixedEncodedString43>, User>,
|
||||||
messageWindow: null == messageWindow
|
messageWindow: null == messageWindow
|
||||||
? _value.messageWindow
|
? _value.messageWindow
|
||||||
: messageWindow // ignore: cast_nullable_to_non_nullable
|
: messageWindow // ignore: cast_nullable_to_non_nullable
|
||||||
|
|
@ -183,6 +212,8 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
|
||||||
required this.scrollController,
|
required this.scrollController,
|
||||||
required this.localUser,
|
required this.localUser,
|
||||||
required this.remoteUsers,
|
required this.remoteUsers,
|
||||||
|
required this.historicalRemoteUsers,
|
||||||
|
required this.unknownUsers,
|
||||||
required this.messageWindow,
|
required this.messageWindow,
|
||||||
required this.title});
|
required this.title});
|
||||||
|
|
||||||
|
|
@ -194,10 +225,16 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
|
||||||
final AutoScrollController scrollController;
|
final AutoScrollController scrollController;
|
||||||
// Local user
|
// Local user
|
||||||
@override
|
@override
|
||||||
final User localUser;
|
final User? localUser;
|
||||||
// Remote users
|
// Active remote users
|
||||||
@override
|
@override
|
||||||
final IMap<Typed<FixedEncodedString43>, User> remoteUsers;
|
final IMap<Typed<FixedEncodedString43>, User> remoteUsers;
|
||||||
|
// Historical remote users
|
||||||
|
@override
|
||||||
|
final IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers;
|
||||||
|
// Unknown users
|
||||||
|
@override
|
||||||
|
final IMap<Typed<FixedEncodedString43>, User> unknownUsers;
|
||||||
// Messages state
|
// Messages state
|
||||||
@override
|
@override
|
||||||
final AsyncValue<WindowState<Message>> messageWindow;
|
final AsyncValue<WindowState<Message>> messageWindow;
|
||||||
|
|
@ -207,7 +244,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, localUser: $localUser, remoteUsers: $remoteUsers, messageWindow: $messageWindow, title: $title)';
|
return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -222,14 +259,26 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
|
||||||
other.localUser == localUser) &&
|
other.localUser == localUser) &&
|
||||||
(identical(other.remoteUsers, remoteUsers) ||
|
(identical(other.remoteUsers, remoteUsers) ||
|
||||||
other.remoteUsers == remoteUsers) &&
|
other.remoteUsers == remoteUsers) &&
|
||||||
|
(identical(other.historicalRemoteUsers, historicalRemoteUsers) ||
|
||||||
|
other.historicalRemoteUsers == historicalRemoteUsers) &&
|
||||||
|
(identical(other.unknownUsers, unknownUsers) ||
|
||||||
|
other.unknownUsers == unknownUsers) &&
|
||||||
(identical(other.messageWindow, messageWindow) ||
|
(identical(other.messageWindow, messageWindow) ||
|
||||||
other.messageWindow == messageWindow) &&
|
other.messageWindow == messageWindow) &&
|
||||||
(identical(other.title, title) || other.title == title));
|
(identical(other.title, title) || other.title == title));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, chatKey, scrollController,
|
int get hashCode => Object.hash(
|
||||||
localUser, remoteUsers, messageWindow, title);
|
runtimeType,
|
||||||
|
chatKey,
|
||||||
|
scrollController,
|
||||||
|
localUser,
|
||||||
|
remoteUsers,
|
||||||
|
historicalRemoteUsers,
|
||||||
|
unknownUsers,
|
||||||
|
messageWindow,
|
||||||
|
title);
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
|
|
@ -243,8 +292,11 @@ abstract class _ChatComponentState implements ChatComponentState {
|
||||||
const factory _ChatComponentState(
|
const factory _ChatComponentState(
|
||||||
{required final GlobalKey<ChatState> chatKey,
|
{required final GlobalKey<ChatState> chatKey,
|
||||||
required final AutoScrollController scrollController,
|
required final AutoScrollController scrollController,
|
||||||
required final User localUser,
|
required final User? localUser,
|
||||||
required final IMap<Typed<FixedEncodedString43>, User> remoteUsers,
|
required final IMap<Typed<FixedEncodedString43>, User> remoteUsers,
|
||||||
|
required final IMap<Typed<FixedEncodedString43>, User>
|
||||||
|
historicalRemoteUsers,
|
||||||
|
required final IMap<Typed<FixedEncodedString43>, User> unknownUsers,
|
||||||
required final AsyncValue<WindowState<Message>> messageWindow,
|
required final AsyncValue<WindowState<Message>> messageWindow,
|
||||||
required final String title}) = _$ChatComponentStateImpl;
|
required final String title}) = _$ChatComponentStateImpl;
|
||||||
|
|
||||||
|
|
@ -253,9 +305,13 @@ abstract class _ChatComponentState implements ChatComponentState {
|
||||||
@override // ScrollController for the chat
|
@override // ScrollController for the chat
|
||||||
AutoScrollController get scrollController;
|
AutoScrollController get scrollController;
|
||||||
@override // Local user
|
@override // Local user
|
||||||
User get localUser;
|
User? get localUser;
|
||||||
@override // Remote users
|
@override // Active remote users
|
||||||
IMap<Typed<FixedEncodedString43>, User> get remoteUsers;
|
IMap<Typed<FixedEncodedString43>, User> get remoteUsers;
|
||||||
|
@override // Historical remote users
|
||||||
|
IMap<Typed<FixedEncodedString43>, User> get historicalRemoteUsers;
|
||||||
|
@override // Unknown users
|
||||||
|
IMap<Typed<FixedEncodedString43>, User> get unknownUsers;
|
||||||
@override // Messages state
|
@override // Messages state
|
||||||
AsyncValue<WindowState<Message>> get messageWindow;
|
AsyncValue<WindowState<Message>> get messageWindow;
|
||||||
@override // Title of the chat
|
@override // Title of the chat
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../account_manager/account_manager.dart';
|
import '../../account_manager/account_manager.dart';
|
||||||
import '../../chat_list/chat_list.dart';
|
import '../../contacts/contacts.dart';
|
||||||
|
import '../../conversation/conversation.dart';
|
||||||
import '../../theme/theme.dart';
|
import '../../theme/theme.dart';
|
||||||
import '../chat.dart';
|
import '../chat.dart';
|
||||||
|
|
||||||
|
|
@ -22,31 +23,29 @@ class ChatComponentWidget extends StatelessWidget {
|
||||||
static Widget builder(
|
static Widget builder(
|
||||||
{required TypedKey localConversationRecordKey, Key? key}) =>
|
{required TypedKey localConversationRecordKey, Key? key}) =>
|
||||||
Builder(builder: (context) {
|
Builder(builder: (context) {
|
||||||
// Get all watched dependendies
|
// Get the account info
|
||||||
final activeAccountInfo = context.watch<ActiveAccountInfo>();
|
final accountInfo = context.watch<AccountInfoCubit>().state;
|
||||||
final accountRecordInfo =
|
|
||||||
context.watch<AccountRecordCubit>().state.asData?.value;
|
|
||||||
if (accountRecordInfo == null) {
|
|
||||||
return debugPage('should always have an account record here');
|
|
||||||
}
|
|
||||||
|
|
||||||
final avconversation = context.select<ActiveConversationsBlocMapCubit,
|
// Get the account record cubit
|
||||||
AsyncValue<ActiveConversationState>?>(
|
final accountRecordCubit = context.read<AccountRecordCubit>();
|
||||||
(x) => x.state[localConversationRecordKey]);
|
|
||||||
if (avconversation == null) {
|
// Get the contact list cubit
|
||||||
|
final contactListCubit = context.watch<ContactListCubit>();
|
||||||
|
|
||||||
|
// Get the active conversation cubit
|
||||||
|
final activeConversationCubit = context
|
||||||
|
.select<ActiveConversationsBlocMapCubit, ActiveConversationCubit?>(
|
||||||
|
(x) => x.tryOperateSync(localConversationRecordKey,
|
||||||
|
closure: (cubit) => cubit));
|
||||||
|
if (activeConversationCubit == null) {
|
||||||
return waitingPage();
|
return waitingPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
final activeConversationState = avconversation.asData?.value;
|
|
||||||
if (activeConversationState == null) {
|
|
||||||
return avconversation.buildNotData();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the messages cubit
|
// Get the messages cubit
|
||||||
final messagesCubit = context.select<
|
final messagesCubit = context.select<
|
||||||
ActiveSingleContactChatBlocMapCubit,
|
ActiveSingleContactChatBlocMapCubit,
|
||||||
SingleContactMessagesCubit?>(
|
SingleContactMessagesCubit?>(
|
||||||
(x) => x.tryOperate(localConversationRecordKey,
|
(x) => x.tryOperateSync(localConversationRecordKey,
|
||||||
closure: (cubit) => cubit));
|
closure: (cubit) => cubit));
|
||||||
if (messagesCubit == null) {
|
if (messagesCubit == null) {
|
||||||
return waitingPage();
|
return waitingPage();
|
||||||
|
|
@ -54,10 +53,12 @@ class ChatComponentWidget extends StatelessWidget {
|
||||||
|
|
||||||
// Make chat component state
|
// Make chat component state
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
|
key: key,
|
||||||
create: (context) => ChatComponentCubit.singleContact(
|
create: (context) => ChatComponentCubit.singleContact(
|
||||||
activeAccountInfo: activeAccountInfo,
|
accountInfo: accountInfo,
|
||||||
accountRecordInfo: accountRecordInfo,
|
accountRecordCubit: accountRecordCubit,
|
||||||
activeConversationState: activeConversationState,
|
contactListCubit: contactListCubit,
|
||||||
|
activeConversationCubit: activeConversationCubit,
|
||||||
messagesCubit: messagesCubit,
|
messagesCubit: messagesCubit,
|
||||||
),
|
),
|
||||||
child: ChatComponentWidget._(key: key));
|
child: ChatComponentWidget._(key: key));
|
||||||
|
|
@ -159,6 +160,11 @@ class ChatComponentWidget extends StatelessWidget {
|
||||||
final chatComponentCubit = context.watch<ChatComponentCubit>();
|
final chatComponentCubit = context.watch<ChatComponentCubit>();
|
||||||
final chatComponentState = chatComponentCubit.state;
|
final chatComponentState = chatComponentCubit.state;
|
||||||
|
|
||||||
|
final localUser = chatComponentState.localUser;
|
||||||
|
if (localUser == null) {
|
||||||
|
return waitingPage();
|
||||||
|
}
|
||||||
|
|
||||||
final messageWindow = chatComponentState.messageWindow.asData?.value;
|
final messageWindow = chatComponentState.messageWindow.asData?.value;
|
||||||
if (messageWindow == null) {
|
if (messageWindow == null) {
|
||||||
return chatComponentState.messageWindow.buildNotData();
|
return chatComponentState.messageWindow.buildNotData();
|
||||||
|
|
@ -281,7 +287,7 @@ class ChatComponentWidget extends StatelessWidget {
|
||||||
_handleSendPressed(chatComponentCubit, pt),
|
_handleSendPressed(chatComponentCubit, pt),
|
||||||
//showUserAvatars: false,
|
//showUserAvatars: false,
|
||||||
//showUserNames: true,
|
//showUserNames: true,
|
||||||
user: chatComponentState.localUser,
|
user: localUser,
|
||||||
emptyState: const EmptyChatWidget())),
|
emptyState: const EmptyChatWidget())),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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 '../../account_manager/account_manager.dart';
|
||||||
import '../../chat/chat.dart';
|
import '../../chat/chat.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../tools/tools.dart';
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
@ -19,33 +18,30 @@ typedef ChatListCubitState = DHTShortArrayBusyState<proto.Chat>;
|
||||||
class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
|
class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
|
||||||
with StateMapFollowable<ChatListCubitState, TypedKey, proto.Chat> {
|
with StateMapFollowable<ChatListCubitState, TypedKey, proto.Chat> {
|
||||||
ChatListCubit({
|
ChatListCubit({
|
||||||
required ActiveAccountInfo activeAccountInfo,
|
required AccountInfo accountInfo,
|
||||||
required proto.Account account,
|
required OwnedDHTRecordPointer chatListRecordPointer,
|
||||||
required this.activeChatCubit,
|
required ActiveChatCubit activeChatCubit,
|
||||||
}) : super(
|
}) : _activeChatCubit = activeChatCubit,
|
||||||
open: () => _open(activeAccountInfo, account),
|
super(
|
||||||
|
open: () => _open(accountInfo, chatListRecordPointer),
|
||||||
decodeElement: proto.Chat.fromBuffer);
|
decodeElement: proto.Chat.fromBuffer);
|
||||||
|
|
||||||
static Future<DHTShortArray> _open(
|
static Future<DHTShortArray> _open(AccountInfo accountInfo,
|
||||||
ActiveAccountInfo activeAccountInfo, proto.Account account) async {
|
OwnedDHTRecordPointer chatListRecordPointer) async {
|
||||||
final accountRecordKey =
|
final dhtRecord = await DHTShortArray.openOwned(chatListRecordPointer,
|
||||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
debugName: 'ChatListCubit::_open::ChatList',
|
||||||
|
parent: accountInfo.accountRecordKey);
|
||||||
final chatListRecordKey = account.chatList.toVeilid();
|
|
||||||
|
|
||||||
final dhtRecord = await DHTShortArray.openOwned(chatListRecordKey,
|
|
||||||
debugName: 'ChatListCubit::_open::ChatList', parent: accountRecordKey);
|
|
||||||
|
|
||||||
return dhtRecord;
|
return dhtRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<proto.ChatSettings> getDefaultChatSettings(
|
Future<proto.ChatSettings> getDefaultChatSettings(
|
||||||
proto.Contact contact) async {
|
proto.Contact contact) async {
|
||||||
final pronouns = contact.editedProfile.pronouns.isEmpty
|
final pronouns = contact.profile.pronouns.isEmpty
|
||||||
? ''
|
? ''
|
||||||
: ' (${contact.editedProfile.pronouns})';
|
: ' [${contact.profile.pronouns}])';
|
||||||
return proto.ChatSettings()
|
return proto.ChatSettings()
|
||||||
..title = '${contact.editedProfile.name}$pronouns'
|
..title = '${contact.displayName}$pronouns'
|
||||||
..description = ''
|
..description = ''
|
||||||
..defaultExpiration = Int64.ZERO;
|
..defaultExpiration = Int64.ZERO;
|
||||||
}
|
}
|
||||||
|
|
@ -57,12 +53,24 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
|
||||||
// Make local copy so we don't share the buffer
|
// Make local copy so we don't share the buffer
|
||||||
final localConversationRecordKey =
|
final localConversationRecordKey =
|
||||||
contact.localConversationRecordKey.toVeilid();
|
contact.localConversationRecordKey.toVeilid();
|
||||||
|
final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
|
||||||
final remoteConversationRecordKey =
|
final remoteConversationRecordKey =
|
||||||
contact.remoteConversationRecordKey.toVeilid();
|
contact.remoteConversationRecordKey.toVeilid();
|
||||||
|
|
||||||
|
// Create 1:1 conversation type Chat
|
||||||
|
final chatMember = proto.ChatMember()
|
||||||
|
..remoteIdentityPublicKey = remoteIdentityPublicKey.toProto()
|
||||||
|
..remoteConversationRecordKey = remoteConversationRecordKey.toProto();
|
||||||
|
|
||||||
|
final directChat = proto.DirectChat()
|
||||||
|
..settings = await getDefaultChatSettings(contact)
|
||||||
|
..localConversationRecordKey = localConversationRecordKey.toProto()
|
||||||
|
..remoteMember = chatMember;
|
||||||
|
|
||||||
|
final chat = proto.Chat()..direct = directChat;
|
||||||
|
|
||||||
// Add Chat to account's list
|
// Add Chat to account's list
|
||||||
// if this fails, don't keep retrying, user can try again later
|
await operateWriteEventual((writer) async {
|
||||||
await operateWrite((writer) async {
|
|
||||||
// See if we have added this chat already
|
// See if we have added this chat already
|
||||||
for (var i = 0; i < writer.length; i++) {
|
for (var i = 0; i < writer.length; i++) {
|
||||||
final cbuf = await writer.get(i);
|
final cbuf = await writer.get(i);
|
||||||
|
|
@ -70,19 +78,27 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
|
||||||
throw Exception('Failed to get chat');
|
throw Exception('Failed to get chat');
|
||||||
}
|
}
|
||||||
final c = proto.Chat.fromBuffer(cbuf);
|
final c = proto.Chat.fromBuffer(cbuf);
|
||||||
if (c.localConversationRecordKey ==
|
|
||||||
contact.localConversationRecordKey) {
|
switch (c.whichKind()) {
|
||||||
// Nothing to do here
|
case proto.Chat_Kind.direct:
|
||||||
return;
|
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
|
// Add chat
|
||||||
await writer.add(chat.writeToBuffer());
|
await writer.add(chat.writeToBuffer());
|
||||||
});
|
});
|
||||||
|
|
@ -91,41 +107,23 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
|
||||||
/// Delete a chat
|
/// Delete a chat
|
||||||
Future<void> deleteChat(
|
Future<void> deleteChat(
|
||||||
{required TypedKey localConversationRecordKey}) async {
|
{required TypedKey localConversationRecordKey}) async {
|
||||||
final localConversationRecordKeyProto =
|
|
||||||
localConversationRecordKey.toProto();
|
|
||||||
|
|
||||||
// Remove Chat from account's list
|
// Remove Chat from account's list
|
||||||
// if this fails, don't keep retrying, user can try again later
|
await operateWriteEventual((writer) async {
|
||||||
final deletedItem =
|
if (_activeChatCubit.state == localConversationRecordKey) {
|
||||||
// Ensure followers get their changes before we return
|
_activeChatCubit.setActiveChat(null);
|
||||||
await syncFollowers(() => operateWrite((writer) async {
|
|
||||||
if (activeChatCubit.state == localConversationRecordKey) {
|
|
||||||
activeChatCubit.setActiveChat(null);
|
|
||||||
}
|
|
||||||
for (var i = 0; i < writer.length; i++) {
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
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 /////////////////////////
|
/// StateMapFollowable /////////////////////////
|
||||||
|
|
@ -136,9 +134,11 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
|
||||||
return IMap();
|
return IMap();
|
||||||
}
|
}
|
||||||
return IMap.fromIterable(stateValue,
|
return IMap.fromIterable(stateValue,
|
||||||
keyMapper: (e) => e.value.localConversationRecordKey.toVeilid(),
|
keyMapper: (e) => e.value.localConversationRecordKey,
|
||||||
valueMapper: (e) => e.value);
|
valueMapper: (e) => e.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
final ActiveChatCubit activeChatCubit;
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
final ActiveChatCubit _activeChatCubit;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1 @@
|
||||||
export 'active_single_contact_chat_bloc_map_cubit.dart';
|
|
||||||
export 'active_conversations_bloc_map_cubit.dart';
|
|
||||||
export 'chat_list_cubit.dart';
|
export 'chat_list_cubit.dart';
|
||||||
|
|
|
||||||
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();
|
_contact.localConversationRecordKey.toVeilid();
|
||||||
final selected = activeChatCubit.state == localConversationRecordKey;
|
final selected = activeChatCubit.state == localConversationRecordKey;
|
||||||
|
|
||||||
|
late final String title;
|
||||||
|
late final String subtitle;
|
||||||
|
if (_contact.nickname.isNotEmpty) {
|
||||||
|
title = _contact.nickname;
|
||||||
|
if (_contact.profile.pronouns.isNotEmpty) {
|
||||||
|
subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})';
|
||||||
|
} else {
|
||||||
|
subtitle = _contact.profile.name;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
title = _contact.profile.name;
|
||||||
|
if (_contact.profile.pronouns.isNotEmpty) {
|
||||||
|
subtitle = '(${_contact.profile.pronouns})';
|
||||||
|
} else {
|
||||||
|
subtitle = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return SliderTile(
|
return SliderTile(
|
||||||
key: ObjectKey(_contact),
|
key: ObjectKey(_contact),
|
||||||
disabled: _disabled,
|
disabled: _disabled,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
tileScale: ScaleKind.secondary,
|
tileScale: ScaleKind.secondary,
|
||||||
title: _contact.editedProfile.name,
|
title: title,
|
||||||
subtitle: _contact.editedProfile.pronouns,
|
subtitle: subtitle,
|
||||||
icon: Icons.chat,
|
icon: Icons.chat,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
singleFuture(activeChatCubit, () async {
|
singleFuture(activeChatCubit, () async {
|
||||||
|
|
|
||||||
|
|
@ -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_item_widget.dart';
|
||||||
export 'chat_single_contact_list_widget.dart';
|
|
||||||
export 'empty_chat_list_widget.dart';
|
export 'empty_chat_list_widget.dart';
|
||||||
|
|
|
||||||
|
|
@ -36,22 +36,16 @@ class ContactInvitationListCubit
|
||||||
StateMapFollowable<ContactInvitiationListState, TypedKey,
|
StateMapFollowable<ContactInvitiationListState, TypedKey,
|
||||||
proto.ContactInvitationRecord> {
|
proto.ContactInvitationRecord> {
|
||||||
ContactInvitationListCubit({
|
ContactInvitationListCubit({
|
||||||
required ActiveAccountInfo activeAccountInfo,
|
required AccountInfo accountInfo,
|
||||||
required proto.Account account,
|
required OwnedDHTRecordPointer contactInvitationListRecordPointer,
|
||||||
}) : _activeAccountInfo = activeAccountInfo,
|
}) : _accountInfo = accountInfo,
|
||||||
_account = account,
|
|
||||||
super(
|
super(
|
||||||
open: () => _open(activeAccountInfo, account),
|
open: () => _open(accountInfo.accountRecordKey,
|
||||||
|
contactInvitationListRecordPointer),
|
||||||
decodeElement: proto.ContactInvitationRecord.fromBuffer);
|
decodeElement: proto.ContactInvitationRecord.fromBuffer);
|
||||||
|
|
||||||
static Future<DHTShortArray> _open(
|
static Future<DHTShortArray> _open(TypedKey accountRecordKey,
|
||||||
ActiveAccountInfo activeAccountInfo, proto.Account account) async {
|
OwnedDHTRecordPointer contactInvitationListRecordPointer) async {
|
||||||
final accountRecordKey =
|
|
||||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
|
||||||
|
|
||||||
final contactInvitationListRecordPointer =
|
|
||||||
account.contactInvitationRecords.toVeilid();
|
|
||||||
|
|
||||||
final dhtRecord = await DHTShortArray.openOwned(
|
final dhtRecord = await DHTShortArray.openOwned(
|
||||||
contactInvitationListRecordPointer,
|
contactInvitationListRecordPointer,
|
||||||
debugName: 'ContactInvitationListCubit::_open::ContactInvitationList',
|
debugName: 'ContactInvitationListCubit::_open::ContactInvitationList',
|
||||||
|
|
@ -61,7 +55,8 @@ class ContactInvitationListCubit
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> createInvitation(
|
Future<Uint8List> createInvitation(
|
||||||
{required EncryptionKeyType encryptionKeyType,
|
{required proto.Profile profile,
|
||||||
|
required EncryptionKeyType encryptionKeyType,
|
||||||
required String encryptionKey,
|
required String encryptionKey,
|
||||||
required String message,
|
required String message,
|
||||||
required Timestamp? expiration}) async {
|
required Timestamp? expiration}) async {
|
||||||
|
|
@ -71,8 +66,8 @@ class ContactInvitationListCubit
|
||||||
final crcs = await pool.veilid.bestCryptoSystem();
|
final crcs = await pool.veilid.bestCryptoSystem();
|
||||||
final contactRequestWriter = await crcs.generateKeyPair();
|
final contactRequestWriter = await crcs.generateKeyPair();
|
||||||
|
|
||||||
final idcs = await _activeAccountInfo.identityCryptoSystem;
|
final idcs = await _accountInfo.identityCryptoSystem;
|
||||||
final identityWriter = _activeAccountInfo.identityWriter;
|
final identityWriter = _accountInfo.identityWriter;
|
||||||
|
|
||||||
// Encrypt the writer secret with the encryption key
|
// Encrypt the writer secret with the encryption key
|
||||||
final encryptedSecret = await encryptionKeyType.encryptSecretToBytes(
|
final encryptedSecret = await encryptionKeyType.encryptSecretToBytes(
|
||||||
|
|
@ -90,7 +85,7 @@ class ContactInvitationListCubit
|
||||||
await (await pool.createRecord(
|
await (await pool.createRecord(
|
||||||
debugName: 'ContactInvitationListCubit::createInvitation::'
|
debugName: 'ContactInvitationListCubit::createInvitation::'
|
||||||
'LocalConversation',
|
'LocalConversation',
|
||||||
parent: _activeAccountInfo.accountRecordKey,
|
parent: _accountInfo.accountRecordKey,
|
||||||
schema: DHTSchema.smpl(
|
schema: DHTSchema.smpl(
|
||||||
oCnt: 0,
|
oCnt: 0,
|
||||||
members: [DHTSchemaMember(mKey: identityWriter.key, mCnt: 1)])))
|
members: [DHTSchemaMember(mKey: identityWriter.key, mCnt: 1)])))
|
||||||
|
|
@ -99,9 +94,8 @@ class ContactInvitationListCubit
|
||||||
// Make ContactRequestPrivate and encrypt with the writer secret
|
// Make ContactRequestPrivate and encrypt with the writer secret
|
||||||
final crpriv = proto.ContactRequestPrivate()
|
final crpriv = proto.ContactRequestPrivate()
|
||||||
..writerKey = contactRequestWriter.key.toProto()
|
..writerKey = contactRequestWriter.key.toProto()
|
||||||
..profile = _account.profile
|
..profile = profile
|
||||||
..superIdentityRecordKey =
|
..superIdentityRecordKey = _accountInfo.superIdentityRecordKey.toProto()
|
||||||
_activeAccountInfo.userLogin.superIdentityRecordKey.toProto()
|
|
||||||
..chatRecordKey = localConversation.key.toProto()
|
..chatRecordKey = localConversation.key.toProto()
|
||||||
..expiration = expiration?.toInt64() ?? Int64.ZERO;
|
..expiration = expiration?.toInt64() ?? Int64.ZERO;
|
||||||
final crprivbytes = crpriv.writeToBuffer();
|
final crprivbytes = crpriv.writeToBuffer();
|
||||||
|
|
@ -119,7 +113,7 @@ class ContactInvitationListCubit
|
||||||
await (await pool.createRecord(
|
await (await pool.createRecord(
|
||||||
debugName: 'ContactInvitationListCubit::createInvitation::'
|
debugName: 'ContactInvitationListCubit::createInvitation::'
|
||||||
'ContactRequestInbox',
|
'ContactRequestInbox',
|
||||||
parent: _activeAccountInfo.accountRecordKey,
|
parent: _accountInfo.accountRecordKey,
|
||||||
schema: DHTSchema.smpl(oCnt: 1, members: [
|
schema: DHTSchema.smpl(oCnt: 1, members: [
|
||||||
DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key)
|
DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key)
|
||||||
]),
|
]),
|
||||||
|
|
@ -158,8 +152,7 @@ class ContactInvitationListCubit
|
||||||
..message = message;
|
..message = message;
|
||||||
|
|
||||||
// Add ContactInvitationRecord to account's list
|
// Add ContactInvitationRecord to account's list
|
||||||
// if this fails, don't keep retrying, user can try again later
|
await operateWriteEventual((writer) async {
|
||||||
await operateWrite((writer) async {
|
|
||||||
await writer.add(cinvrec.writeToBuffer());
|
await writer.add(cinvrec.writeToBuffer());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -172,8 +165,6 @@ class ContactInvitationListCubit
|
||||||
{required bool accepted,
|
{required bool accepted,
|
||||||
required TypedKey contactRequestInboxRecordKey}) async {
|
required TypedKey contactRequestInboxRecordKey}) async {
|
||||||
final pool = DHTRecordPool.instance;
|
final pool = DHTRecordPool.instance;
|
||||||
final accountRecordKey =
|
|
||||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
|
||||||
|
|
||||||
// Remove ContactInvitationRecord from account's list
|
// Remove ContactInvitationRecord from account's list
|
||||||
final deletedItem = await operateWrite((writer) async {
|
final deletedItem = await operateWrite((writer) async {
|
||||||
|
|
@ -198,7 +189,7 @@ class ContactInvitationListCubit
|
||||||
await (await pool.openRecordOwned(contactRequestInbox,
|
await (await pool.openRecordOwned(contactRequestInbox,
|
||||||
debugName: 'ContactInvitationListCubit::deleteInvitation::'
|
debugName: 'ContactInvitationListCubit::deleteInvitation::'
|
||||||
'ContactRequestInbox',
|
'ContactRequestInbox',
|
||||||
parent: accountRecordKey))
|
parent: _accountInfo.accountRecordKey))
|
||||||
.scope((contactRequestInbox) async {
|
.scope((contactRequestInbox) async {
|
||||||
// Wipe out old invitation so it shows up as invalid
|
// Wipe out old invitation so it shows up as invalid
|
||||||
await contactRequestInbox.tryWriteBytes(Uint8List(0));
|
await contactRequestInbox.tryWriteBytes(Uint8List(0));
|
||||||
|
|
@ -240,7 +231,12 @@ class ContactInvitationListCubit
|
||||||
// inbox with our list of extant invitations
|
// inbox with our list of extant invitations
|
||||||
// If we're chatting to ourselves,
|
// If we're chatting to ourselves,
|
||||||
// we are validating an invitation we have created
|
// we are validating an invitation we have created
|
||||||
final isSelf = state.state.asData!.value.indexWhere((cir) =>
|
final contactInvitationList = state.state.asData?.value;
|
||||||
|
if (contactInvitationList == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final isSelf = contactInvitationList.indexWhere((cir) =>
|
||||||
cir.value.contactRequestInbox.recordKey.toVeilid() ==
|
cir.value.contactRequestInbox.recordKey.toVeilid() ==
|
||||||
contactRequestInboxKey) !=
|
contactRequestInboxKey) !=
|
||||||
-1;
|
-1;
|
||||||
|
|
@ -248,7 +244,8 @@ class ContactInvitationListCubit
|
||||||
await (await pool.openRecordRead(contactRequestInboxKey,
|
await (await pool.openRecordRead(contactRequestInboxKey,
|
||||||
debugName: 'ContactInvitationListCubit::validateInvitation::'
|
debugName: 'ContactInvitationListCubit::validateInvitation::'
|
||||||
'ContactRequestInbox',
|
'ContactRequestInbox',
|
||||||
parent: _activeAccountInfo.accountRecordKey))
|
parent: pool.getParentRecordKey(contactRequestInboxKey) ??
|
||||||
|
_accountInfo.accountRecordKey))
|
||||||
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
||||||
//
|
//
|
||||||
final contactRequest = await contactRequestInbox
|
final contactRequest = await contactRequestInbox
|
||||||
|
|
@ -293,8 +290,7 @@ class ContactInvitationListCubit
|
||||||
secret: writerSecret);
|
secret: writerSecret);
|
||||||
|
|
||||||
out = ValidContactInvitation(
|
out = ValidContactInvitation(
|
||||||
activeAccountInfo: _activeAccountInfo,
|
accountInfo: _accountInfo,
|
||||||
account: _account,
|
|
||||||
contactRequestInboxKey: contactRequestInboxKey,
|
contactRequestInboxKey: contactRequestInboxKey,
|
||||||
contactRequestPrivate: contactRequestPrivate,
|
contactRequestPrivate: contactRequestPrivate,
|
||||||
contactSuperIdentity: contactSuperIdentity,
|
contactSuperIdentity: contactSuperIdentity,
|
||||||
|
|
@ -318,6 +314,5 @@ class ContactInvitationListCubit
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
final ActiveAccountInfo _activeAccountInfo;
|
final AccountInfo _accountInfo;
|
||||||
final proto.Account _account;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,27 +7,22 @@ import '../../proto/proto.dart' as proto;
|
||||||
class ContactRequestInboxCubit
|
class ContactRequestInboxCubit
|
||||||
extends DefaultDHTRecordCubit<proto.SignedContactResponse?> {
|
extends DefaultDHTRecordCubit<proto.SignedContactResponse?> {
|
||||||
ContactRequestInboxCubit(
|
ContactRequestInboxCubit(
|
||||||
{required this.activeAccountInfo, required this.contactInvitationRecord})
|
{required AccountInfo accountInfo, required this.contactInvitationRecord})
|
||||||
: super(
|
: super(
|
||||||
open: () => _open(
|
open: () => _open(
|
||||||
activeAccountInfo: activeAccountInfo,
|
accountInfo: accountInfo,
|
||||||
contactInvitationRecord: contactInvitationRecord),
|
contactInvitationRecord: contactInvitationRecord),
|
||||||
decodeState: (buf) => buf.isEmpty
|
decodeState: (buf) => buf.isEmpty
|
||||||
? null
|
? null
|
||||||
: proto.SignedContactResponse.fromBuffer(buf));
|
: proto.SignedContactResponse.fromBuffer(buf));
|
||||||
|
|
||||||
// ContactRequestInboxCubit.value(
|
|
||||||
// {required super.record,
|
|
||||||
// required this.activeAccountInfo,
|
|
||||||
// required this.contactInvitationRecord})
|
|
||||||
// : super.value(decodeState: proto.SignedContactResponse.fromBuffer);
|
|
||||||
|
|
||||||
static Future<DHTRecord> _open(
|
static Future<DHTRecord> _open(
|
||||||
{required ActiveAccountInfo activeAccountInfo,
|
{required AccountInfo accountInfo,
|
||||||
required proto.ContactInvitationRecord contactInvitationRecord}) async {
|
required proto.ContactInvitationRecord contactInvitationRecord}) async {
|
||||||
final pool = DHTRecordPool.instance;
|
final pool = DHTRecordPool.instance;
|
||||||
final accountRecordKey =
|
|
||||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
final accountRecordKey = accountInfo.accountRecordKey;
|
||||||
|
|
||||||
final writerSecret = contactInvitationRecord.writerSecret.toVeilid();
|
final writerSecret = contactInvitationRecord.writerSecret.toVeilid();
|
||||||
final recordKey =
|
final recordKey =
|
||||||
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid();
|
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid();
|
||||||
|
|
@ -42,6 +37,5 @@ class ContactRequestInboxCubit
|
||||||
defaultSubkey: 1);
|
defaultSubkey: 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
final ActiveAccountInfo activeAccountInfo;
|
|
||||||
final proto.ContactInvitationRecord contactInvitationRecord;
|
final proto.ContactInvitationRecord contactInvitationRecord;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import 'package:meta/meta.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../account_manager/account_manager.dart';
|
import '../../account_manager/account_manager.dart';
|
||||||
import '../../contacts/contacts.dart';
|
import '../../conversation/conversation.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
import '../models/accepted_contact.dart';
|
import '../models/accepted_contact.dart';
|
||||||
|
|
@ -24,25 +24,27 @@ class InvitationStatus extends Equatable {
|
||||||
|
|
||||||
class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
|
class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
|
||||||
proto.SignedContactResponse?> {
|
proto.SignedContactResponse?> {
|
||||||
WaitingInvitationCubit(ContactRequestInboxCubit super.input,
|
WaitingInvitationCubit(
|
||||||
{required ActiveAccountInfo activeAccountInfo,
|
ContactRequestInboxCubit super.input, {
|
||||||
required proto.Account account,
|
required AccountInfo accountInfo,
|
||||||
required proto.ContactInvitationRecord contactInvitationRecord})
|
required AccountRecordCubit accountRecordCubit,
|
||||||
: super(
|
required proto.ContactInvitationRecord contactInvitationRecord,
|
||||||
|
}) : super(
|
||||||
transform: (signedContactResponse) => _transform(
|
transform: (signedContactResponse) => _transform(
|
||||||
signedContactResponse,
|
signedContactResponse,
|
||||||
activeAccountInfo: activeAccountInfo,
|
accountInfo: accountInfo,
|
||||||
account: account,
|
accountRecordCubit: accountRecordCubit,
|
||||||
contactInvitationRecord: contactInvitationRecord));
|
contactInvitationRecord: contactInvitationRecord));
|
||||||
|
|
||||||
static Future<AsyncValue<InvitationStatus>> _transform(
|
static Future<AsyncValue<InvitationStatus>> _transform(
|
||||||
proto.SignedContactResponse? signedContactResponse,
|
proto.SignedContactResponse? signedContactResponse,
|
||||||
{required ActiveAccountInfo activeAccountInfo,
|
{required AccountInfo accountInfo,
|
||||||
required proto.Account account,
|
required AccountRecordCubit accountRecordCubit,
|
||||||
required proto.ContactInvitationRecord contactInvitationRecord}) async {
|
required proto.ContactInvitationRecord contactInvitationRecord}) async {
|
||||||
if (signedContactResponse == null) {
|
if (signedContactResponse == null) {
|
||||||
return const AsyncValue.loading();
|
return const AsyncValue.loading();
|
||||||
}
|
}
|
||||||
|
|
||||||
final contactResponseBytes =
|
final contactResponseBytes =
|
||||||
Uint8List.fromList(signedContactResponse.contactResponse);
|
Uint8List.fromList(signedContactResponse.contactResponse);
|
||||||
final contactResponse =
|
final contactResponse =
|
||||||
|
|
@ -57,8 +59,11 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
|
||||||
// Verify
|
// Verify
|
||||||
final idcs = await contactSuperIdentity.currentInstance.cryptoSystem;
|
final idcs = await contactSuperIdentity.currentInstance.cryptoSystem;
|
||||||
final signature = signedContactResponse.identitySignature.toVeilid();
|
final signature = signedContactResponse.identitySignature.toVeilid();
|
||||||
await idcs.verify(contactSuperIdentity.currentInstance.publicKey,
|
if (!await idcs.verify(contactSuperIdentity.currentInstance.publicKey,
|
||||||
contactResponseBytes, signature);
|
contactResponseBytes, signature)) {
|
||||||
|
// Could not verify signature of contact response
|
||||||
|
return AsyncValue.error('Invalid signature on contact response.');
|
||||||
|
}
|
||||||
|
|
||||||
// Check for rejection
|
// Check for rejection
|
||||||
if (!contactResponse.accept) {
|
if (!contactResponse.accept) {
|
||||||
|
|
@ -71,7 +76,7 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
|
||||||
contactResponse.remoteConversationRecordKey.toVeilid();
|
contactResponse.remoteConversationRecordKey.toVeilid();
|
||||||
|
|
||||||
final conversation = ConversationCubit(
|
final conversation = ConversationCubit(
|
||||||
activeAccountInfo: activeAccountInfo,
|
accountInfo: accountInfo,
|
||||||
remoteIdentityPublicKey:
|
remoteIdentityPublicKey:
|
||||||
contactSuperIdentity.currentInstance.typedPublicKey,
|
contactSuperIdentity.currentInstance.typedPublicKey,
|
||||||
remoteConversationRecordKey: remoteConversationRecordKey);
|
remoteConversationRecordKey: remoteConversationRecordKey);
|
||||||
|
|
@ -98,16 +103,13 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
|
||||||
final localConversationRecordKey =
|
final localConversationRecordKey =
|
||||||
contactInvitationRecord.localConversationRecordKey.toVeilid();
|
contactInvitationRecord.localConversationRecordKey.toVeilid();
|
||||||
return conversation.initLocalConversation(
|
return conversation.initLocalConversation(
|
||||||
|
profile: accountRecordCubit.state.asData!.value.profile,
|
||||||
existingConversationRecordKey: localConversationRecordKey,
|
existingConversationRecordKey: localConversationRecordKey,
|
||||||
profile: account.profile,
|
callback: (localConversation) async => AsyncValue.data(InvitationStatus(
|
||||||
// ignore: prefer_expression_function_bodies
|
acceptedContact: AcceptedContact(
|
||||||
callback: (localConversation) async {
|
remoteProfile: remoteProfile,
|
||||||
return AsyncValue.data(InvitationStatus(
|
remoteIdentity: contactSuperIdentity,
|
||||||
acceptedContact: AcceptedContact(
|
remoteConversationRecordKey: remoteConversationRecordKey,
|
||||||
remoteProfile: remoteProfile,
|
localConversationRecordKey: localConversationRecordKey))));
|
||||||
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 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../account_manager/account_manager.dart';
|
import '../../account_manager/account_manager.dart';
|
||||||
|
import '../../contacts/contacts.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import 'cubits.dart';
|
import 'cubits.dart';
|
||||||
|
|
||||||
|
|
@ -18,7 +19,27 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||||
StateMapFollower<DHTShortArrayBusyState<proto.ContactInvitationRecord>,
|
StateMapFollower<DHTShortArrayBusyState<proto.ContactInvitationRecord>,
|
||||||
TypedKey, proto.ContactInvitationRecord> {
|
TypedKey, proto.ContactInvitationRecord> {
|
||||||
WaitingInvitationsBlocMapCubit(
|
WaitingInvitationsBlocMapCubit(
|
||||||
{required this.activeAccountInfo, required this.account});
|
{required AccountInfo accountInfo,
|
||||||
|
required AccountRecordCubit accountRecordCubit,
|
||||||
|
required ContactInvitationListCubit contactInvitationListCubit,
|
||||||
|
required ContactListCubit contactListCubit})
|
||||||
|
: _accountInfo = accountInfo,
|
||||||
|
_accountRecordCubit = accountRecordCubit,
|
||||||
|
_contactInvitationListCubit = contactInvitationListCubit,
|
||||||
|
_contactListCubit = contactListCubit {
|
||||||
|
// React to invitation status changes
|
||||||
|
_singleInvitationStatusProcessor.follow(
|
||||||
|
stream, state, _invitationStatusListener);
|
||||||
|
|
||||||
|
// Follow the contact invitation list cubit
|
||||||
|
follow(contactInvitationListCubit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _singleInvitationStatusProcessor.unfollow();
|
||||||
|
await super.close();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _addWaitingInvitation(
|
Future<void> _addWaitingInvitation(
|
||||||
{required proto.ContactInvitationRecord
|
{required proto.ContactInvitationRecord
|
||||||
|
|
@ -27,22 +48,66 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||||
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(),
|
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(),
|
||||||
WaitingInvitationCubit(
|
WaitingInvitationCubit(
|
||||||
ContactRequestInboxCubit(
|
ContactRequestInboxCubit(
|
||||||
activeAccountInfo: activeAccountInfo,
|
accountInfo: _accountInfo,
|
||||||
contactInvitationRecord: contactInvitationRecord),
|
contactInvitationRecord: contactInvitationRecord),
|
||||||
activeAccountInfo: activeAccountInfo,
|
accountInfo: _accountInfo,
|
||||||
account: account,
|
accountRecordCubit: _accountRecordCubit,
|
||||||
contactInvitationRecord: contactInvitationRecord)));
|
contactInvitationRecord: contactInvitationRecord)));
|
||||||
|
|
||||||
|
// Process all accepted or rejected invitations
|
||||||
|
Future<void> _invitationStatusListener(
|
||||||
|
WaitingInvitationsBlocMapState newState) async {
|
||||||
|
for (final entry in newState.entries) {
|
||||||
|
final contactRequestInboxRecordKey = entry.key;
|
||||||
|
final invStatus = entry.value.asData?.value;
|
||||||
|
// Skip invitations that have not yet been accepted or rejected
|
||||||
|
if (invStatus == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete invitation and process the accepted or rejected contact
|
||||||
|
final acceptedContact = invStatus.acceptedContact;
|
||||||
|
if (acceptedContact != null) {
|
||||||
|
await _contactInvitationListCubit.deleteInvitation(
|
||||||
|
accepted: true,
|
||||||
|
contactRequestInboxRecordKey: contactRequestInboxRecordKey);
|
||||||
|
|
||||||
|
// Accept
|
||||||
|
await _contactListCubit.createContact(
|
||||||
|
profile: acceptedContact.remoteProfile,
|
||||||
|
remoteSuperIdentity: acceptedContact.remoteIdentity,
|
||||||
|
remoteConversationRecordKey:
|
||||||
|
acceptedContact.remoteConversationRecordKey,
|
||||||
|
localConversationRecordKey:
|
||||||
|
acceptedContact.localConversationRecordKey,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Reject
|
||||||
|
await _contactInvitationListCubit.deleteInvitation(
|
||||||
|
accepted: false,
|
||||||
|
contactRequestInboxRecordKey: contactRequestInboxRecordKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// StateFollower /////////////////////////
|
/// StateFollower /////////////////////////
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> removeFromState(TypedKey key) => remove(key);
|
Future<void> removeFromState(TypedKey key) => remove(key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateState(TypedKey key, proto.ContactInvitationRecord value) =>
|
Future<void> updateState(
|
||||||
_addWaitingInvitation(contactInvitationRecord: value);
|
TypedKey key,
|
||||||
|
proto.ContactInvitationRecord? oldValue,
|
||||||
|
proto.ContactInvitationRecord newValue) async {
|
||||||
|
await _addWaitingInvitation(contactInvitationRecord: newValue);
|
||||||
|
}
|
||||||
|
|
||||||
////
|
////
|
||||||
final ActiveAccountInfo activeAccountInfo;
|
final AccountInfo _accountInfo;
|
||||||
final proto.Account account;
|
final AccountRecordCubit _accountRecordCubit;
|
||||||
|
final ContactInvitationListCubit _contactInvitationListCubit;
|
||||||
|
final ContactListCubit _contactListCubit;
|
||||||
|
final _singleInvitationStatusProcessor =
|
||||||
|
SingleStateProcessor<WaitingInvitationsBlocMapState>();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import 'package:meta/meta.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../account_manager/account_manager.dart';
|
import '../../account_manager/account_manager.dart';
|
||||||
import '../../contacts/contacts.dart';
|
import '../../conversation/conversation.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
|
|
@ -13,14 +13,12 @@ import 'models.dart';
|
||||||
class ValidContactInvitation {
|
class ValidContactInvitation {
|
||||||
@internal
|
@internal
|
||||||
ValidContactInvitation(
|
ValidContactInvitation(
|
||||||
{required ActiveAccountInfo activeAccountInfo,
|
{required AccountInfo accountInfo,
|
||||||
required proto.Account account,
|
|
||||||
required TypedKey contactRequestInboxKey,
|
required TypedKey contactRequestInboxKey,
|
||||||
required proto.ContactRequestPrivate contactRequestPrivate,
|
required proto.ContactRequestPrivate contactRequestPrivate,
|
||||||
required SuperIdentity contactSuperIdentity,
|
required SuperIdentity contactSuperIdentity,
|
||||||
required KeyPair writer})
|
required KeyPair writer})
|
||||||
: _activeAccountInfo = activeAccountInfo,
|
: _accountInfo = accountInfo,
|
||||||
_account = account,
|
|
||||||
_contactRequestInboxKey = contactRequestInboxKey,
|
_contactRequestInboxKey = contactRequestInboxKey,
|
||||||
_contactRequestPrivate = contactRequestPrivate,
|
_contactRequestPrivate = contactRequestPrivate,
|
||||||
_contactSuperIdentity = contactSuperIdentity,
|
_contactSuperIdentity = contactSuperIdentity,
|
||||||
|
|
@ -28,44 +26,40 @@ class ValidContactInvitation {
|
||||||
|
|
||||||
proto.Profile get remoteProfile => _contactRequestPrivate.profile;
|
proto.Profile get remoteProfile => _contactRequestPrivate.profile;
|
||||||
|
|
||||||
Future<AcceptedContact?> accept() async {
|
Future<AcceptedContact?> accept(proto.Profile profile) async {
|
||||||
final pool = DHTRecordPool.instance;
|
final pool = DHTRecordPool.instance;
|
||||||
try {
|
try {
|
||||||
// Ensure we don't delete this if we're trying to chat to self
|
// Ensure we don't delete this if we're trying to chat to self
|
||||||
// The initiating side will delete the records in deleteInvitation()
|
// The initiating side will delete the records in deleteInvitation()
|
||||||
final isSelf = _contactSuperIdentity.currentInstance.publicKey ==
|
final isSelf = _contactSuperIdentity.currentInstance.publicKey ==
|
||||||
_activeAccountInfo.identityPublicKey;
|
_accountInfo.identityPublicKey;
|
||||||
final accountRecordKey = _activeAccountInfo.accountRecordKey;
|
|
||||||
|
|
||||||
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
|
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
|
||||||
debugName: 'ValidContactInvitation::accept::'
|
debugName: 'ValidContactInvitation::accept::'
|
||||||
'ContactRequestInbox',
|
'ContactRequestInbox',
|
||||||
parent: accountRecordKey))
|
parent: pool.getParentRecordKey(_contactRequestInboxKey) ??
|
||||||
|
_accountInfo.accountRecordKey))
|
||||||
// ignore: prefer_expression_function_bodies
|
// ignore: prefer_expression_function_bodies
|
||||||
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
||||||
// Create local conversation key for this
|
// Create local conversation key for this
|
||||||
// contact and send via contact response
|
// contact and send via contact response
|
||||||
final conversation = ConversationCubit(
|
final conversation = ConversationCubit(
|
||||||
activeAccountInfo: _activeAccountInfo,
|
accountInfo: _accountInfo,
|
||||||
remoteIdentityPublicKey:
|
remoteIdentityPublicKey:
|
||||||
_contactSuperIdentity.currentInstance.typedPublicKey);
|
_contactSuperIdentity.currentInstance.typedPublicKey);
|
||||||
return conversation.initLocalConversation(
|
return conversation.initLocalConversation(
|
||||||
profile: _account.profile,
|
profile: profile,
|
||||||
callback: (localConversation) async {
|
callback: (localConversation) async {
|
||||||
final contactResponse = proto.ContactResponse()
|
final contactResponse = proto.ContactResponse()
|
||||||
..accept = true
|
..accept = true
|
||||||
..remoteConversationRecordKey = localConversation.key.toProto()
|
..remoteConversationRecordKey = localConversation.key.toProto()
|
||||||
..superIdentityRecordKey =
|
..superIdentityRecordKey =
|
||||||
_activeAccountInfo.superIdentityRecordKey.toProto();
|
_accountInfo.superIdentityRecordKey.toProto();
|
||||||
final contactResponseBytes = contactResponse.writeToBuffer();
|
final contactResponseBytes = contactResponse.writeToBuffer();
|
||||||
|
|
||||||
final cs = await pool.veilid
|
final cs = await _accountInfo.identityCryptoSystem;
|
||||||
.getCryptoSystem(_contactRequestInboxKey.kind);
|
final identitySignature = await cs.signWithKeyPair(
|
||||||
|
_accountInfo.identityWriter, contactResponseBytes);
|
||||||
final identitySignature = await cs.sign(
|
|
||||||
_activeAccountInfo.identityWriter.key,
|
|
||||||
_activeAccountInfo.identityWriter.secret,
|
|
||||||
contactResponseBytes);
|
|
||||||
|
|
||||||
final signedContactResponse = proto.SignedContactResponse()
|
final signedContactResponse = proto.SignedContactResponse()
|
||||||
..contactResponse = contactResponseBytes
|
..contactResponse = contactResponseBytes
|
||||||
|
|
@ -95,27 +89,22 @@ class ValidContactInvitation {
|
||||||
|
|
||||||
// Ensure we don't delete this if we're trying to chat to self
|
// Ensure we don't delete this if we're trying to chat to self
|
||||||
final isSelf = _contactSuperIdentity.currentInstance.publicKey ==
|
final isSelf = _contactSuperIdentity.currentInstance.publicKey ==
|
||||||
_activeAccountInfo.identityPublicKey;
|
_accountInfo.identityPublicKey;
|
||||||
final accountRecordKey = _activeAccountInfo.accountRecordKey;
|
|
||||||
|
|
||||||
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
|
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
|
||||||
debugName: 'ValidContactInvitation::reject::'
|
debugName: 'ValidContactInvitation::reject::'
|
||||||
'ContactRequestInbox',
|
'ContactRequestInbox',
|
||||||
parent: accountRecordKey))
|
parent: _accountInfo.accountRecordKey))
|
||||||
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
|
||||||
final cs =
|
|
||||||
await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind);
|
|
||||||
|
|
||||||
final contactResponse = proto.ContactResponse()
|
final contactResponse = proto.ContactResponse()
|
||||||
..accept = false
|
..accept = false
|
||||||
..superIdentityRecordKey =
|
..superIdentityRecordKey =
|
||||||
_activeAccountInfo.superIdentityRecordKey.toProto();
|
_accountInfo.superIdentityRecordKey.toProto();
|
||||||
final contactResponseBytes = contactResponse.writeToBuffer();
|
final contactResponseBytes = contactResponse.writeToBuffer();
|
||||||
|
|
||||||
final identitySignature = await cs.sign(
|
final cs = await _accountInfo.identityCryptoSystem;
|
||||||
_activeAccountInfo.identityWriter.key,
|
final identitySignature = await cs.signWithKeyPair(
|
||||||
_activeAccountInfo.identityWriter.secret,
|
_accountInfo.identityWriter, contactResponseBytes);
|
||||||
contactResponseBytes);
|
|
||||||
|
|
||||||
final signedContactResponse = proto.SignedContactResponse()
|
final signedContactResponse = proto.SignedContactResponse()
|
||||||
..contactResponse = contactResponseBytes
|
..contactResponse = contactResponseBytes
|
||||||
|
|
@ -129,8 +118,7 @@ class ValidContactInvitation {
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
final ActiveAccountInfo _activeAccountInfo;
|
final AccountInfo _accountInfo;
|
||||||
final proto.Account _account;
|
|
||||||
final TypedKey _contactRequestInboxKey;
|
final TypedKey _contactRequestInboxKey;
|
||||||
final SuperIdentity _contactSuperIdentity;
|
final SuperIdentity _contactSuperIdentity;
|
||||||
final KeyPair _writer;
|
final KeyPair _writer;
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||||
// ignore: prefer_expression_function_bodies
|
// ignore: prefer_expression_function_bodies
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
//final scale = theme.extension<ScaleScheme>()!;
|
|
||||||
final textTheme = theme.textTheme;
|
final textTheme = theme.textTheme;
|
||||||
|
|
||||||
final signedContactInvitationBytesV =
|
final signedContactInvitationBytesV =
|
||||||
|
|
@ -58,6 +57,9 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||||
return PopControl(
|
return PopControl(
|
||||||
dismissible: !signedContactInvitationBytesV.isLoading,
|
dismissible: !signedContactInvitationBytesV.isLoading,
|
||||||
child: Dialog(
|
child: Dialog(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: const BorderSide(width: 2),
|
||||||
|
borderRadius: BorderRadius.circular(16)),
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
|
|
@ -90,6 +92,10 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
|
||||||
.paddingAll(8),
|
.paddingAll(8),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.copy),
|
icon: const Icon(Icons.copy),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
side: const BorderSide()),
|
||||||
label: Text(translate(
|
label: Text(translate(
|
||||||
'create_invitation_dialog.copy_invitation')),
|
'create_invitation_dialog.copy_invitation')),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|
|
||||||
|
|
@ -140,8 +140,18 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
|
||||||
// Start generation
|
// Start generation
|
||||||
final contactInvitationListCubit =
|
final contactInvitationListCubit =
|
||||||
widget.modalContext.read<ContactInvitationListCubit>();
|
widget.modalContext.read<ContactInvitationListCubit>();
|
||||||
|
final profile = widget.modalContext
|
||||||
|
.read<AccountRecordCubit>()
|
||||||
|
.state
|
||||||
|
.asData
|
||||||
|
?.value
|
||||||
|
.profile;
|
||||||
|
if (profile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final generator = contactInvitationListCubit.createInvitation(
|
final generator = contactInvitationListCubit.createInvitation(
|
||||||
|
profile: profile,
|
||||||
encryptionKeyType: _encryptionKeyType,
|
encryptionKeyType: _encryptionKeyType,
|
||||||
encryptionKey: _encryptionKey,
|
encryptionKey: _encryptionKey,
|
||||||
message: _messageTextController.text,
|
message: _messageTextController.text,
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import 'dart:async';
|
||||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../account_manager/account_manager.dart';
|
import '../../account_manager/account_manager.dart';
|
||||||
|
|
@ -15,13 +15,14 @@ import '../contact_invitation.dart';
|
||||||
|
|
||||||
class InvitationDialog extends StatefulWidget {
|
class InvitationDialog extends StatefulWidget {
|
||||||
const InvitationDialog(
|
const InvitationDialog(
|
||||||
{required this.modalContext,
|
{required Locator locator,
|
||||||
required this.onValidationCancelled,
|
required this.onValidationCancelled,
|
||||||
required this.onValidationSuccess,
|
required this.onValidationSuccess,
|
||||||
required this.onValidationFailed,
|
required this.onValidationFailed,
|
||||||
required this.inviteControlIsValid,
|
required this.inviteControlIsValid,
|
||||||
required this.buildInviteControl,
|
required this.buildInviteControl,
|
||||||
super.key});
|
super.key})
|
||||||
|
: _locator = locator;
|
||||||
|
|
||||||
final void Function() onValidationCancelled;
|
final void Function() onValidationCancelled;
|
||||||
final void Function() onValidationSuccess;
|
final void Function() onValidationSuccess;
|
||||||
|
|
@ -32,7 +33,7 @@ class InvitationDialog extends StatefulWidget {
|
||||||
InvitationDialogState dialogState,
|
InvitationDialogState dialogState,
|
||||||
Future<void> Function({required Uint8List inviteData})
|
Future<void> Function({required Uint8List inviteData})
|
||||||
validateInviteData) buildInviteControl;
|
validateInviteData) buildInviteControl;
|
||||||
final BuildContext modalContext;
|
final Locator _locator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InvitationDialogState createState() => InvitationDialogState();
|
InvitationDialogState createState() => InvitationDialogState();
|
||||||
|
|
@ -54,8 +55,7 @@ class InvitationDialog extends StatefulWidget {
|
||||||
InvitationDialogState dialogState,
|
InvitationDialogState dialogState,
|
||||||
Future<void> Function({required Uint8List inviteData})
|
Future<void> Function({required Uint8List inviteData})
|
||||||
validateInviteData)>.has(
|
validateInviteData)>.has(
|
||||||
'buildInviteControl', buildInviteControl))
|
'buildInviteControl', buildInviteControl));
|
||||||
..add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,23 +74,25 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||||
|
|
||||||
Future<void> _onAccept() async {
|
Future<void> _onAccept() async {
|
||||||
final navigator = Navigator.of(context);
|
final navigator = Navigator.of(context);
|
||||||
final activeAccountInfo = widget.modalContext.read<ActiveAccountInfo>();
|
final accountInfo = widget._locator<AccountInfoCubit>().state;
|
||||||
final contactList = widget.modalContext.read<ContactListCubit>();
|
final contactList = widget._locator<ContactListCubit>();
|
||||||
|
final profile =
|
||||||
|
widget._locator<AccountRecordCubit>().state.asData!.value.profile;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isAccepting = true;
|
_isAccepting = true;
|
||||||
});
|
});
|
||||||
final validInvitation = _validInvitation;
|
final validInvitation = _validInvitation;
|
||||||
if (validInvitation != null) {
|
if (validInvitation != null) {
|
||||||
final acceptedContact = await validInvitation.accept();
|
final acceptedContact = await validInvitation.accept(profile);
|
||||||
if (acceptedContact != null) {
|
if (acceptedContact != null) {
|
||||||
// initiator when accept is received will create
|
// initiator when accept is received will create
|
||||||
// contact in the case of a 'note to self'
|
// contact in the case of a 'note to self'
|
||||||
final isSelf = activeAccountInfo.identityPublicKey ==
|
final isSelf = accountInfo.identityPublicKey ==
|
||||||
acceptedContact.remoteIdentity.currentInstance.publicKey;
|
acceptedContact.remoteIdentity.currentInstance.publicKey;
|
||||||
if (!isSelf) {
|
if (!isSelf) {
|
||||||
await contactList.createContact(
|
await contactList.createContact(
|
||||||
remoteProfile: acceptedContact.remoteProfile,
|
profile: acceptedContact.remoteProfile,
|
||||||
remoteSuperIdentity: acceptedContact.remoteIdentity,
|
remoteSuperIdentity: acceptedContact.remoteIdentity,
|
||||||
remoteConversationRecordKey:
|
remoteConversationRecordKey:
|
||||||
acceptedContact.remoteConversationRecordKey,
|
acceptedContact.remoteConversationRecordKey,
|
||||||
|
|
@ -137,7 +139,7 @@ class InvitationDialogState extends State<InvitationDialog> {
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final contactInvitationListCubit =
|
final contactInvitationListCubit =
|
||||||
widget.modalContext.read<ContactInvitationListCubit>();
|
widget._locator<ContactInvitationListCubit>();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isValidating = true;
|
_isValidating = true;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:awesome_extensions/awesome_extensions.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../theme/theme.dart';
|
import '../../theme/theme.dart';
|
||||||
|
|
@ -11,29 +12,23 @@ import '../../tools/tools.dart';
|
||||||
import 'invitation_dialog.dart';
|
import 'invitation_dialog.dart';
|
||||||
|
|
||||||
class PasteInvitationDialog extends StatefulWidget {
|
class PasteInvitationDialog extends StatefulWidget {
|
||||||
const PasteInvitationDialog({required this.modalContext, super.key});
|
const PasteInvitationDialog({required Locator locator, super.key})
|
||||||
|
: _locator = locator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PasteInvitationDialogState createState() => PasteInvitationDialogState();
|
PasteInvitationDialogState createState() => PasteInvitationDialogState();
|
||||||
|
|
||||||
static Future<void> show(BuildContext context) async {
|
static Future<void> show(BuildContext context) async {
|
||||||
final modalContext = context;
|
final locator = context.read;
|
||||||
|
|
||||||
await showPopControlDialog<void>(
|
await showPopControlDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => StyledDialog(
|
builder: (context) => StyledDialog(
|
||||||
title: translate('paste_invitation_dialog.title'),
|
title: translate('paste_invitation_dialog.title'),
|
||||||
child: PasteInvitationDialog(modalContext: modalContext)));
|
child: PasteInvitationDialog(locator: locator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
final BuildContext modalContext;
|
final Locator _locator;
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
||||||
super.debugFillProperties(properties);
|
|
||||||
properties
|
|
||||||
.add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PasteInvitationDialogState extends State<PasteInvitationDialog> {
|
class PasteInvitationDialogState extends State<PasteInvitationDialog> {
|
||||||
|
|
@ -138,7 +133,7 @@ class PasteInvitationDialogState extends State<PasteInvitationDialog> {
|
||||||
// ignore: prefer_expression_function_bodies
|
// ignore: prefer_expression_function_bodies
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InvitationDialog(
|
return InvitationDialog(
|
||||||
modalContext: widget.modalContext,
|
locator: widget._locator,
|
||||||
onValidationCancelled: onValidationCancelled,
|
onValidationCancelled: onValidationCancelled,
|
||||||
onValidationSuccess: onValidationSuccess,
|
onValidationSuccess: onValidationSuccess,
|
||||||
onValidationFailed: onValidationFailed,
|
onValidationFailed: onValidationFailed,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
import 'package:pasteboard/pasteboard.dart';
|
import 'package:pasteboard/pasteboard.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:zxing2/qrcode.dart';
|
import 'package:zxing2/qrcode.dart';
|
||||||
|
|
||||||
import '../../theme/theme.dart';
|
import '../../theme/theme.dart';
|
||||||
|
|
@ -102,28 +103,22 @@ class ScannerOverlay extends CustomPainter {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScanInvitationDialog extends StatefulWidget {
|
class ScanInvitationDialog extends StatefulWidget {
|
||||||
const ScanInvitationDialog({required this.modalContext, super.key});
|
const ScanInvitationDialog({required Locator locator, super.key})
|
||||||
|
: _locator = locator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ScanInvitationDialogState createState() => ScanInvitationDialogState();
|
ScanInvitationDialogState createState() => ScanInvitationDialogState();
|
||||||
|
|
||||||
static Future<void> show(BuildContext context) async {
|
static Future<void> show(BuildContext context) async {
|
||||||
final modalContext = context;
|
final locator = context.read;
|
||||||
await showPopControlDialog<void>(
|
await showPopControlDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => StyledDialog(
|
builder: (context) => StyledDialog(
|
||||||
title: translate('scan_invitation_dialog.title'),
|
title: translate('scan_invitation_dialog.title'),
|
||||||
child: ScanInvitationDialog(modalContext: modalContext)));
|
child: ScanInvitationDialog(locator: locator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
final BuildContext modalContext;
|
final Locator _locator;
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
||||||
super.debugFillProperties(properties);
|
|
||||||
properties
|
|
||||||
.add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScanInvitationDialogState extends State<ScanInvitationDialog> {
|
class ScanInvitationDialogState extends State<ScanInvitationDialog> {
|
||||||
|
|
@ -396,7 +391,7 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
|
||||||
// ignore: prefer_expression_function_bodies
|
// ignore: prefer_expression_function_bodies
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InvitationDialog(
|
return InvitationDialog(
|
||||||
modalContext: widget.modalContext,
|
locator: widget._locator,
|
||||||
onValidationCancelled: onValidationCancelled,
|
onValidationCancelled: onValidationCancelled,
|
||||||
onValidationSuccess: onValidationSuccess,
|
onValidationSuccess: onValidationSuccess,
|
||||||
onValidationFailed: onValidationFailed,
|
onValidationFailed: onValidationFailed,
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,117 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:async_tools/async_tools.dart';
|
||||||
|
import 'package:protobuf/protobuf.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../account_manager/account_manager.dart';
|
import '../../account_manager/account_manager.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
import 'conversation_cubit.dart';
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
// Mutable state for per-account contacts
|
// Mutable state for per-account contacts
|
||||||
|
|
||||||
class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
||||||
ContactListCubit({
|
ContactListCubit({
|
||||||
required ActiveAccountInfo activeAccountInfo,
|
required AccountInfo accountInfo,
|
||||||
required proto.Account account,
|
required OwnedDHTRecordPointer contactListRecordPointer,
|
||||||
}) : _activeAccountInfo = activeAccountInfo,
|
}) : super(
|
||||||
super(
|
open: () =>
|
||||||
open: () => _open(activeAccountInfo, account),
|
_open(accountInfo.accountRecordKey, contactListRecordPointer),
|
||||||
decodeElement: proto.Contact.fromBuffer);
|
decodeElement: proto.Contact.fromBuffer);
|
||||||
|
|
||||||
static Future<DHTShortArray> _open(
|
static Future<DHTShortArray> _open(TypedKey accountRecordKey,
|
||||||
ActiveAccountInfo activeAccountInfo, proto.Account account) async {
|
OwnedDHTRecordPointer contactListRecordPointer) async {
|
||||||
final accountRecordKey =
|
final dhtRecord = await DHTShortArray.openOwned(contactListRecordPointer,
|
||||||
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
|
||||||
|
|
||||||
final contactListRecordKey = account.contactList.toVeilid();
|
|
||||||
|
|
||||||
final dhtRecord = await DHTShortArray.openOwned(contactListRecordKey,
|
|
||||||
debugName: 'ContactListCubit::_open::ContactList',
|
debugName: 'ContactListCubit::_open::ContactList',
|
||||||
parent: accountRecordKey);
|
parent: accountRecordKey);
|
||||||
|
|
||||||
return dhtRecord;
|
return dhtRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createContact({
|
@override
|
||||||
required proto.Profile remoteProfile,
|
Future<void> close() async {
|
||||||
required SuperIdentity remoteSuperIdentity,
|
await _contactProfileUpdateMap.close();
|
||||||
required TypedKey remoteConversationRecordKey,
|
await super.close();
|
||||||
|
}
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Public Interface
|
||||||
|
|
||||||
|
void followContactProfileChanges(TypedKey localConversationRecordKey,
|
||||||
|
Stream<proto.Profile?> profileStream, proto.Profile? profileState) {
|
||||||
|
_contactProfileUpdateMap
|
||||||
|
.follow(localConversationRecordKey, profileStream, profileState,
|
||||||
|
(remoteProfile) async {
|
||||||
|
if (remoteProfile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return updateContactProfile(
|
||||||
|
localConversationRecordKey: localConversationRecordKey,
|
||||||
|
profile: remoteProfile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateContactProfile({
|
||||||
required TypedKey localConversationRecordKey,
|
required TypedKey localConversationRecordKey,
|
||||||
|
required proto.Profile profile,
|
||||||
|
}) async {
|
||||||
|
// Update contact's remoteProfile
|
||||||
|
await operateWriteEventual((writer) async {
|
||||||
|
for (var pos = 0; pos < writer.length; pos++) {
|
||||||
|
final c = await writer.getProtobuf(proto.Contact.fromBuffer, pos);
|
||||||
|
if (c != null &&
|
||||||
|
c.localConversationRecordKey.toVeilid() ==
|
||||||
|
localConversationRecordKey) {
|
||||||
|
if (c.profile == profile) {
|
||||||
|
// Unchanged
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
final newContact = c.deepCopy()..profile = profile;
|
||||||
|
final updated = await writer.tryWriteItemProtobuf(
|
||||||
|
proto.Contact.fromBuffer, pos, newContact);
|
||||||
|
if (!updated) {
|
||||||
|
throw DHTExceptionTryAgain();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createContact({
|
||||||
|
required proto.Profile profile,
|
||||||
|
required SuperIdentity remoteSuperIdentity,
|
||||||
|
required TypedKey localConversationRecordKey,
|
||||||
|
required TypedKey remoteConversationRecordKey,
|
||||||
}) async {
|
}) async {
|
||||||
// Create Contact
|
// Create Contact
|
||||||
final contact = proto.Contact()
|
final contact = proto.Contact()
|
||||||
..editedProfile = remoteProfile
|
..profile = profile
|
||||||
..remoteProfile = remoteProfile
|
|
||||||
..superIdentityJson = jsonEncode(remoteSuperIdentity.toJson())
|
..superIdentityJson = jsonEncode(remoteSuperIdentity.toJson())
|
||||||
..identityPublicKey =
|
..identityPublicKey =
|
||||||
remoteSuperIdentity.currentInstance.typedPublicKey.toProto()
|
remoteSuperIdentity.currentInstance.typedPublicKey.toProto()
|
||||||
..remoteConversationRecordKey = remoteConversationRecordKey.toProto()
|
|
||||||
..localConversationRecordKey = localConversationRecordKey.toProto()
|
..localConversationRecordKey = localConversationRecordKey.toProto()
|
||||||
|
..remoteConversationRecordKey = remoteConversationRecordKey.toProto()
|
||||||
..showAvailability = false;
|
..showAvailability = false;
|
||||||
|
|
||||||
// Add Contact to account's list
|
// Add Contact to account's list
|
||||||
// if this fails, don't keep retrying, user can try again later
|
await operateWriteEventual((writer) async {
|
||||||
await operateWrite((writer) async {
|
|
||||||
await writer.add(contact.writeToBuffer());
|
await writer.add(contact.writeToBuffer());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteContact({required proto.Contact contact}) async {
|
Future<void> deleteContact(
|
||||||
final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
|
{required TypedKey localConversationRecordKey}) async {
|
||||||
final localConversationRecordKey =
|
|
||||||
contact.localConversationRecordKey.toVeilid();
|
|
||||||
final remoteConversationRecordKey =
|
|
||||||
contact.remoteConversationRecordKey.toVeilid();
|
|
||||||
|
|
||||||
// Remove Contact from account's list
|
// Remove Contact from account's list
|
||||||
final deletedItem = await operateWrite((writer) async {
|
final deletedItem = await operateWriteEventual((writer) async {
|
||||||
for (var i = 0; i < writer.length; i++) {
|
for (var i = 0; i < writer.length; i++) {
|
||||||
final item = await writer.getProtobuf(proto.Contact.fromBuffer, i);
|
final item = await writer.getProtobuf(proto.Contact.fromBuffer, i);
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
throw Exception('Failed to get contact');
|
throw Exception('Failed to get contact');
|
||||||
}
|
}
|
||||||
if (item.localConversationRecordKey ==
|
if (item.localConversationRecordKey.toVeilid() ==
|
||||||
contact.localConversationRecordKey) {
|
localConversationRecordKey) {
|
||||||
await writer.remove(i);
|
await writer.remove(i);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
@ -83,21 +121,17 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
||||||
|
|
||||||
if (deletedItem != null) {
|
if (deletedItem != null) {
|
||||||
try {
|
try {
|
||||||
// Make a conversation cubit to manipulate the conversation
|
// Mark the conversation records for deletion
|
||||||
final conversationCubit = ConversationCubit(
|
await DHTRecordPool.instance
|
||||||
activeAccountInfo: _activeAccountInfo,
|
.deleteRecord(deletedItem.localConversationRecordKey.toVeilid());
|
||||||
remoteIdentityPublicKey: remoteIdentityPublicKey,
|
await DHTRecordPool.instance
|
||||||
localConversationRecordKey: localConversationRecordKey,
|
.deleteRecord(deletedItem.remoteConversationRecordKey.toVeilid());
|
||||||
remoteConversationRecordKey: remoteConversationRecordKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delete the local and remote conversation records
|
|
||||||
await conversationCubit.delete();
|
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
log.debug('error deleting conversation records: $e', e);
|
log.debug('error deleting conversation records: $e', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final ActiveAccountInfo _activeAccountInfo;
|
final _contactProfileUpdateMap =
|
||||||
|
SingleStateProcessorMap<TypedKey, proto.Profile?>();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
export 'contact_list_cubit.dart';
|
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/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
@ -11,18 +10,9 @@ import '../contacts.dart';
|
||||||
|
|
||||||
class ContactItemWidget extends StatelessWidget {
|
class ContactItemWidget extends StatelessWidget {
|
||||||
const ContactItemWidget(
|
const ContactItemWidget(
|
||||||
{required this.contact, required this.disabled, super.key});
|
{required proto.Contact contact, required bool disabled, super.key})
|
||||||
|
: _disabled = disabled,
|
||||||
final proto.Contact contact;
|
_contact = contact;
|
||||||
final bool disabled;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
||||||
super.debugFillProperties(properties);
|
|
||||||
properties
|
|
||||||
..add(DiagnosticsProperty<proto.Contact>('contact', contact))
|
|
||||||
..add(DiagnosticsProperty<bool>('disabled', disabled));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: prefer_expression_function_bodies
|
// ignore: prefer_expression_function_bodies
|
||||||
|
|
@ -30,26 +20,44 @@ class ContactItemWidget extends StatelessWidget {
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
) {
|
) {
|
||||||
final localConversationRecordKey =
|
final localConversationRecordKey =
|
||||||
contact.localConversationRecordKey.toVeilid();
|
_contact.localConversationRecordKey.toVeilid();
|
||||||
|
|
||||||
const selected = false; // xxx: eventually when we have selectable contacts:
|
const selected = false; // xxx: eventually when we have selectable contacts:
|
||||||
// activeContactCubit.state == localConversationRecordKey;
|
// activeContactCubit.state == localConversationRecordKey;
|
||||||
|
|
||||||
final tileDisabled = disabled || context.watch<ContactListCubit>().isBusy;
|
final tileDisabled = _disabled || context.watch<ContactListCubit>().isBusy;
|
||||||
|
|
||||||
|
late final String title;
|
||||||
|
late final String subtitle;
|
||||||
|
if (_contact.nickname.isNotEmpty) {
|
||||||
|
title = _contact.nickname;
|
||||||
|
if (_contact.profile.pronouns.isNotEmpty) {
|
||||||
|
subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})';
|
||||||
|
} else {
|
||||||
|
subtitle = _contact.profile.name;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
title = _contact.profile.name;
|
||||||
|
if (_contact.profile.pronouns.isNotEmpty) {
|
||||||
|
subtitle = '(${_contact.profile.pronouns})';
|
||||||
|
} else {
|
||||||
|
subtitle = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return SliderTile(
|
return SliderTile(
|
||||||
key: ObjectKey(contact),
|
key: ObjectKey(_contact),
|
||||||
disabled: tileDisabled,
|
disabled: tileDisabled,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
tileScale: ScaleKind.primary,
|
tileScale: ScaleKind.primary,
|
||||||
title: contact.editedProfile.name,
|
title: title,
|
||||||
subtitle: contact.editedProfile.pronouns,
|
subtitle: subtitle,
|
||||||
icon: Icons.person,
|
icon: Icons.person,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
// Start a chat
|
// Start a chat
|
||||||
final chatListCubit = context.read<ChatListCubit>();
|
final chatListCubit = context.read<ChatListCubit>();
|
||||||
|
|
||||||
await chatListCubit.getOrCreateChatSingleContact(contact: contact);
|
await chatListCubit.getOrCreateChatSingleContact(contact: _contact);
|
||||||
// Click over to chats
|
// Click over to chats
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
await MainPager.of(context)
|
await MainPager.of(context)
|
||||||
|
|
@ -66,14 +74,20 @@ class ContactItemWidget extends StatelessWidget {
|
||||||
final contactListCubit = context.read<ContactListCubit>();
|
final contactListCubit = context.read<ContactListCubit>();
|
||||||
final chatListCubit = context.read<ChatListCubit>();
|
final chatListCubit = context.read<ChatListCubit>();
|
||||||
|
|
||||||
|
// Delete the contact itself
|
||||||
|
await contactListCubit.deleteContact(
|
||||||
|
localConversationRecordKey: localConversationRecordKey);
|
||||||
|
|
||||||
// Remove any chats for this contact
|
// Remove any chats for this contact
|
||||||
await chatListCubit.deleteChat(
|
await chatListCubit.deleteChat(
|
||||||
localConversationRecordKey: localConversationRecordKey);
|
localConversationRecordKey: localConversationRecordKey);
|
||||||
|
|
||||||
// Delete the contact itself
|
|
||||||
await contactListCubit.deleteContact(contact: contact);
|
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
final proto.Contact _contact;
|
||||||
|
final bool _disabled;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,13 @@ class ContactListWidget extends StatelessWidget {
|
||||||
final lowerValue = value.toLowerCase();
|
final lowerValue = value.toLowerCase();
|
||||||
return contactList
|
return contactList
|
||||||
.where((element) =>
|
.where((element) =>
|
||||||
element.editedProfile.name
|
element.nickname
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.contains(lowerValue) ||
|
.contains(lowerValue) ||
|
||||||
element.editedProfile.pronouns
|
element.profile.name
|
||||||
|
.toLowerCase()
|
||||||
|
.contains(lowerValue) ||
|
||||||
|
element.profile.pronouns
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.contains(lowerValue))
|
.contains(lowerValue))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
|
||||||
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:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:protobuf/protobuf.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../account_manager/account_manager.dart';
|
import '../../account_manager/account_manager.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../tools/tools.dart';
|
|
||||||
|
const _sfUpdateAccountChange = 'updateAccountChange';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class ConversationState extends Equatable {
|
class ConversationState extends Equatable {
|
||||||
|
|
@ -27,32 +29,35 @@ class ConversationState extends Equatable {
|
||||||
List<Object?> get props => [localConversation, remoteConversation];
|
List<Object?> get props => [localConversation, remoteConversation];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the control channel between two contacts
|
||||||
|
/// Used to pass profile, identity and status changes, and the messages key for
|
||||||
|
/// 1-1 chats
|
||||||
class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||||
ConversationCubit(
|
ConversationCubit(
|
||||||
{required ActiveAccountInfo activeAccountInfo,
|
{required AccountInfo accountInfo,
|
||||||
required TypedKey remoteIdentityPublicKey,
|
required TypedKey remoteIdentityPublicKey,
|
||||||
TypedKey? localConversationRecordKey,
|
TypedKey? localConversationRecordKey,
|
||||||
TypedKey? remoteConversationRecordKey})
|
TypedKey? remoteConversationRecordKey})
|
||||||
: _activeAccountInfo = activeAccountInfo,
|
: _accountInfo = accountInfo,
|
||||||
_localConversationRecordKey = localConversationRecordKey,
|
_localConversationRecordKey = localConversationRecordKey,
|
||||||
_remoteIdentityPublicKey = remoteIdentityPublicKey,
|
_remoteIdentityPublicKey = remoteIdentityPublicKey,
|
||||||
_remoteConversationRecordKey = remoteConversationRecordKey,
|
_remoteConversationRecordKey = remoteConversationRecordKey,
|
||||||
super(const AsyncValue.loading()) {
|
super(const AsyncValue.loading()) {
|
||||||
|
_identityWriter = _accountInfo.identityWriter;
|
||||||
|
|
||||||
if (_localConversationRecordKey != null) {
|
if (_localConversationRecordKey != null) {
|
||||||
_initWait.add(() async {
|
_initWait.add(() async {
|
||||||
await _setLocalConversation(() async {
|
await _setLocalConversation(() async {
|
||||||
final accountRecordKey = _activeAccountInfo
|
|
||||||
.userLogin.accountRecordInfo.accountRecord.recordKey;
|
|
||||||
|
|
||||||
// Open local record key if it is specified
|
// Open local record key if it is specified
|
||||||
final pool = DHTRecordPool.instance;
|
final pool = DHTRecordPool.instance;
|
||||||
final crypto = await _cachedConversationCrypto();
|
final crypto = await _cachedConversationCrypto();
|
||||||
final writer = _activeAccountInfo.identityWriter;
|
final writer = _identityWriter;
|
||||||
final record = await pool.openRecordWrite(
|
final record = await pool.openRecordWrite(
|
||||||
_localConversationRecordKey!, writer,
|
_localConversationRecordKey!, writer,
|
||||||
debugName: 'ConversationCubit::LocalConversation',
|
debugName: 'ConversationCubit::LocalConversation',
|
||||||
parent: accountRecordKey,
|
parent: accountInfo.accountRecordKey,
|
||||||
crypto: crypto);
|
crypto: crypto);
|
||||||
|
|
||||||
return record;
|
return record;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -61,15 +66,13 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||||
if (_remoteConversationRecordKey != null) {
|
if (_remoteConversationRecordKey != null) {
|
||||||
_initWait.add(() async {
|
_initWait.add(() async {
|
||||||
await _setRemoteConversation(() async {
|
await _setRemoteConversation(() async {
|
||||||
final accountRecordKey = _activeAccountInfo
|
|
||||||
.userLogin.accountRecordInfo.accountRecord.recordKey;
|
|
||||||
|
|
||||||
// Open remote record key if it is specified
|
// Open remote record key if it is specified
|
||||||
final pool = DHTRecordPool.instance;
|
final pool = DHTRecordPool.instance;
|
||||||
final crypto = await _cachedConversationCrypto();
|
final crypto = await _cachedConversationCrypto();
|
||||||
final record = await pool.openRecordRead(_remoteConversationRecordKey,
|
final record = await pool.openRecordRead(_remoteConversationRecordKey,
|
||||||
debugName: 'ConversationCubit::RemoteConversation',
|
debugName: 'ConversationCubit::RemoteConversation',
|
||||||
parent: accountRecordKey,
|
parent: pool.getParentRecordKey(_remoteConversationRecordKey) ??
|
||||||
|
accountInfo.accountRecordKey,
|
||||||
crypto: crypto);
|
crypto: crypto);
|
||||||
return record;
|
return record;
|
||||||
});
|
});
|
||||||
|
|
@ -80,6 +83,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await _initWait();
|
await _initWait();
|
||||||
|
await _accountSubscription?.cancel();
|
||||||
await _localSubscription?.cancel();
|
await _localSubscription?.cancel();
|
||||||
await _remoteSubscription?.cancel();
|
await _remoteSubscription?.cancel();
|
||||||
await _localConversationCubit?.close();
|
await _localConversationCubit?.close();
|
||||||
|
|
@ -88,6 +92,130 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Public Interface
|
||||||
|
|
||||||
|
/// Initialize a local conversation
|
||||||
|
/// If we were the initiator of the conversation there may be an
|
||||||
|
/// incomplete 'existingConversationRecord' that we need to fill
|
||||||
|
/// in now that we have the remote identity key
|
||||||
|
/// The ConversationCubit must not already have a local conversation
|
||||||
|
/// The callback allows for more initialization to occur and for
|
||||||
|
/// cleanup to delete records upon failure of the callback
|
||||||
|
Future<T> initLocalConversation<T>(
|
||||||
|
{required proto.Profile profile,
|
||||||
|
required FutureOr<T> Function(DHTRecord) callback,
|
||||||
|
TypedKey? existingConversationRecordKey}) async {
|
||||||
|
assert(_localConversationRecordKey == null,
|
||||||
|
'must not have a local conversation yet');
|
||||||
|
|
||||||
|
final pool = DHTRecordPool.instance;
|
||||||
|
|
||||||
|
final crypto = await _cachedConversationCrypto();
|
||||||
|
final accountRecordKey = _accountInfo.accountRecordKey;
|
||||||
|
final writer = _accountInfo.identityWriter;
|
||||||
|
|
||||||
|
// Open with SMPL schema for identity writer
|
||||||
|
late final DHTRecord localConversationRecord;
|
||||||
|
if (existingConversationRecordKey != null) {
|
||||||
|
localConversationRecord = await pool.openRecordWrite(
|
||||||
|
existingConversationRecordKey, writer,
|
||||||
|
debugName:
|
||||||
|
'ConversationCubit::initLocalConversation::LocalConversation',
|
||||||
|
parent: accountRecordKey,
|
||||||
|
crypto: crypto);
|
||||||
|
} else {
|
||||||
|
localConversationRecord = await pool.createRecord(
|
||||||
|
debugName:
|
||||||
|
'ConversationCubit::initLocalConversation::LocalConversation',
|
||||||
|
parent: accountRecordKey,
|
||||||
|
crypto: crypto,
|
||||||
|
writer: writer,
|
||||||
|
schema: DHTSchema.smpl(
|
||||||
|
oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)]));
|
||||||
|
}
|
||||||
|
final out = localConversationRecord
|
||||||
|
// ignore: prefer_expression_function_bodies
|
||||||
|
.deleteScope((localConversation) async {
|
||||||
|
// Make messages log
|
||||||
|
return _initLocalMessages(
|
||||||
|
localConversationKey: localConversation.key,
|
||||||
|
callback: (messages) async {
|
||||||
|
// Create initial local conversation key contents
|
||||||
|
final conversation = proto.Conversation()
|
||||||
|
..profile = profile
|
||||||
|
..superIdentityJson =
|
||||||
|
jsonEncode(_accountInfo.localAccount.superIdentity.toJson())
|
||||||
|
..messages = messages.recordKey.toProto();
|
||||||
|
|
||||||
|
// Write initial conversation to record
|
||||||
|
final update = await localConversation.tryWriteProtobuf(
|
||||||
|
proto.Conversation.fromBuffer, conversation);
|
||||||
|
if (update != null) {
|
||||||
|
throw Exception('Failed to write local conversation');
|
||||||
|
}
|
||||||
|
final out = await callback(localConversation);
|
||||||
|
|
||||||
|
// Upon success emit the local conversation record to the state
|
||||||
|
_updateLocalConversationState(AsyncValue.data(conversation));
|
||||||
|
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// If success, save the new local conversation record key in this object
|
||||||
|
_localConversationRecordKey = localConversationRecord.key;
|
||||||
|
await _setLocalConversation(() async => localConversationRecord);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force refresh of conversation keys
|
||||||
|
Future<void> refresh() async {
|
||||||
|
await _initWait();
|
||||||
|
|
||||||
|
final lcc = _localConversationCubit;
|
||||||
|
final rcc = _remoteConversationCubit;
|
||||||
|
|
||||||
|
if (lcc != null) {
|
||||||
|
await lcc.refreshDefault();
|
||||||
|
}
|
||||||
|
if (rcc != null) {
|
||||||
|
await rcc.refreshDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Watch for account record changes and update the conversation
|
||||||
|
void watchAccountChanges(Stream<AsyncValue<proto.Account>> accountStream,
|
||||||
|
AsyncValue<proto.Account> currentState) {
|
||||||
|
assert(_accountSubscription == null, 'only watch account once');
|
||||||
|
_accountSubscription = accountStream.listen(_updateAccountChange);
|
||||||
|
_updateAccountChange(currentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private Implementation
|
||||||
|
|
||||||
|
void _updateAccountChange(AsyncValue<proto.Account> avaccount) {
|
||||||
|
final account = avaccount.asData?.value;
|
||||||
|
if (account == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final cubit = _localConversationCubit;
|
||||||
|
if (cubit == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
serialFuture((this, _sfUpdateAccountChange), () async {
|
||||||
|
await cubit.record.eventualUpdateProtobuf(proto.Conversation.fromBuffer,
|
||||||
|
(old) async {
|
||||||
|
if (old == null || old.profile == account.profile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return old.deepCopy()..profile = account.profile;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _updateLocalConversationState(AsyncValue<proto.Conversation> avconv) {
|
void _updateLocalConversationState(AsyncValue<proto.Conversation> avconv) {
|
||||||
final newState = avconv.when(
|
final newState = avconv.when(
|
||||||
data: (conv) {
|
data: (conv) {
|
||||||
|
|
@ -140,6 +268,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||||
open: open, decodeState: proto.Conversation.fromBuffer);
|
open: open, decodeState: proto.Conversation.fromBuffer);
|
||||||
_localSubscription =
|
_localSubscription =
|
||||||
_localConversationCubit!.stream.listen(_updateLocalConversationState);
|
_localConversationCubit!.stream.listen(_updateLocalConversationState);
|
||||||
|
_updateLocalConversationState(_localConversationCubit!.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open remote converation key
|
// Open remote converation key
|
||||||
|
|
@ -150,146 +279,16 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||||
open: open, decodeState: proto.Conversation.fromBuffer);
|
open: open, decodeState: proto.Conversation.fromBuffer);
|
||||||
_remoteSubscription =
|
_remoteSubscription =
|
||||||
_remoteConversationCubit!.stream.listen(_updateRemoteConversationState);
|
_remoteConversationCubit!.stream.listen(_updateRemoteConversationState);
|
||||||
}
|
_updateRemoteConversationState(_remoteConversationCubit!.state);
|
||||||
|
|
||||||
Future<bool> delete() async {
|
|
||||||
final pool = DHTRecordPool.instance;
|
|
||||||
|
|
||||||
await _initWait();
|
|
||||||
final localConversationCubit = _localConversationCubit;
|
|
||||||
final remoteConversationCubit = _remoteConversationCubit;
|
|
||||||
|
|
||||||
final deleteSet = DelayedWaitSet<void>();
|
|
||||||
|
|
||||||
if (localConversationCubit != null) {
|
|
||||||
final data = localConversationCubit.state.asData;
|
|
||||||
if (data == null) {
|
|
||||||
log.warning('could not delete local conversation');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteSet.add(() async {
|
|
||||||
_localConversationCubit = null;
|
|
||||||
await localConversationCubit.close();
|
|
||||||
final conversation = data.value;
|
|
||||||
final messagesKey = conversation.messages.toVeilid();
|
|
||||||
await pool.deleteRecord(messagesKey);
|
|
||||||
await pool.deleteRecord(_localConversationRecordKey!);
|
|
||||||
_localConversationRecordKey = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remoteConversationCubit != null) {
|
|
||||||
final data = remoteConversationCubit.state.asData;
|
|
||||||
if (data == null) {
|
|
||||||
log.warning('could not delete remote conversation');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteSet.add(() async {
|
|
||||||
_remoteConversationCubit = null;
|
|
||||||
await remoteConversationCubit.close();
|
|
||||||
final conversation = data.value;
|
|
||||||
final messagesKey = conversation.messages.toVeilid();
|
|
||||||
await pool.deleteRecord(messagesKey);
|
|
||||||
await pool.deleteRecord(_remoteConversationRecordKey!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit the delete futures
|
|
||||||
await deleteSet();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize a local conversation
|
|
||||||
// If we were the initiator of the conversation there may be an
|
|
||||||
// incomplete 'existingConversationRecord' that we need to fill
|
|
||||||
// in now that we have the remote identity key
|
|
||||||
// The ConversationCubit must not already have a local conversation
|
|
||||||
// The callback allows for more initialization to occur and for
|
|
||||||
// cleanup to delete records upon failure of the callback
|
|
||||||
Future<T> initLocalConversation<T>(
|
|
||||||
{required proto.Profile profile,
|
|
||||||
required FutureOr<T> Function(DHTRecord) callback,
|
|
||||||
TypedKey? existingConversationRecordKey}) async {
|
|
||||||
assert(_localConversationRecordKey == null,
|
|
||||||
'must not have a local conversation yet');
|
|
||||||
|
|
||||||
final pool = DHTRecordPool.instance;
|
|
||||||
final accountRecordKey =
|
|
||||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
|
||||||
|
|
||||||
final crypto = await _cachedConversationCrypto();
|
|
||||||
final writer = _activeAccountInfo.identityWriter;
|
|
||||||
|
|
||||||
// Open with SMPL scheme for identity writer
|
|
||||||
late final DHTRecord localConversationRecord;
|
|
||||||
if (existingConversationRecordKey != null) {
|
|
||||||
localConversationRecord = await pool.openRecordWrite(
|
|
||||||
existingConversationRecordKey, writer,
|
|
||||||
debugName:
|
|
||||||
'ConversationCubit::initLocalConversation::LocalConversation',
|
|
||||||
parent: accountRecordKey,
|
|
||||||
crypto: crypto);
|
|
||||||
} else {
|
|
||||||
localConversationRecord = await pool.createRecord(
|
|
||||||
debugName:
|
|
||||||
'ConversationCubit::initLocalConversation::LocalConversation',
|
|
||||||
parent: accountRecordKey,
|
|
||||||
crypto: crypto,
|
|
||||||
writer: writer,
|
|
||||||
schema: DHTSchema.smpl(
|
|
||||||
oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)]));
|
|
||||||
}
|
|
||||||
final out = localConversationRecord
|
|
||||||
// ignore: prefer_expression_function_bodies
|
|
||||||
.deleteScope((localConversation) async {
|
|
||||||
// Make messages log
|
|
||||||
return _initLocalMessages(
|
|
||||||
activeAccountInfo: _activeAccountInfo,
|
|
||||||
remoteIdentityPublicKey: _remoteIdentityPublicKey,
|
|
||||||
localConversationKey: localConversation.key,
|
|
||||||
callback: (messages) async {
|
|
||||||
// Create initial local conversation key contents
|
|
||||||
final conversation = proto.Conversation()
|
|
||||||
..profile = profile
|
|
||||||
..superIdentityJson = jsonEncode(
|
|
||||||
_activeAccountInfo.localAccount.superIdentity.toJson())
|
|
||||||
..messages = messages.recordKey.toProto();
|
|
||||||
|
|
||||||
// Write initial conversation to record
|
|
||||||
final update = await localConversation.tryWriteProtobuf(
|
|
||||||
proto.Conversation.fromBuffer, conversation);
|
|
||||||
if (update != null) {
|
|
||||||
throw Exception('Failed to write local conversation');
|
|
||||||
}
|
|
||||||
final out = await callback(localConversation);
|
|
||||||
|
|
||||||
// Upon success emit the local conversation record to the state
|
|
||||||
_updateLocalConversationState(AsyncValue.data(conversation));
|
|
||||||
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// If success, save the new local conversation record key in this object
|
|
||||||
_localConversationRecordKey = localConversationRecord.key;
|
|
||||||
await _setLocalConversation(() async => localConversationRecord);
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize local messages
|
// Initialize local messages
|
||||||
Future<T> _initLocalMessages<T>({
|
Future<T> _initLocalMessages<T>({
|
||||||
required ActiveAccountInfo activeAccountInfo,
|
|
||||||
required TypedKey remoteIdentityPublicKey,
|
|
||||||
required TypedKey localConversationKey,
|
required TypedKey localConversationKey,
|
||||||
required FutureOr<T> Function(DHTLog) callback,
|
required FutureOr<T> Function(DHTLog) callback,
|
||||||
}) async {
|
}) async {
|
||||||
final crypto =
|
final crypto = await _cachedConversationCrypto();
|
||||||
await activeAccountInfo.makeConversationCrypto(remoteIdentityPublicKey);
|
final writer = _identityWriter;
|
||||||
final writer = activeAccountInfo.identityWriter;
|
|
||||||
|
|
||||||
return (await DHTLog.create(
|
return (await DHTLog.create(
|
||||||
debugName: 'ConversationCubit::initLocalMessages::LocalMessages',
|
debugName: 'ConversationCubit::initLocalMessages::LocalMessages',
|
||||||
|
|
@ -299,47 +298,23 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||||
.deleteScope((messages) async => await callback(messages));
|
.deleteScope((messages) async => await callback(messages));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force refresh of conversation keys
|
|
||||||
Future<void> refresh() async {
|
|
||||||
await _initWait();
|
|
||||||
|
|
||||||
final lcc = _localConversationCubit;
|
|
||||||
final rcc = _remoteConversationCubit;
|
|
||||||
|
|
||||||
if (lcc != null) {
|
|
||||||
await lcc.refreshDefault();
|
|
||||||
}
|
|
||||||
if (rcc != null) {
|
|
||||||
await rcc.refreshDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<proto.Conversation?> writeLocalConversation({
|
|
||||||
required proto.Conversation conversation,
|
|
||||||
}) async {
|
|
||||||
final update = await _localConversationCubit!.record
|
|
||||||
.tryWriteProtobuf(proto.Conversation.fromBuffer, conversation);
|
|
||||||
|
|
||||||
if (update != null) {
|
|
||||||
_updateLocalConversationState(AsyncValue.data(conversation));
|
|
||||||
}
|
|
||||||
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<VeilidCrypto> _cachedConversationCrypto() async {
|
Future<VeilidCrypto> _cachedConversationCrypto() async {
|
||||||
var conversationCrypto = _conversationCrypto;
|
var conversationCrypto = _conversationCrypto;
|
||||||
if (conversationCrypto != null) {
|
if (conversationCrypto != null) {
|
||||||
return conversationCrypto;
|
return conversationCrypto;
|
||||||
}
|
}
|
||||||
conversationCrypto = await _activeAccountInfo
|
conversationCrypto =
|
||||||
.makeConversationCrypto(_remoteIdentityPublicKey);
|
await _accountInfo.makeConversationCrypto(_remoteIdentityPublicKey);
|
||||||
|
|
||||||
_conversationCrypto = conversationCrypto;
|
_conversationCrypto = conversationCrypto;
|
||||||
return conversationCrypto;
|
return conversationCrypto;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ActiveAccountInfo _activeAccountInfo;
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fields
|
||||||
|
TypedKey get remoteIdentityPublicKey => _remoteIdentityPublicKey;
|
||||||
|
|
||||||
|
final AccountInfo _accountInfo;
|
||||||
|
late final KeyPair _identityWriter;
|
||||||
final TypedKey _remoteIdentityPublicKey;
|
final TypedKey _remoteIdentityPublicKey;
|
||||||
TypedKey? _localConversationRecordKey;
|
TypedKey? _localConversationRecordKey;
|
||||||
final TypedKey? _remoteConversationRecordKey;
|
final TypedKey? _remoteConversationRecordKey;
|
||||||
|
|
@ -347,9 +322,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||||
DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit;
|
DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit;
|
||||||
StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription;
|
StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription;
|
||||||
StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription;
|
StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription;
|
||||||
|
StreamSubscription<AsyncValue<proto.Account>>? _accountSubscription;
|
||||||
ConversationState _incrementalState = const ConversationState(
|
ConversationState _incrementalState = const ConversationState(
|
||||||
localConversation: null, remoteConversation: null);
|
localConversation: null, remoteConversation: null);
|
||||||
//
|
|
||||||
VeilidCrypto? _conversationCrypto;
|
VeilidCrypto? _conversationCrypto;
|
||||||
final WaitSet<void> _initWait = WaitSet();
|
final WaitSet<void> _initWait = WaitSet();
|
||||||
}
|
}
|
||||||
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_invalid.dart';
|
||||||
export 'home_account_locked.dart';
|
export 'home_account_locked.dart';
|
||||||
export 'home_account_missing.dart';
|
export 'home_account_missing.dart';
|
||||||
export 'home_account_ready/home_account_ready.dart';
|
export 'home_account_ready/home_account_ready.dart';
|
||||||
export 'home_no_active.dart';
|
export 'home_no_active.dart';
|
||||||
export 'home_shell.dart';
|
export 'home_screen.dart';
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,3 @@ class HomeAccountMissingState extends State<HomeAccountMissing> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => const Text('Account missing');
|
Widget build(BuildContext context) => const Text('Account missing');
|
||||||
}
|
}
|
||||||
|
|
||||||
// xxx click to delete missing account or add to postframecallback
|
|
||||||
// Future.delayed(0.ms, () async {
|
|
||||||
// await showErrorModal(context, translate('home.missing_account_title'),
|
|
||||||
// translate('home.missing_account_text'));
|
|
||||||
// // Delete account
|
|
||||||
// await AccountRepository.instance.deleteLocalAccount(activeUserLogin);
|
|
||||||
// // Switch to no active user login
|
|
||||||
// await AccountRepository.instance.switchToAccount(null);
|
|
||||||
// });
|
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
export 'home_account_ready_chat.dart';
|
export 'home_account_ready_chat.dart';
|
||||||
export 'home_account_ready_main.dart';
|
export 'home_account_ready_main.dart';
|
||||||
export 'home_account_ready_shell.dart';
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ import 'package:awesome_extensions/awesome_extensions.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
|
||||||
|
|
||||||
import '../../../account_manager/account_manager.dart';
|
import '../../../account_manager/account_manager.dart';
|
||||||
import '../../../chat/chat.dart';
|
import '../../../chat/chat.dart';
|
||||||
|
import '../../../proto/proto.dart' as proto;
|
||||||
import '../../../theme/theme.dart';
|
import '../../../theme/theme.dart';
|
||||||
import '../../../tools/tools.dart';
|
import '../../../tools/tools.dart';
|
||||||
import 'main_pager/main_pager.dart';
|
import 'main_pager/main_pager.dart';
|
||||||
|
|
@ -29,14 +30,15 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildUserPanel() => Builder(builder: (context) {
|
Widget buildUserPanel() => Builder(builder: (context) {
|
||||||
final account = context.watch<AccountRecordCubit>().state;
|
final profile = context.select<AccountRecordCubit, proto.Profile>(
|
||||||
|
(c) => c.state.asData!.value.profile);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final scale = theme.extension<ScaleScheme>()!;
|
final scale = theme.extension<ScaleScheme>()!;
|
||||||
|
|
||||||
return Column(children: <Widget>[
|
return Column(children: <Widget>[
|
||||||
Row(children: [
|
Row(children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.settings),
|
icon: const Icon(Icons.menu),
|
||||||
color: scale.secondaryScale.borderText,
|
color: scale.secondaryScale.borderText,
|
||||||
constraints: const BoxConstraints.expand(height: 64, width: 64),
|
constraints: const BoxConstraints.expand(height: 64, width: 64),
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
|
|
@ -44,13 +46,13 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
|
||||||
WidgetStateProperty.all(scale.primaryScale.hoverBorder),
|
WidgetStateProperty.all(scale.primaryScale.hoverBorder),
|
||||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16))))),
|
borderRadius: BorderRadius.all(Radius.circular(16))))),
|
||||||
tooltip: translate('app_bar.settings_tooltip'),
|
tooltip: translate('menu.settings_tooltip'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await GoRouterHelper(context).push('/settings');
|
final ctrl = context.read<ZoomDrawerController>();
|
||||||
|
await ctrl.toggle?.call();
|
||||||
|
//await GoRouterHelper(context).push('/settings');
|
||||||
}).paddingLTRB(0, 0, 8, 0),
|
}).paddingLTRB(0, 0, 8, 0),
|
||||||
asyncValueBuilder(account,
|
ProfileWidget(profile: profile).expanded(),
|
||||||
(_, account) => ProfileWidget(profile: account.profile))
|
|
||||||
.expanded(),
|
|
||||||
]).paddingAll(8),
|
]).paddingAll(8),
|
||||||
const MainPager().expanded()
|
const MainPager().expanded()
|
||||||
]);
|
]);
|
||||||
|
|
@ -70,8 +72,8 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
|
||||||
return const NoConversationWidget();
|
return const NoConversationWidget();
|
||||||
}
|
}
|
||||||
return ChatComponentWidget.builder(
|
return ChatComponentWidget.builder(
|
||||||
localConversationRecordKey: activeChatLocalConversationKey,
|
localConversationRecordKey: activeChatLocalConversationKey,
|
||||||
);
|
key: ValueKey(activeChatLocalConversationKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore: prefer_expression_function_bodies
|
// 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
|
// ignore: prefer_expression_function_bodies
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(children: <Widget>[
|
return Column(children: <Widget>[
|
||||||
const ChatSingleContactListWidget().expanded(),
|
const ChatListWidget().expanded(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:preload_page_view/preload_page_view.dart';
|
import 'package:preload_page_view/preload_page_view.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
|
import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
|
||||||
|
|
||||||
import '../../../../chat/chat.dart';
|
import '../../../../chat/chat.dart';
|
||||||
|
|
@ -117,7 +118,7 @@ class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
|
||||||
style: TextStyle(fontSize: 24),
|
style: TextStyle(fontSize: 24),
|
||||||
),
|
),
|
||||||
content: ScanInvitationDialog(
|
content: ScanInvitationDialog(
|
||||||
modalContext: context,
|
locator: context.read,
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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/material.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
|
|
||||||
import 'package:stack_trace/stack_trace.dart';
|
import 'package:stack_trace/stack_trace.dart';
|
||||||
|
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
|
|
@ -45,6 +46,9 @@ void main() async {
|
||||||
fallbackLocale: 'en_US', supportedLocales: ['en_US']);
|
fallbackLocale: 'en_US', supportedLocales: ['en_US']);
|
||||||
await initializeDateFormatting();
|
await initializeDateFormatting();
|
||||||
|
|
||||||
|
// Get package info
|
||||||
|
await initPackageInfo();
|
||||||
|
|
||||||
// Run the app
|
// Run the app
|
||||||
// Hot reloads will only restart this part, not Veilid
|
// Hot reloads will only restart this part, not Veilid
|
||||||
runApp(LocalizedApp(localizationDelegate,
|
runApp(LocalizedApp(localizationDelegate,
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,21 @@ extension MessageExt on proto.Message {
|
||||||
static int compareTimestamp(proto.Message a, proto.Message b) =>
|
static int compareTimestamp(proto.Message a, proto.Message b) =>
|
||||||
a.timestamp.compareTo(b.timestamp);
|
a.timestamp.compareTo(b.timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ContactExt on proto.Contact {
|
||||||
|
String get displayName =>
|
||||||
|
nickname.isNotEmpty ? '$nickname (${profile.name})' : profile.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ChatExt on proto.Chat {
|
||||||
|
TypedKey get localConversationRecordKey {
|
||||||
|
switch (whichKind()) {
|
||||||
|
case proto.Chat_Kind.direct:
|
||||||
|
return direct.localConversationRecordKey.toVeilid();
|
||||||
|
case proto.Chat_Kind.group:
|
||||||
|
return group.localConversationRecordKey.toVeilid();
|
||||||
|
case proto.Chat_Kind.notSet:
|
||||||
|
throw StateError('unknown chat kind');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,177 @@ import 'veilidchat.pbenum.dart';
|
||||||
|
|
||||||
export 'veilidchat.pbenum.dart';
|
export 'veilidchat.pbenum.dart';
|
||||||
|
|
||||||
|
class DHTDataReference extends $pb.GeneratedMessage {
|
||||||
|
factory DHTDataReference() => create();
|
||||||
|
DHTDataReference._() : super();
|
||||||
|
factory DHTDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory DHTDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DHTDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
||||||
|
..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: $0.TypedKey.create)
|
||||||
|
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'hash', subBuilder: $0.TypedKey.create)
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
DHTDataReference clone() => DHTDataReference()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
DHTDataReference copyWith(void Function(DHTDataReference) updates) => super.copyWith((message) => updates(message as DHTDataReference)) as DHTDataReference;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static DHTDataReference create() => DHTDataReference._();
|
||||||
|
DHTDataReference createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<DHTDataReference> createRepeated() => $pb.PbList<DHTDataReference>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static DHTDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DHTDataReference>(create);
|
||||||
|
static DHTDataReference? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$0.TypedKey get dhtData => $_getN(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set dhtData($0.TypedKey v) { setField(1, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasDhtData() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearDhtData() => clearField(1);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$0.TypedKey ensureDhtData() => $_ensure(0);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$0.TypedKey get hash => $_getN(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set hash($0.TypedKey v) { setField(2, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasHash() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearHash() => clearField(2);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$0.TypedKey ensureHash() => $_ensure(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlockStoreDataReference extends $pb.GeneratedMessage {
|
||||||
|
factory BlockStoreDataReference() => create();
|
||||||
|
BlockStoreDataReference._() : super();
|
||||||
|
factory BlockStoreDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory BlockStoreDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BlockStoreDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
||||||
|
..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'block', subBuilder: $0.TypedKey.create)
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
BlockStoreDataReference clone() => BlockStoreDataReference()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
BlockStoreDataReference copyWith(void Function(BlockStoreDataReference) updates) => super.copyWith((message) => updates(message as BlockStoreDataReference)) as BlockStoreDataReference;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static BlockStoreDataReference create() => BlockStoreDataReference._();
|
||||||
|
BlockStoreDataReference createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<BlockStoreDataReference> createRepeated() => $pb.PbList<BlockStoreDataReference>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static BlockStoreDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BlockStoreDataReference>(create);
|
||||||
|
static BlockStoreDataReference? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$0.TypedKey get block => $_getN(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set block($0.TypedKey v) { setField(1, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasBlock() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearBlock() => clearField(1);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$0.TypedKey ensureBlock() => $_ensure(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DataReference_Kind {
|
||||||
|
dhtData,
|
||||||
|
blockStoreData,
|
||||||
|
notSet
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataReference extends $pb.GeneratedMessage {
|
||||||
|
factory DataReference() => create();
|
||||||
|
DataReference._() : super();
|
||||||
|
factory DataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory DataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static const $core.Map<$core.int, DataReference_Kind> _DataReference_KindByTag = {
|
||||||
|
1 : DataReference_Kind.dhtData,
|
||||||
|
2 : DataReference_Kind.blockStoreData,
|
||||||
|
0 : DataReference_Kind.notSet
|
||||||
|
};
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
||||||
|
..oo(0, [1, 2])
|
||||||
|
..aOM<DHTDataReference>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: DHTDataReference.create)
|
||||||
|
..aOM<BlockStoreDataReference>(2, _omitFieldNames ? '' : 'blockStoreData', subBuilder: BlockStoreDataReference.create)
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
DataReference clone() => DataReference()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
DataReference copyWith(void Function(DataReference) updates) => super.copyWith((message) => updates(message as DataReference)) as DataReference;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static DataReference create() => DataReference._();
|
||||||
|
DataReference createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<DataReference> createRepeated() => $pb.PbList<DataReference>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static DataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DataReference>(create);
|
||||||
|
static DataReference? _defaultInstance;
|
||||||
|
|
||||||
|
DataReference_Kind whichKind() => _DataReference_KindByTag[$_whichOneof(0)]!;
|
||||||
|
void clearKind() => clearField($_whichOneof(0));
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
DHTDataReference get dhtData => $_getN(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set dhtData(DHTDataReference v) { setField(1, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasDhtData() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearDhtData() => clearField(1);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
DHTDataReference ensureDhtData() => $_ensure(0);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
BlockStoreDataReference get blockStoreData => $_getN(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set blockStoreData(BlockStoreDataReference v) { setField(2, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasBlockStoreData() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearBlockStoreData() => clearField(2);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
BlockStoreDataReference ensureBlockStoreData() => $_ensure(1);
|
||||||
|
}
|
||||||
|
|
||||||
enum Attachment_Kind {
|
enum Attachment_Kind {
|
||||||
media,
|
media,
|
||||||
notSet
|
notSet
|
||||||
|
|
@ -98,7 +269,7 @@ class AttachmentMedia extends $pb.GeneratedMessage {
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AttachmentMedia', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AttachmentMedia', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
||||||
..aOS(1, _omitFieldNames ? '' : 'mime')
|
..aOS(1, _omitFieldNames ? '' : 'mime')
|
||||||
..aOS(2, _omitFieldNames ? '' : 'name')
|
..aOS(2, _omitFieldNames ? '' : 'name')
|
||||||
..aOM<$1.DataReference>(3, _omitFieldNames ? '' : 'content', subBuilder: $1.DataReference.create)
|
..aOM<DataReference>(3, _omitFieldNames ? '' : 'content', subBuilder: DataReference.create)
|
||||||
..hasRequiredFields = false
|
..hasRequiredFields = false
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -142,15 +313,15 @@ class AttachmentMedia extends $pb.GeneratedMessage {
|
||||||
void clearName() => clearField(2);
|
void clearName() => clearField(2);
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$1.DataReference get content => $_getN(2);
|
DataReference get content => $_getN(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
set content($1.DataReference v) { setField(3, v); }
|
set content(DataReference v) { setField(3, v); }
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$core.bool hasContent() => $_has(2);
|
$core.bool hasContent() => $_has(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
void clearContent() => clearField(3);
|
void clearContent() => clearField(3);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$1.DataReference ensureContent() => $_ensure(2);
|
DataReference ensureContent() => $_ensure(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Permissions extends $pb.GeneratedMessage {
|
class Permissions extends $pb.GeneratedMessage {
|
||||||
|
|
@ -276,7 +447,7 @@ class ChatSettings extends $pb.GeneratedMessage {
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatSettings', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatSettings', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
||||||
..aOS(1, _omitFieldNames ? '' : 'title')
|
..aOS(1, _omitFieldNames ? '' : 'title')
|
||||||
..aOS(2, _omitFieldNames ? '' : 'description')
|
..aOS(2, _omitFieldNames ? '' : 'description')
|
||||||
..aOM<$1.DataReference>(3, _omitFieldNames ? '' : 'icon', subBuilder: $1.DataReference.create)
|
..aOM<DataReference>(3, _omitFieldNames ? '' : 'icon', subBuilder: DataReference.create)
|
||||||
..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'defaultExpiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
|
..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'defaultExpiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
|
||||||
..hasRequiredFields = false
|
..hasRequiredFields = false
|
||||||
;
|
;
|
||||||
|
|
@ -321,15 +492,15 @@ class ChatSettings extends $pb.GeneratedMessage {
|
||||||
void clearDescription() => clearField(2);
|
void clearDescription() => clearField(2);
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$1.DataReference get icon => $_getN(2);
|
DataReference get icon => $_getN(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
set icon($1.DataReference v) { setField(3, v); }
|
set icon(DataReference v) { setField(3, v); }
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$core.bool hasIcon() => $_has(2);
|
$core.bool hasIcon() => $_has(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
void clearIcon() => clearField(3);
|
void clearIcon() => clearField(3);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$1.DataReference ensureIcon() => $_ensure(2);
|
DataReference ensureIcon() => $_ensure(2);
|
||||||
|
|
||||||
@$pb.TagNumber(4)
|
@$pb.TagNumber(4)
|
||||||
$fixnum.Int64 get defaultExpiration => $_getI64(3);
|
$fixnum.Int64 get defaultExpiration => $_getI64(3);
|
||||||
|
|
@ -1084,16 +1255,15 @@ class Conversation extends $pb.GeneratedMessage {
|
||||||
$0.TypedKey ensureMessages() => $_ensure(2);
|
$0.TypedKey ensureMessages() => $_ensure(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Chat extends $pb.GeneratedMessage {
|
class ChatMember extends $pb.GeneratedMessage {
|
||||||
factory Chat() => create();
|
factory ChatMember() => create();
|
||||||
Chat._() : super();
|
ChatMember._() : super();
|
||||||
factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
factory ChatMember.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
factory ChatMember.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatMember', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
||||||
..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create)
|
..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'remoteIdentityPublicKey', subBuilder: $0.TypedKey.create)
|
||||||
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
|
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
|
||||||
..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
|
|
||||||
..hasRequiredFields = false
|
..hasRequiredFields = false
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -1101,22 +1271,79 @@ class Chat extends $pb.GeneratedMessage {
|
||||||
'Using this can add significant overhead to your binary. '
|
'Using this can add significant overhead to your binary. '
|
||||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
'Will be removed in next major version')
|
'Will be removed in next major version')
|
||||||
Chat clone() => Chat()..mergeFromMessage(this);
|
ChatMember clone() => ChatMember()..mergeFromMessage(this);
|
||||||
@$core.Deprecated(
|
@$core.Deprecated(
|
||||||
'Using this can add significant overhead to your binary. '
|
'Using this can add significant overhead to your binary. '
|
||||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
'Will be removed in next major version')
|
'Will be removed in next major version')
|
||||||
Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat;
|
ChatMember copyWith(void Function(ChatMember) updates) => super.copyWith((message) => updates(message as ChatMember)) as ChatMember;
|
||||||
|
|
||||||
$pb.BuilderInfo get info_ => _i;
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
@$core.pragma('dart2js:noInline')
|
@$core.pragma('dart2js:noInline')
|
||||||
static Chat create() => Chat._();
|
static ChatMember create() => ChatMember._();
|
||||||
Chat createEmptyInstance() => create();
|
ChatMember createEmptyInstance() => create();
|
||||||
static $pb.PbList<Chat> createRepeated() => $pb.PbList<Chat>();
|
static $pb.PbList<ChatMember> createRepeated() => $pb.PbList<ChatMember>();
|
||||||
@$core.pragma('dart2js:noInline')
|
@$core.pragma('dart2js:noInline')
|
||||||
static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Chat>(create);
|
static ChatMember getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ChatMember>(create);
|
||||||
static Chat? _defaultInstance;
|
static ChatMember? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$0.TypedKey get remoteIdentityPublicKey => $_getN(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set remoteIdentityPublicKey($0.TypedKey v) { setField(1, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasRemoteIdentityPublicKey() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearRemoteIdentityPublicKey() => clearField(1);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$0.TypedKey ensureRemoteIdentityPublicKey() => $_ensure(0);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$0.TypedKey get remoteConversationRecordKey => $_getN(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set remoteConversationRecordKey($0.TypedKey v) { setField(2, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasRemoteConversationRecordKey() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearRemoteConversationRecordKey() => clearField(2);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectChat extends $pb.GeneratedMessage {
|
||||||
|
factory DirectChat() => create();
|
||||||
|
DirectChat._() : super();
|
||||||
|
factory DirectChat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory DirectChat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DirectChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
||||||
|
..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create)
|
||||||
|
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
|
||||||
|
..aOM<ChatMember>(3, _omitFieldNames ? '' : 'remoteMember', subBuilder: ChatMember.create)
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
DirectChat clone() => DirectChat()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
DirectChat copyWith(void Function(DirectChat) updates) => super.copyWith((message) => updates(message as DirectChat)) as DirectChat;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static DirectChat create() => DirectChat._();
|
||||||
|
DirectChat createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<DirectChat> createRepeated() => $pb.PbList<DirectChat>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static DirectChat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DirectChat>(create);
|
||||||
|
static DirectChat? _defaultInstance;
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
ChatSettings get settings => $_getN(0);
|
ChatSettings get settings => $_getN(0);
|
||||||
|
|
@ -1141,15 +1368,15 @@ class Chat extends $pb.GeneratedMessage {
|
||||||
$0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1);
|
$0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1);
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$0.TypedKey get remoteConversationRecordKey => $_getN(2);
|
ChatMember get remoteMember => $_getN(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
set remoteConversationRecordKey($0.TypedKey v) { setField(3, v); }
|
set remoteMember(ChatMember v) { setField(3, v); }
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$core.bool hasRemoteConversationRecordKey() => $_has(2);
|
$core.bool hasRemoteMember() => $_has(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
void clearRemoteConversationRecordKey() => clearField(3);
|
void clearRemoteMember() => clearField(3);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2);
|
ChatMember ensureRemoteMember() => $_ensure(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
class GroupChat extends $pb.GeneratedMessage {
|
class GroupChat extends $pb.GeneratedMessage {
|
||||||
|
|
@ -1160,8 +1387,10 @@ class GroupChat extends $pb.GeneratedMessage {
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GroupChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GroupChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
||||||
..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create)
|
..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create)
|
||||||
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
|
..aOM<Membership>(2, _omitFieldNames ? '' : 'membership', subBuilder: Membership.create)
|
||||||
..pc<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKeys', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create)
|
..aOM<Permissions>(3, _omitFieldNames ? '' : 'permissions', subBuilder: Permissions.create)
|
||||||
|
..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
|
||||||
|
..pc<ChatMember>(5, _omitFieldNames ? '' : 'remoteMembers', $pb.PbFieldType.PM, subBuilder: ChatMember.create)
|
||||||
..hasRequiredFields = false
|
..hasRequiredFields = false
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -1198,18 +1427,111 @@ class GroupChat extends $pb.GeneratedMessage {
|
||||||
ChatSettings ensureSettings() => $_ensure(0);
|
ChatSettings ensureSettings() => $_ensure(0);
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$0.TypedKey get localConversationRecordKey => $_getN(1);
|
Membership get membership => $_getN(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
set localConversationRecordKey($0.TypedKey v) { setField(2, v); }
|
set membership(Membership v) { setField(2, v); }
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$core.bool hasLocalConversationRecordKey() => $_has(1);
|
$core.bool hasMembership() => $_has(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
void clearLocalConversationRecordKey() => clearField(2);
|
void clearMembership() => clearField(2);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1);
|
Membership ensureMembership() => $_ensure(1);
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$core.List<$0.TypedKey> get remoteConversationRecordKeys => $_getList(2);
|
Permissions get permissions => $_getN(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
set permissions(Permissions v) { setField(3, v); }
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.bool hasPermissions() => $_has(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
void clearPermissions() => clearField(3);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
Permissions ensurePermissions() => $_ensure(2);
|
||||||
|
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$0.TypedKey get localConversationRecordKey => $_getN(3);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
set localConversationRecordKey($0.TypedKey v) { setField(4, v); }
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$core.bool hasLocalConversationRecordKey() => $_has(3);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
void clearLocalConversationRecordKey() => clearField(4);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$0.TypedKey ensureLocalConversationRecordKey() => $_ensure(3);
|
||||||
|
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
$core.List<ChatMember> get remoteMembers => $_getList(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Chat_Kind {
|
||||||
|
direct,
|
||||||
|
group,
|
||||||
|
notSet
|
||||||
|
}
|
||||||
|
|
||||||
|
class Chat extends $pb.GeneratedMessage {
|
||||||
|
factory Chat() => create();
|
||||||
|
Chat._() : super();
|
||||||
|
factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static const $core.Map<$core.int, Chat_Kind> _Chat_KindByTag = {
|
||||||
|
1 : Chat_Kind.direct,
|
||||||
|
2 : Chat_Kind.group,
|
||||||
|
0 : Chat_Kind.notSet
|
||||||
|
};
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
||||||
|
..oo(0, [1, 2])
|
||||||
|
..aOM<DirectChat>(1, _omitFieldNames ? '' : 'direct', subBuilder: DirectChat.create)
|
||||||
|
..aOM<GroupChat>(2, _omitFieldNames ? '' : 'group', subBuilder: GroupChat.create)
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
Chat clone() => Chat()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static Chat create() => Chat._();
|
||||||
|
Chat createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<Chat> createRepeated() => $pb.PbList<Chat>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Chat>(create);
|
||||||
|
static Chat? _defaultInstance;
|
||||||
|
|
||||||
|
Chat_Kind whichKind() => _Chat_KindByTag[$_whichOneof(0)]!;
|
||||||
|
void clearKind() => clearField($_whichOneof(0));
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
DirectChat get direct => $_getN(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set direct(DirectChat v) { setField(1, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasDirect() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearDirect() => clearField(1);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
DirectChat ensureDirect() => $_ensure(0);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
GroupChat get group => $_getN(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set group(GroupChat v) { setField(2, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasGroup() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearGroup() => clearField(2);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
GroupChat ensureGroup() => $_ensure(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Profile extends $pb.GeneratedMessage {
|
class Profile extends $pb.GeneratedMessage {
|
||||||
|
|
@ -1224,7 +1546,8 @@ class Profile extends $pb.GeneratedMessage {
|
||||||
..aOS(3, _omitFieldNames ? '' : 'about')
|
..aOS(3, _omitFieldNames ? '' : 'about')
|
||||||
..aOS(4, _omitFieldNames ? '' : 'status')
|
..aOS(4, _omitFieldNames ? '' : 'status')
|
||||||
..e<Availability>(5, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values)
|
..e<Availability>(5, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values)
|
||||||
..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'avatar', subBuilder: $0.TypedKey.create)
|
..aOM<DataReference>(6, _omitFieldNames ? '' : 'avatar', subBuilder: DataReference.create)
|
||||||
|
..a<$fixnum.Int64>(7, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
|
||||||
..hasRequiredFields = false
|
..hasRequiredFields = false
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -1295,15 +1618,24 @@ class Profile extends $pb.GeneratedMessage {
|
||||||
void clearAvailability() => clearField(5);
|
void clearAvailability() => clearField(5);
|
||||||
|
|
||||||
@$pb.TagNumber(6)
|
@$pb.TagNumber(6)
|
||||||
$0.TypedKey get avatar => $_getN(5);
|
DataReference get avatar => $_getN(5);
|
||||||
@$pb.TagNumber(6)
|
@$pb.TagNumber(6)
|
||||||
set avatar($0.TypedKey v) { setField(6, v); }
|
set avatar(DataReference v) { setField(6, v); }
|
||||||
@$pb.TagNumber(6)
|
@$pb.TagNumber(6)
|
||||||
$core.bool hasAvatar() => $_has(5);
|
$core.bool hasAvatar() => $_has(5);
|
||||||
@$pb.TagNumber(6)
|
@$pb.TagNumber(6)
|
||||||
void clearAvatar() => clearField(6);
|
void clearAvatar() => clearField(6);
|
||||||
@$pb.TagNumber(6)
|
@$pb.TagNumber(6)
|
||||||
$0.TypedKey ensureAvatar() => $_ensure(5);
|
DataReference ensureAvatar() => $_ensure(5);
|
||||||
|
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
$fixnum.Int64 get timestamp => $_getI64(6);
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
set timestamp($fixnum.Int64 v) { $_setInt64(6, v); }
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
$core.bool hasTimestamp() => $_has(6);
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
void clearTimestamp() => clearField(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Account extends $pb.GeneratedMessage {
|
class Account extends $pb.GeneratedMessage {
|
||||||
|
|
@ -1425,13 +1757,14 @@ class Contact extends $pb.GeneratedMessage {
|
||||||
factory Contact.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
factory Contact.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Contact', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Contact', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
|
||||||
..aOM<Profile>(1, _omitFieldNames ? '' : 'editedProfile', subBuilder: Profile.create)
|
..aOS(1, _omitFieldNames ? '' : 'nickname')
|
||||||
..aOM<Profile>(2, _omitFieldNames ? '' : 'remoteProfile', subBuilder: Profile.create)
|
..aOM<Profile>(2, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create)
|
||||||
..aOS(3, _omitFieldNames ? '' : 'superIdentityJson')
|
..aOS(3, _omitFieldNames ? '' : 'superIdentityJson')
|
||||||
..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $0.TypedKey.create)
|
..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $0.TypedKey.create)
|
||||||
..aOM<$0.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
|
..aOM<$0.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
|
||||||
..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
|
..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
|
||||||
..aOB(7, _omitFieldNames ? '' : 'showAvailability')
|
..aOB(7, _omitFieldNames ? '' : 'showAvailability')
|
||||||
|
..aOS(8, _omitFieldNames ? '' : 'notes')
|
||||||
..hasRequiredFields = false
|
..hasRequiredFields = false
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -1457,26 +1790,24 @@ class Contact extends $pb.GeneratedMessage {
|
||||||
static Contact? _defaultInstance;
|
static Contact? _defaultInstance;
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
Profile get editedProfile => $_getN(0);
|
$core.String get nickname => $_getSZ(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
set editedProfile(Profile v) { setField(1, v); }
|
set nickname($core.String v) { $_setString(0, v); }
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
$core.bool hasEditedProfile() => $_has(0);
|
$core.bool hasNickname() => $_has(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
void clearEditedProfile() => clearField(1);
|
void clearNickname() => clearField(1);
|
||||||
@$pb.TagNumber(1)
|
|
||||||
Profile ensureEditedProfile() => $_ensure(0);
|
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
Profile get remoteProfile => $_getN(1);
|
Profile get profile => $_getN(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
set remoteProfile(Profile v) { setField(2, v); }
|
set profile(Profile v) { setField(2, v); }
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$core.bool hasRemoteProfile() => $_has(1);
|
$core.bool hasProfile() => $_has(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
void clearRemoteProfile() => clearField(2);
|
void clearProfile() => clearField(2);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
Profile ensureRemoteProfile() => $_ensure(1);
|
Profile ensureProfile() => $_ensure(1);
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$core.String get superIdentityJson => $_getSZ(2);
|
$core.String get superIdentityJson => $_getSZ(2);
|
||||||
|
|
@ -1528,6 +1859,15 @@ class Contact extends $pb.GeneratedMessage {
|
||||||
$core.bool hasShowAvailability() => $_has(6);
|
$core.bool hasShowAvailability() => $_has(6);
|
||||||
@$pb.TagNumber(7)
|
@$pb.TagNumber(7)
|
||||||
void clearShowAvailability() => clearField(7);
|
void clearShowAvailability() => clearField(7);
|
||||||
|
|
||||||
|
@$pb.TagNumber(8)
|
||||||
|
$core.String get notes => $_getSZ(7);
|
||||||
|
@$pb.TagNumber(8)
|
||||||
|
set notes($core.String v) { $_setString(7, v); }
|
||||||
|
@$pb.TagNumber(8)
|
||||||
|
$core.bool hasNotes() => $_has(7);
|
||||||
|
@$pb.TagNumber(8)
|
||||||
|
void clearNotes() => clearField(8);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContactInvitation extends $pb.GeneratedMessage {
|
class ContactInvitation extends $pb.GeneratedMessage {
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,51 @@ final $typed_data.Uint8List scopeDescriptor = $convert.base64Decode(
|
||||||
'CgVTY29wZRIMCghXQVRDSEVSUxAAEg0KCU1PREVSQVRFRBABEgsKB1RBTEtFUlMQAhIOCgpNT0'
|
'CgVTY29wZRIMCghXQVRDSEVSUxAAEg0KCU1PREVSQVRFRBABEgsKB1RBTEtFUlMQAhIOCgpNT0'
|
||||||
'RFUkFUT1JTEAMSCgoGQURNSU5TEAQ=');
|
'RFUkFUT1JTEAMSCgoGQURNSU5TEAQ=');
|
||||||
|
|
||||||
|
@$core.Deprecated('Use dHTDataReferenceDescriptor instead')
|
||||||
|
const DHTDataReference$json = {
|
||||||
|
'1': 'DHTDataReference',
|
||||||
|
'2': [
|
||||||
|
{'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'dhtData'},
|
||||||
|
{'1': 'hash', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'hash'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Descriptor for `DHTDataReference`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
|
final $typed_data.Uint8List dHTDataReferenceDescriptor = $convert.base64Decode(
|
||||||
|
'ChBESFREYXRhUmVmZXJlbmNlEisKCGRodF9kYXRhGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5Ug'
|
||||||
|
'dkaHREYXRhEiQKBGhhc2gYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGhhc2g=');
|
||||||
|
|
||||||
|
@$core.Deprecated('Use blockStoreDataReferenceDescriptor instead')
|
||||||
|
const BlockStoreDataReference$json = {
|
||||||
|
'1': 'BlockStoreDataReference',
|
||||||
|
'2': [
|
||||||
|
{'1': 'block', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'block'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Descriptor for `BlockStoreDataReference`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
|
final $typed_data.Uint8List blockStoreDataReferenceDescriptor = $convert.base64Decode(
|
||||||
|
'ChdCbG9ja1N0b3JlRGF0YVJlZmVyZW5jZRImCgVibG9jaxgBIAEoCzIQLnZlaWxpZC5UeXBlZE'
|
||||||
|
'tleVIFYmxvY2s=');
|
||||||
|
|
||||||
|
@$core.Deprecated('Use dataReferenceDescriptor instead')
|
||||||
|
const DataReference$json = {
|
||||||
|
'1': 'DataReference',
|
||||||
|
'2': [
|
||||||
|
{'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.DHTDataReference', '9': 0, '10': 'dhtData'},
|
||||||
|
{'1': 'block_store_data', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.BlockStoreDataReference', '9': 0, '10': 'blockStoreData'},
|
||||||
|
],
|
||||||
|
'8': [
|
||||||
|
{'1': 'kind'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Descriptor for `DataReference`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
|
final $typed_data.Uint8List dataReferenceDescriptor = $convert.base64Decode(
|
||||||
|
'Cg1EYXRhUmVmZXJlbmNlEjkKCGRodF9kYXRhGAEgASgLMhwudmVpbGlkY2hhdC5ESFREYXRhUm'
|
||||||
|
'VmZXJlbmNlSABSB2RodERhdGESTwoQYmxvY2tfc3RvcmVfZGF0YRgCIAEoCzIjLnZlaWxpZGNo'
|
||||||
|
'YXQuQmxvY2tTdG9yZURhdGFSZWZlcmVuY2VIAFIOYmxvY2tTdG9yZURhdGFCBgoEa2luZA==');
|
||||||
|
|
||||||
@$core.Deprecated('Use attachmentDescriptor instead')
|
@$core.Deprecated('Use attachmentDescriptor instead')
|
||||||
const Attachment$json = {
|
const Attachment$json = {
|
||||||
'1': 'Attachment',
|
'1': 'Attachment',
|
||||||
|
|
@ -89,14 +134,14 @@ const AttachmentMedia$json = {
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'mime', '3': 1, '4': 1, '5': 9, '10': 'mime'},
|
{'1': 'mime', '3': 1, '4': 1, '5': 9, '10': 'mime'},
|
||||||
{'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
|
{'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
|
||||||
{'1': 'content', '3': 3, '4': 1, '5': 11, '6': '.dht.DataReference', '10': 'content'},
|
{'1': 'content', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '10': 'content'},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Descriptor for `AttachmentMedia`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `AttachmentMedia`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List attachmentMediaDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List attachmentMediaDescriptor = $convert.base64Decode(
|
||||||
'Cg9BdHRhY2htZW50TWVkaWESEgoEbWltZRgBIAEoCVIEbWltZRISCgRuYW1lGAIgASgJUgRuYW'
|
'Cg9BdHRhY2htZW50TWVkaWESEgoEbWltZRgBIAEoCVIEbWltZRISCgRuYW1lGAIgASgJUgRuYW'
|
||||||
'1lEiwKB2NvbnRlbnQYAyABKAsyEi5kaHQuRGF0YVJlZmVyZW5jZVIHY29udGVudA==');
|
'1lEjMKB2NvbnRlbnQYAyABKAsyGS52ZWlsaWRjaGF0LkRhdGFSZWZlcmVuY2VSB2NvbnRlbnQ=');
|
||||||
|
|
||||||
@$core.Deprecated('Use permissionsDescriptor instead')
|
@$core.Deprecated('Use permissionsDescriptor instead')
|
||||||
const Permissions$json = {
|
const Permissions$json = {
|
||||||
|
|
@ -140,7 +185,7 @@ const ChatSettings$json = {
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'},
|
{'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'},
|
||||||
{'1': 'description', '3': 2, '4': 1, '5': 9, '10': 'description'},
|
{'1': 'description', '3': 2, '4': 1, '5': 9, '10': 'description'},
|
||||||
{'1': 'icon', '3': 3, '4': 1, '5': 11, '6': '.dht.DataReference', '9': 0, '10': 'icon', '17': true},
|
{'1': 'icon', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '9': 0, '10': 'icon', '17': true},
|
||||||
{'1': 'default_expiration', '3': 4, '4': 1, '5': 4, '10': 'defaultExpiration'},
|
{'1': 'default_expiration', '3': 4, '4': 1, '5': 4, '10': 'defaultExpiration'},
|
||||||
],
|
],
|
||||||
'8': [
|
'8': [
|
||||||
|
|
@ -151,9 +196,9 @@ const ChatSettings$json = {
|
||||||
/// Descriptor for `ChatSettings`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `ChatSettings`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List chatSettingsDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List chatSettingsDescriptor = $convert.base64Decode(
|
||||||
'CgxDaGF0U2V0dGluZ3MSFAoFdGl0bGUYASABKAlSBXRpdGxlEiAKC2Rlc2NyaXB0aW9uGAIgAS'
|
'CgxDaGF0U2V0dGluZ3MSFAoFdGl0bGUYASABKAlSBXRpdGxlEiAKC2Rlc2NyaXB0aW9uGAIgAS'
|
||||||
'gJUgtkZXNjcmlwdGlvbhIrCgRpY29uGAMgASgLMhIuZGh0LkRhdGFSZWZlcmVuY2VIAFIEaWNv'
|
'gJUgtkZXNjcmlwdGlvbhIyCgRpY29uGAMgASgLMhkudmVpbGlkY2hhdC5EYXRhUmVmZXJlbmNl'
|
||||||
'bogBARItChJkZWZhdWx0X2V4cGlyYXRpb24YBCABKARSEWRlZmF1bHRFeHBpcmF0aW9uQgcKBV'
|
'SABSBGljb26IAQESLQoSZGVmYXVsdF9leHBpcmF0aW9uGAQgASgEUhFkZWZhdWx0RXhwaXJhdG'
|
||||||
'9pY29u');
|
'lvbkIHCgVfaWNvbg==');
|
||||||
|
|
||||||
@$core.Deprecated('Use messageDescriptor instead')
|
@$core.Deprecated('Use messageDescriptor instead')
|
||||||
const Message$json = {
|
const Message$json = {
|
||||||
|
|
@ -320,41 +365,76 @@ final $typed_data.Uint8List conversationDescriptor = $convert.base64Decode(
|
||||||
'JvZmlsZRIuChNzdXBlcl9pZGVudGl0eV9qc29uGAIgASgJUhFzdXBlcklkZW50aXR5SnNvbhIs'
|
'JvZmlsZRIuChNzdXBlcl9pZGVudGl0eV9qc29uGAIgASgJUhFzdXBlcklkZW50aXR5SnNvbhIs'
|
||||||
'CghtZXNzYWdlcxgDIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIIbWVzc2FnZXM=');
|
'CghtZXNzYWdlcxgDIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIIbWVzc2FnZXM=');
|
||||||
|
|
||||||
@$core.Deprecated('Use chatDescriptor instead')
|
@$core.Deprecated('Use chatMemberDescriptor instead')
|
||||||
const Chat$json = {
|
const ChatMember$json = {
|
||||||
'1': 'Chat',
|
'1': 'ChatMember',
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'},
|
{'1': 'remote_identity_public_key', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteIdentityPublicKey'},
|
||||||
{'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
|
{'1': 'remote_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
|
||||||
{'1': 'remote_conversation_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `ChatMember`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List chatDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List chatMemberDescriptor = $convert.base64Decode(
|
||||||
'CgRDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3NSCHNldH'
|
'CgpDaGF0TWVtYmVyEk0KGnJlbW90ZV9pZGVudGl0eV9wdWJsaWNfa2V5GAEgASgLMhAudmVpbG'
|
||||||
'RpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVpbGlkLlR5'
|
'lkLlR5cGVkS2V5UhdyZW1vdGVJZGVudGl0eVB1YmxpY0tleRJVCh5yZW1vdGVfY29udmVyc2F0'
|
||||||
'cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRJVCh5yZW1vdGVfY29udmVyc2F0aW'
|
'aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdG'
|
||||||
'9uX3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdGlv'
|
'lvblJlY29yZEtleQ==');
|
||||||
'blJlY29yZEtleQ==');
|
|
||||||
|
@$core.Deprecated('Use directChatDescriptor instead')
|
||||||
|
const DirectChat$json = {
|
||||||
|
'1': 'DirectChat',
|
||||||
|
'2': [
|
||||||
|
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'},
|
||||||
|
{'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
|
||||||
|
{'1': 'remote_member', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.ChatMember', '10': 'remoteMember'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Descriptor for `DirectChat`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
|
final $typed_data.Uint8List directChatDescriptor = $convert.base64Decode(
|
||||||
|
'CgpEaXJlY3RDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3'
|
||||||
|
'NSCHNldHRpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVp'
|
||||||
|
'bGlkLlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRI7Cg1yZW1vdGVfbWVtYm'
|
||||||
|
'VyGAMgASgLMhYudmVpbGlkY2hhdC5DaGF0TWVtYmVyUgxyZW1vdGVNZW1iZXI=');
|
||||||
|
|
||||||
@$core.Deprecated('Use groupChatDescriptor instead')
|
@$core.Deprecated('Use groupChatDescriptor instead')
|
||||||
const GroupChat$json = {
|
const GroupChat$json = {
|
||||||
'1': 'GroupChat',
|
'1': 'GroupChat',
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'},
|
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'},
|
||||||
{'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
|
{'1': 'membership', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Membership', '10': 'membership'},
|
||||||
{'1': 'remote_conversation_record_keys', '3': 3, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKeys'},
|
{'1': 'permissions', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.Permissions', '10': 'permissions'},
|
||||||
|
{'1': 'local_conversation_record_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
|
||||||
|
{'1': 'remote_members', '3': 5, '4': 3, '5': 11, '6': '.veilidchat.ChatMember', '10': 'remoteMembers'},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Descriptor for `GroupChat`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `GroupChat`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List groupChatDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List groupChatDescriptor = $convert.base64Decode(
|
||||||
'CglHcm91cENoYXQSNAoIc2V0dGluZ3MYASABKAsyGC52ZWlsaWRjaGF0LkNoYXRTZXR0aW5nc1'
|
'CglHcm91cENoYXQSNAoIc2V0dGluZ3MYASABKAsyGC52ZWlsaWRjaGF0LkNoYXRTZXR0aW5nc1'
|
||||||
'IIc2V0dGluZ3MSUwodbG9jYWxfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWls'
|
'IIc2V0dGluZ3MSNgoKbWVtYmVyc2hpcBgCIAEoCzIWLnZlaWxpZGNoYXQuTWVtYmVyc2hpcFIK'
|
||||||
'aWQuVHlwZWRLZXlSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5ElcKH3JlbW90ZV9jb252ZX'
|
'bWVtYmVyc2hpcBI5CgtwZXJtaXNzaW9ucxgDIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbn'
|
||||||
'JzYXRpb25fcmVjb3JkX2tleXMYAyADKAsyEC52ZWlsaWQuVHlwZWRLZXlSHHJlbW90ZUNvbnZl'
|
'NSC3Blcm1pc3Npb25zElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAQgASgLMhAu'
|
||||||
'cnNhdGlvblJlY29yZEtleXM=');
|
'dmVpbGlkLlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRI9Cg5yZW1vdGVfbW'
|
||||||
|
'VtYmVycxgFIAMoCzIWLnZlaWxpZGNoYXQuQ2hhdE1lbWJlclINcmVtb3RlTWVtYmVycw==');
|
||||||
|
|
||||||
|
@$core.Deprecated('Use chatDescriptor instead')
|
||||||
|
const Chat$json = {
|
||||||
|
'1': 'Chat',
|
||||||
|
'2': [
|
||||||
|
{'1': 'direct', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.DirectChat', '9': 0, '10': 'direct'},
|
||||||
|
{'1': 'group', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.GroupChat', '9': 0, '10': 'group'},
|
||||||
|
],
|
||||||
|
'8': [
|
||||||
|
{'1': 'kind'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
|
final $typed_data.Uint8List chatDescriptor = $convert.base64Decode(
|
||||||
|
'CgRDaGF0EjAKBmRpcmVjdBgBIAEoCzIWLnZlaWxpZGNoYXQuRGlyZWN0Q2hhdEgAUgZkaXJlY3'
|
||||||
|
'QSLQoFZ3JvdXAYAiABKAsyFS52ZWlsaWRjaGF0Lkdyb3VwQ2hhdEgAUgVncm91cEIGCgRraW5k');
|
||||||
|
|
||||||
@$core.Deprecated('Use profileDescriptor instead')
|
@$core.Deprecated('Use profileDescriptor instead')
|
||||||
const Profile$json = {
|
const Profile$json = {
|
||||||
|
|
@ -365,7 +445,8 @@ const Profile$json = {
|
||||||
{'1': 'about', '3': 3, '4': 1, '5': 9, '10': 'about'},
|
{'1': 'about', '3': 3, '4': 1, '5': 9, '10': 'about'},
|
||||||
{'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'},
|
{'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'},
|
||||||
{'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'},
|
{'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'},
|
||||||
{'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '9': 0, '10': 'avatar', '17': true},
|
{'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '9': 0, '10': 'avatar', '17': true},
|
||||||
|
{'1': 'timestamp', '3': 7, '4': 1, '5': 4, '10': 'timestamp'},
|
||||||
],
|
],
|
||||||
'8': [
|
'8': [
|
||||||
{'1': '_avatar'},
|
{'1': '_avatar'},
|
||||||
|
|
@ -376,9 +457,9 @@ const Profile$json = {
|
||||||
final $typed_data.Uint8List profileDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List profileDescriptor = $convert.base64Decode(
|
||||||
'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW'
|
'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW'
|
||||||
'5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp'
|
'5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp'
|
||||||
'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ei'
|
'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ej'
|
||||||
'0KBmF2YXRhchgGIAEoCzIQLnZlaWxpZC5UeXBlZEtleUgAUgZhdmF0YXKIAQFCCQoHX2F2YXRh'
|
'YKBmF2YXRhchgGIAEoCzIZLnZlaWxpZGNoYXQuRGF0YVJlZmVyZW5jZUgAUgZhdmF0YXKIAQES'
|
||||||
'cg==');
|
'HAoJdGltZXN0YW1wGAcgASgEUgl0aW1lc3RhbXBCCQoHX2F2YXRhcg==');
|
||||||
|
|
||||||
@$core.Deprecated('Use accountDescriptor instead')
|
@$core.Deprecated('Use accountDescriptor instead')
|
||||||
const Account$json = {
|
const Account$json = {
|
||||||
|
|
@ -409,27 +490,27 @@ final $typed_data.Uint8List accountDescriptor = $convert.base64Decode(
|
||||||
const Contact$json = {
|
const Contact$json = {
|
||||||
'1': 'Contact',
|
'1': 'Contact',
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'edited_profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'editedProfile'},
|
{'1': 'nickname', '3': 1, '4': 1, '5': 9, '10': 'nickname'},
|
||||||
{'1': 'remote_profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'remoteProfile'},
|
{'1': 'profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'},
|
||||||
{'1': 'super_identity_json', '3': 3, '4': 1, '5': 9, '10': 'superIdentityJson'},
|
{'1': 'super_identity_json', '3': 3, '4': 1, '5': 9, '10': 'superIdentityJson'},
|
||||||
{'1': 'identity_public_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityPublicKey'},
|
{'1': 'identity_public_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityPublicKey'},
|
||||||
{'1': 'remote_conversation_record_key', '3': 5, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
|
{'1': 'remote_conversation_record_key', '3': 5, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
|
||||||
{'1': 'local_conversation_record_key', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
|
{'1': 'local_conversation_record_key', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
|
||||||
{'1': 'show_availability', '3': 7, '4': 1, '5': 8, '10': 'showAvailability'},
|
{'1': 'show_availability', '3': 7, '4': 1, '5': 8, '10': 'showAvailability'},
|
||||||
|
{'1': 'notes', '3': 8, '4': 1, '5': 9, '10': 'notes'},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Descriptor for `Contact`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `Contact`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List contactDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List contactDescriptor = $convert.base64Decode(
|
||||||
'CgdDb250YWN0EjoKDmVkaXRlZF9wcm9maWxlGAEgASgLMhMudmVpbGlkY2hhdC5Qcm9maWxlUg'
|
'CgdDb250YWN0EhoKCG5pY2tuYW1lGAEgASgJUghuaWNrbmFtZRItCgdwcm9maWxlGAIgASgLMh'
|
||||||
'1lZGl0ZWRQcm9maWxlEjoKDnJlbW90ZV9wcm9maWxlGAIgASgLMhMudmVpbGlkY2hhdC5Qcm9m'
|
'MudmVpbGlkY2hhdC5Qcm9maWxlUgdwcm9maWxlEi4KE3N1cGVyX2lkZW50aXR5X2pzb24YAyAB'
|
||||||
'aWxlUg1yZW1vdGVQcm9maWxlEi4KE3N1cGVyX2lkZW50aXR5X2pzb24YAyABKAlSEXN1cGVySW'
|
'KAlSEXN1cGVySWRlbnRpdHlKc29uEkAKE2lkZW50aXR5X3B1YmxpY19rZXkYBCABKAsyEC52ZW'
|
||||||
'RlbnRpdHlKc29uEkAKE2lkZW50aXR5X3B1YmxpY19rZXkYBCABKAsyEC52ZWlsaWQuVHlwZWRL'
|
'lsaWQuVHlwZWRLZXlSEWlkZW50aXR5UHVibGljS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25f'
|
||||||
'ZXlSEWlkZW50aXR5UHVibGljS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleR'
|
'cmVjb3JkX2tleRgFIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUm'
|
||||||
'gFIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5ElMK'
|
'Vjb3JkS2V5ElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAYgASgLMhAudmVpbGlk'
|
||||||
'HWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAYgASgLMhAudmVpbGlkLlR5cGVkS2V5Uh'
|
'LlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRIrChFzaG93X2F2YWlsYWJpbG'
|
||||||
'psb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRIrChFzaG93X2F2YWlsYWJpbGl0eRgHIAEoCFIQ'
|
'l0eRgHIAEoCFIQc2hvd0F2YWlsYWJpbGl0eRIUCgVub3RlcxgIIAEoCVIFbm90ZXM=');
|
||||||
'c2hvd0F2YWlsYWJpbGl0eQ==');
|
|
||||||
|
|
||||||
@$core.Deprecated('Use contactInvitationDescriptor instead')
|
@$core.Deprecated('Use contactInvitationDescriptor instead')
|
||||||
const ContactInvitation$json = {
|
const ContactInvitation$json = {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,31 @@ enum Scope {
|
||||||
ADMINS = 4;
|
ADMINS = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Data
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Reference to data on the DHT
|
||||||
|
message DHTDataReference {
|
||||||
|
veilid.TypedKey dht_data = 1;
|
||||||
|
veilid.TypedKey hash = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference to data on the BlockStore
|
||||||
|
message BlockStoreDataReference {
|
||||||
|
veilid.TypedKey block = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataReference
|
||||||
|
// Pointer to data somewhere in Veilid
|
||||||
|
// Abstraction over DHTData and BlockStore
|
||||||
|
message DataReference {
|
||||||
|
oneof kind {
|
||||||
|
DHTDataReference dht_data = 1;
|
||||||
|
BlockStoreDataReference block_store_data = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Attachments
|
// Attachments
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
@ -67,10 +92,9 @@ message AttachmentMedia {
|
||||||
// Title or filename
|
// Title or filename
|
||||||
string name = 2;
|
string name = 2;
|
||||||
// Pointer to the data content
|
// Pointer to the data content
|
||||||
dht.DataReference content = 3;
|
DataReference content = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Chat room controls
|
// Chat room controls
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
@ -106,7 +130,7 @@ message ChatSettings {
|
||||||
// Description for the chat
|
// Description for the chat
|
||||||
string description = 2;
|
string description = 2;
|
||||||
// Icon for the chat
|
// Icon for the chat
|
||||||
optional dht.DataReference icon = 3;
|
optional DataReference icon = 3;
|
||||||
// Default message expiration duration (in us)
|
// Default message expiration duration (in us)
|
||||||
uint64 default_expiration = 4;
|
uint64 default_expiration = 4;
|
||||||
}
|
}
|
||||||
|
|
@ -243,15 +267,23 @@ message Conversation {
|
||||||
veilid.TypedKey messages = 3;
|
veilid.TypedKey messages = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Either a 1-1 conversation or a group chat
|
// A member of chat which may or may not be associated with a contact
|
||||||
|
message ChatMember {
|
||||||
|
// The identity public key most recently associated with the chat member
|
||||||
|
veilid.TypedKey remote_identity_public_key = 1;
|
||||||
|
// Conversation key for the other party
|
||||||
|
veilid.TypedKey remote_conversation_record_key = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A 1-1 chat
|
||||||
// Privately encrypted, this is the local user's copy of the chat
|
// Privately encrypted, this is the local user's copy of the chat
|
||||||
message Chat {
|
message DirectChat {
|
||||||
// Settings
|
// Settings
|
||||||
ChatSettings settings = 1;
|
ChatSettings settings = 1;
|
||||||
// Conversation key for this user
|
// Conversation key for this user
|
||||||
veilid.TypedKey local_conversation_record_key = 2;
|
veilid.TypedKey local_conversation_record_key = 2;
|
||||||
// Conversation key for the other party
|
// Conversation key for the other party
|
||||||
veilid.TypedKey remote_conversation_record_key = 3;
|
ChatMember remote_member = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A group chat
|
// A group chat
|
||||||
|
|
@ -259,10 +291,22 @@ message Chat {
|
||||||
message GroupChat {
|
message GroupChat {
|
||||||
// Settings
|
// Settings
|
||||||
ChatSettings settings = 1;
|
ChatSettings settings = 1;
|
||||||
|
// Membership
|
||||||
|
Membership membership = 2;
|
||||||
|
// Permissions
|
||||||
|
Permissions permissions = 3;
|
||||||
// Conversation key for this user
|
// Conversation key for this user
|
||||||
veilid.TypedKey local_conversation_record_key = 2;
|
veilid.TypedKey local_conversation_record_key = 4;
|
||||||
// Conversation keys for the other parties
|
// Conversation keys for the other parties
|
||||||
repeated veilid.TypedKey remote_conversation_record_keys = 3;
|
repeated ChatMember remote_members = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some kind of chat
|
||||||
|
message Chat {
|
||||||
|
oneof kind {
|
||||||
|
DirectChat direct = 1;
|
||||||
|
GroupChat group = 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
@ -285,8 +329,10 @@ message Profile {
|
||||||
string status = 4;
|
string status = 4;
|
||||||
// Availability
|
// Availability
|
||||||
Availability availability = 5;
|
Availability availability = 5;
|
||||||
// Avatar DHTData
|
// Avatar
|
||||||
optional veilid.TypedKey avatar = 6;
|
optional DataReference avatar = 6;
|
||||||
|
// Timestamp of last change
|
||||||
|
uint64 timestamp = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A record of an individual account
|
// A record of an individual account
|
||||||
|
|
@ -323,10 +369,10 @@ message Account {
|
||||||
//
|
//
|
||||||
// Stored in ContactList DHTList
|
// Stored in ContactList DHTList
|
||||||
message Contact {
|
message Contact {
|
||||||
// Friend's profile as locally edited
|
// Friend's nickname
|
||||||
Profile edited_profile = 1;
|
string nickname = 1;
|
||||||
// Copy of friend's profile from remote conversation
|
// Copy of friend's profile from remote conversation
|
||||||
Profile remote_profile = 2;
|
Profile profile = 2;
|
||||||
// Copy of friend's SuperIdentity in JSON from remote conversation
|
// Copy of friend's SuperIdentity in JSON from remote conversation
|
||||||
string super_identity_json = 3;
|
string super_identity_json = 3;
|
||||||
// Copy of friend's most recent identity public key from their identityMaster
|
// Copy of friend's most recent identity public key from their identityMaster
|
||||||
|
|
@ -337,6 +383,8 @@ message Contact {
|
||||||
veilid.TypedKey local_conversation_record_key = 6;
|
veilid.TypedKey local_conversation_record_key = 6;
|
||||||
// Show availability to this contact
|
// Show availability to this contact
|
||||||
bool show_availability = 7;
|
bool show_availability = 7;
|
||||||
|
// Notes about this friend
|
||||||
|
string notes = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:stream_transform/stream_transform.dart';
|
import 'package:stream_transform/stream_transform.dart';
|
||||||
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../../account_manager/account_manager.dart';
|
import '../../../account_manager/account_manager.dart';
|
||||||
import '../../layout/layout.dart';
|
import '../../layout/layout.dart';
|
||||||
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../settings/settings.dart';
|
import '../../settings/settings.dart';
|
||||||
import '../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
import '../../veilid_processor/views/developer.dart';
|
import '../../veilid_processor/views/developer.dart';
|
||||||
|
|
@ -18,13 +20,12 @@ part 'router_cubit.freezed.dart';
|
||||||
part 'router_cubit.g.dart';
|
part 'router_cubit.g.dart';
|
||||||
|
|
||||||
final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'rootNavKey');
|
final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'rootNavKey');
|
||||||
final _homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'homeNavKey');
|
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class RouterState with _$RouterState {
|
class RouterState with _$RouterState {
|
||||||
const factory RouterState(
|
const factory RouterState({
|
||||||
{required bool hasAnyAccount,
|
required bool hasAnyAccount,
|
||||||
required bool hasActiveChat}) = _RouterState;
|
}) = _RouterState;
|
||||||
|
|
||||||
factory RouterState.fromJson(dynamic json) =>
|
factory RouterState.fromJson(dynamic json) =>
|
||||||
_$RouterStateFromJson(json as Map<String, dynamic>);
|
_$RouterStateFromJson(json as Map<String, dynamic>);
|
||||||
|
|
@ -34,7 +35,6 @@ class RouterCubit extends Cubit<RouterState> {
|
||||||
RouterCubit(AccountRepository accountRepository)
|
RouterCubit(AccountRepository accountRepository)
|
||||||
: super(RouterState(
|
: super(RouterState(
|
||||||
hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty,
|
hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty,
|
||||||
hasActiveChat: false,
|
|
||||||
)) {
|
)) {
|
||||||
// Subscribe to repository streams
|
// Subscribe to repository streams
|
||||||
_accountRepositorySubscription = accountRepository.stream.listen((event) {
|
_accountRepositorySubscription = accountRepository.stream.listen((event) {
|
||||||
|
|
@ -50,10 +50,6 @@ class RouterCubit extends Cubit<RouterState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void setHasActiveChat(bool active) {
|
|
||||||
emit(state.copyWith(hasActiveChat: active));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await _accountRepositorySubscription.cancel();
|
await _accountRepositorySubscription.cancel();
|
||||||
|
|
@ -62,27 +58,29 @@ class RouterCubit extends Cubit<RouterState> {
|
||||||
|
|
||||||
/// Our application routes
|
/// Our application routes
|
||||||
List<RouteBase> get routes => [
|
List<RouteBase> get routes => [
|
||||||
ShellRoute(
|
GoRoute(
|
||||||
navigatorKey: _homeNavKey,
|
path: '/',
|
||||||
builder: (context, state, child) => HomeShell(
|
builder: (context, state) => const HomeScreen(),
|
||||||
accountReadyBuilder: Builder(
|
),
|
||||||
builder: (context) =>
|
GoRoute(
|
||||||
HomeAccountReadyShell(context: context, child: child))),
|
path: '/edit_account',
|
||||||
routes: [
|
builder: (context, state) {
|
||||||
GoRoute(
|
final extra = state.extra! as List<Object?>;
|
||||||
path: '/',
|
return EditAccountPage(
|
||||||
builder: (context, state) => const HomeAccountReadyMain(),
|
superIdentityRecordKey: extra[0]! as TypedKey,
|
||||||
),
|
existingProfile: extra[1]! as proto.Profile,
|
||||||
GoRoute(
|
);
|
||||||
path: '/chat',
|
},
|
||||||
builder: (context, state) => const HomeAccountReadyChat(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/new_account',
|
path: '/new_account',
|
||||||
builder: (context, state) => const NewAccountPage(),
|
builder: (context, state) => const NewAccountPage(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/new_account/recovery_key',
|
||||||
|
builder: (context, state) =>
|
||||||
|
ShowRecoveryKeyPage(secretKey: state.extra! as SecretKey),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
builder: (context, state) => const SettingsPage(),
|
builder: (context, state) => const SettingsPage(),
|
||||||
|
|
@ -98,37 +96,14 @@ class RouterCubit extends Cubit<RouterState> {
|
||||||
// No matter where we are, if there's not
|
// No matter where we are, if there's not
|
||||||
|
|
||||||
switch (goRouterState.matchedLocation) {
|
switch (goRouterState.matchedLocation) {
|
||||||
case '/new_account':
|
|
||||||
return state.hasAnyAccount ? '/' : null;
|
|
||||||
case '/':
|
case '/':
|
||||||
if (!state.hasAnyAccount) {
|
if (!state.hasAnyAccount) {
|
||||||
return '/new_account';
|
return '/new_account';
|
||||||
}
|
}
|
||||||
if (responsiveVisibility(
|
|
||||||
context: context,
|
|
||||||
tablet: false,
|
|
||||||
tabletLandscape: false,
|
|
||||||
desktop: false)) {
|
|
||||||
if (state.hasActiveChat) {
|
|
||||||
return '/chat';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
case '/chat':
|
case '/new_account':
|
||||||
if (!state.hasAnyAccount) {
|
return null;
|
||||||
return '/new_account';
|
case '/new_account/recovery_key':
|
||||||
}
|
|
||||||
if (responsiveVisibility(
|
|
||||||
context: context,
|
|
||||||
tablet: false,
|
|
||||||
tabletLandscape: false,
|
|
||||||
desktop: false)) {
|
|
||||||
if (!state.hasActiveChat) {
|
|
||||||
return '/';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return '/';
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
case '/settings':
|
case '/settings':
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ RouterState _$RouterStateFromJson(Map<String, dynamic> json) {
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$RouterState {
|
mixin _$RouterState {
|
||||||
bool get hasAnyAccount => throw _privateConstructorUsedError;
|
bool get hasAnyAccount => throw _privateConstructorUsedError;
|
||||||
bool get hasActiveChat => throw _privateConstructorUsedError;
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
|
|
@ -35,7 +34,7 @@ abstract class $RouterStateCopyWith<$Res> {
|
||||||
RouterState value, $Res Function(RouterState) then) =
|
RouterState value, $Res Function(RouterState) then) =
|
||||||
_$RouterStateCopyWithImpl<$Res, RouterState>;
|
_$RouterStateCopyWithImpl<$Res, RouterState>;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({bool hasAnyAccount, bool hasActiveChat});
|
$Res call({bool hasAnyAccount});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -52,17 +51,12 @@ class _$RouterStateCopyWithImpl<$Res, $Val extends RouterState>
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? hasAnyAccount = null,
|
Object? hasAnyAccount = null,
|
||||||
Object? hasActiveChat = null,
|
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
hasAnyAccount: null == hasAnyAccount
|
hasAnyAccount: null == hasAnyAccount
|
||||||
? _value.hasAnyAccount
|
? _value.hasAnyAccount
|
||||||
: hasAnyAccount // ignore: cast_nullable_to_non_nullable
|
: hasAnyAccount // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
hasActiveChat: null == hasActiveChat
|
|
||||||
? _value.hasActiveChat
|
|
||||||
: hasActiveChat // ignore: cast_nullable_to_non_nullable
|
|
||||||
as bool,
|
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +69,7 @@ abstract class _$$RouterStateImplCopyWith<$Res>
|
||||||
__$$RouterStateImplCopyWithImpl<$Res>;
|
__$$RouterStateImplCopyWithImpl<$Res>;
|
||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({bool hasAnyAccount, bool hasActiveChat});
|
$Res call({bool hasAnyAccount});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -90,17 +84,12 @@ class __$$RouterStateImplCopyWithImpl<$Res>
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? hasAnyAccount = null,
|
Object? hasAnyAccount = null,
|
||||||
Object? hasActiveChat = null,
|
|
||||||
}) {
|
}) {
|
||||||
return _then(_$RouterStateImpl(
|
return _then(_$RouterStateImpl(
|
||||||
hasAnyAccount: null == hasAnyAccount
|
hasAnyAccount: null == hasAnyAccount
|
||||||
? _value.hasAnyAccount
|
? _value.hasAnyAccount
|
||||||
: hasAnyAccount // ignore: cast_nullable_to_non_nullable
|
: hasAnyAccount // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
hasActiveChat: null == hasActiveChat
|
|
||||||
? _value.hasActiveChat
|
|
||||||
: hasActiveChat // ignore: cast_nullable_to_non_nullable
|
|
||||||
as bool,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -108,20 +97,17 @@ class __$$RouterStateImplCopyWithImpl<$Res>
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
|
class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
|
||||||
const _$RouterStateImpl(
|
const _$RouterStateImpl({required this.hasAnyAccount});
|
||||||
{required this.hasAnyAccount, required this.hasActiveChat});
|
|
||||||
|
|
||||||
factory _$RouterStateImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$RouterStateImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$RouterStateImplFromJson(json);
|
_$$RouterStateImplFromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final bool hasAnyAccount;
|
final bool hasAnyAccount;
|
||||||
@override
|
|
||||||
final bool hasActiveChat;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||||
return 'RouterState(hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)';
|
return 'RouterState(hasAnyAccount: $hasAnyAccount)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -129,8 +115,7 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
|
||||||
super.debugFillProperties(properties);
|
super.debugFillProperties(properties);
|
||||||
properties
|
properties
|
||||||
..add(DiagnosticsProperty('type', 'RouterState'))
|
..add(DiagnosticsProperty('type', 'RouterState'))
|
||||||
..add(DiagnosticsProperty('hasAnyAccount', hasAnyAccount))
|
..add(DiagnosticsProperty('hasAnyAccount', hasAnyAccount));
|
||||||
..add(DiagnosticsProperty('hasActiveChat', hasActiveChat));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -139,14 +124,12 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
|
||||||
(other.runtimeType == runtimeType &&
|
(other.runtimeType == runtimeType &&
|
||||||
other is _$RouterStateImpl &&
|
other is _$RouterStateImpl &&
|
||||||
(identical(other.hasAnyAccount, hasAnyAccount) ||
|
(identical(other.hasAnyAccount, hasAnyAccount) ||
|
||||||
other.hasAnyAccount == hasAnyAccount) &&
|
other.hasAnyAccount == hasAnyAccount));
|
||||||
(identical(other.hasActiveChat, hasActiveChat) ||
|
|
||||||
other.hasActiveChat == hasActiveChat));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, hasAnyAccount, hasActiveChat);
|
int get hashCode => Object.hash(runtimeType, hasAnyAccount);
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
|
|
@ -163,9 +146,8 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _RouterState implements RouterState {
|
abstract class _RouterState implements RouterState {
|
||||||
const factory _RouterState(
|
const factory _RouterState({required final bool hasAnyAccount}) =
|
||||||
{required final bool hasAnyAccount,
|
_$RouterStateImpl;
|
||||||
required final bool hasActiveChat}) = _$RouterStateImpl;
|
|
||||||
|
|
||||||
factory _RouterState.fromJson(Map<String, dynamic> json) =
|
factory _RouterState.fromJson(Map<String, dynamic> json) =
|
||||||
_$RouterStateImpl.fromJson;
|
_$RouterStateImpl.fromJson;
|
||||||
|
|
@ -173,8 +155,6 @@ abstract class _RouterState implements RouterState {
|
||||||
@override
|
@override
|
||||||
bool get hasAnyAccount;
|
bool get hasAnyAccount;
|
||||||
@override
|
@override
|
||||||
bool get hasActiveChat;
|
|
||||||
@override
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
_$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith =>
|
_$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,9 @@ part of 'router_cubit.dart';
|
||||||
_$RouterStateImpl _$$RouterStateImplFromJson(Map<String, dynamic> json) =>
|
_$RouterStateImpl _$$RouterStateImplFromJson(Map<String, dynamic> json) =>
|
||||||
_$RouterStateImpl(
|
_$RouterStateImpl(
|
||||||
hasAnyAccount: json['has_any_account'] as bool,
|
hasAnyAccount: json['has_any_account'] as bool,
|
||||||
hasActiveChat: json['has_active_chat'] as bool,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$RouterStateImplToJson(_$RouterStateImpl instance) =>
|
Map<String, dynamic> _$$RouterStateImplToJson(_$RouterStateImpl instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'has_any_account': instance.hasAnyAccount,
|
'has_any_account': instance.hasAnyAccount,
|
||||||
'has_active_chat': instance.hasActiveChat,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -89,11 +89,9 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
|
||||||
onError: errorScale.primaryText,
|
onError: errorScale.primaryText,
|
||||||
// errorContainer: errorScale.hoverElementBackground,
|
// errorContainer: errorScale.hoverElementBackground,
|
||||||
// onErrorContainer: errorScale.subtleText,
|
// onErrorContainer: errorScale.subtleText,
|
||||||
background: grayScale.appBackground, // reviewed
|
|
||||||
onBackground: grayScale.appText, // reviewed
|
|
||||||
surface: primaryScale.primary, // reviewed
|
surface: primaryScale.primary, // reviewed
|
||||||
onSurface: primaryScale.primaryText, // reviewed
|
onSurface: primaryScale.primaryText, // reviewed
|
||||||
surfaceVariant: secondaryScale.primary,
|
surfaceContainerHighest: secondaryScale.primary,
|
||||||
onSurfaceVariant: secondaryScale.primaryText, // ?? reviewed a little
|
onSurfaceVariant: secondaryScale.primaryText, // ?? reviewed a little
|
||||||
outline: primaryScale.border,
|
outline: primaryScale.border,
|
||||||
outlineVariant: secondaryScale.border,
|
outlineVariant: secondaryScale.border,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
|
||||||
import 'package:motion_toast/motion_toast.dart';
|
import 'package:motion_toast/motion_toast.dart';
|
||||||
import 'package:quickalert/quickalert.dart';
|
import 'package:quickalert/quickalert.dart';
|
||||||
|
|
||||||
|
|
@ -122,36 +121,45 @@ Future<void> showErrorModal(
|
||||||
}
|
}
|
||||||
|
|
||||||
void showErrorToast(BuildContext context, String message) {
|
void showErrorToast(BuildContext context, String message) {
|
||||||
MotionToast.error(
|
final theme = Theme.of(context);
|
||||||
title: Text(translate('toast.error')),
|
final scale = theme.extension<ScaleScheme>()!;
|
||||||
|
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||||
|
|
||||||
|
MotionToast(
|
||||||
|
//title: Text(translate('toast.error')),
|
||||||
description: Text(message),
|
description: Text(message),
|
||||||
|
constraints: BoxConstraints.loose(const Size(400, 100)),
|
||||||
|
contentPadding: const EdgeInsets.all(16),
|
||||||
|
primaryColor: scale.errorScale.elementBackground,
|
||||||
|
secondaryColor: scale.errorScale.calloutBackground,
|
||||||
|
borderRadius: 16,
|
||||||
|
toastDuration: const Duration(seconds: 4),
|
||||||
|
animationDuration: const Duration(milliseconds: 1000),
|
||||||
|
displayBorder: scaleConfig.useVisualIndicators,
|
||||||
|
icon: Icons.error,
|
||||||
).show(context);
|
).show(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showInfoToast(BuildContext context, String message) {
|
void showInfoToast(BuildContext context, String message) {
|
||||||
MotionToast.info(
|
final theme = Theme.of(context);
|
||||||
title: Text(translate('toast.info')),
|
final scale = theme.extension<ScaleScheme>()!;
|
||||||
|
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||||
|
|
||||||
|
MotionToast(
|
||||||
|
//title: Text(translate('toast.info')),
|
||||||
description: Text(message),
|
description: Text(message),
|
||||||
|
constraints: BoxConstraints.loose(const Size(400, 100)),
|
||||||
|
contentPadding: const EdgeInsets.all(16),
|
||||||
|
primaryColor: scale.tertiaryScale.elementBackground,
|
||||||
|
secondaryColor: scale.tertiaryScale.calloutBackground,
|
||||||
|
borderRadius: 16,
|
||||||
|
toastDuration: const Duration(seconds: 2),
|
||||||
|
animationDuration: const Duration(milliseconds: 500),
|
||||||
|
displayBorder: scaleConfig.useVisualIndicators,
|
||||||
|
icon: Icons.info,
|
||||||
).show(context);
|
).show(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget insetBorder(
|
|
||||||
// {required BuildContext context,
|
|
||||||
// required bool enabled,
|
|
||||||
// required Color color,
|
|
||||||
// required Widget child}) {
|
|
||||||
// if (!enabled) {
|
|
||||||
// return child;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return Stack({
|
|
||||||
// children: [] {
|
|
||||||
// DecoratedBox(decoration: BoxDecoration()
|
|
||||||
// child,
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
Widget styledTitleContainer({
|
Widget styledTitleContainer({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required String title,
|
required String title,
|
||||||
|
|
@ -230,3 +238,26 @@ Widget styledBottomSheet({
|
||||||
bool get isPlatformDark =>
|
bool get isPlatformDark =>
|
||||||
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
|
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
|
||||||
Brightness.dark;
|
Brightness.dark;
|
||||||
|
|
||||||
|
const grayColorFilter = ColorFilter.matrix(<double>[
|
||||||
|
0.2126,
|
||||||
|
0.7152,
|
||||||
|
0.0722,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0.2126,
|
||||||
|
0.7152,
|
||||||
|
0.0722,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0.2126,
|
||||||
|
0.7152,
|
||||||
|
0.0722,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
]);
|
||||||
|
|
|
||||||
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 'animations.dart';
|
||||||
export 'enter_password.dart';
|
export 'enter_password.dart';
|
||||||
export 'enter_pin.dart';
|
export 'enter_pin.dart';
|
||||||
export 'loggy.dart';
|
export 'loggy.dart';
|
||||||
export 'misc.dart';
|
export 'misc.dart';
|
||||||
|
export 'package_info.dart';
|
||||||
export 'phono_byte.dart';
|
export 'phono_byte.dart';
|
||||||
export 'pop_control.dart';
|
export 'pop_control.dart';
|
||||||
export 'responsive.dart';
|
export 'responsive.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:async_tools/async_tools.dart';
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
@ -11,7 +12,7 @@ import '../../theme/theme.dart';
|
||||||
import '../cubit/connection_state_cubit.dart';
|
import '../cubit/connection_state_cubit.dart';
|
||||||
|
|
||||||
class SignalStrengthMeterWidget extends StatelessWidget {
|
class SignalStrengthMeterWidget extends StatelessWidget {
|
||||||
const SignalStrengthMeterWidget({super.key});
|
const SignalStrengthMeterWidget({super.key, this.color, this.inactiveColor});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: prefer_expression_function_bodies
|
// ignore: prefer_expression_function_bodies
|
||||||
|
|
@ -33,32 +34,35 @@ class SignalStrengthMeterWidget extends StatelessWidget {
|
||||||
switch (connectionState.attachment.state) {
|
switch (connectionState.attachment.state) {
|
||||||
case AttachmentState.detached:
|
case AttachmentState.detached:
|
||||||
iconWidget = Icon(Icons.signal_cellular_nodata,
|
iconWidget = Icon(Icons.signal_cellular_nodata,
|
||||||
size: iconSize, color: scale.primaryScale.primaryText);
|
size: iconSize,
|
||||||
|
color: this.color ?? scale.primaryScale.primaryText);
|
||||||
return;
|
return;
|
||||||
case AttachmentState.detaching:
|
case AttachmentState.detaching:
|
||||||
iconWidget = Icon(Icons.signal_cellular_off,
|
iconWidget = Icon(Icons.signal_cellular_off,
|
||||||
size: iconSize, color: scale.primaryScale.primaryText);
|
size: iconSize,
|
||||||
|
color: this.color ?? scale.primaryScale.primaryText);
|
||||||
return;
|
return;
|
||||||
case AttachmentState.attaching:
|
case AttachmentState.attaching:
|
||||||
value = 0;
|
value = 0;
|
||||||
color = scale.primaryScale.primaryText;
|
color = this.color ?? scale.primaryScale.primaryText;
|
||||||
case AttachmentState.attachedWeak:
|
case AttachmentState.attachedWeak:
|
||||||
value = 1;
|
value = 1;
|
||||||
color = scale.primaryScale.primaryText;
|
color = this.color ?? scale.primaryScale.primaryText;
|
||||||
case AttachmentState.attachedStrong:
|
case AttachmentState.attachedStrong:
|
||||||
value = 2;
|
value = 2;
|
||||||
color = scale.primaryScale.primaryText;
|
color = this.color ?? scale.primaryScale.primaryText;
|
||||||
case AttachmentState.attachedGood:
|
case AttachmentState.attachedGood:
|
||||||
value = 3;
|
value = 3;
|
||||||
color = scale.primaryScale.primaryText;
|
color = this.color ?? scale.primaryScale.primaryText;
|
||||||
case AttachmentState.fullyAttached:
|
case AttachmentState.fullyAttached:
|
||||||
value = 4;
|
value = 4;
|
||||||
color = scale.primaryScale.primaryText;
|
color = this.color ?? scale.primaryScale.primaryText;
|
||||||
case AttachmentState.overAttached:
|
case AttachmentState.overAttached:
|
||||||
value = 4;
|
value = 4;
|
||||||
color = scale.primaryScale.primaryText;
|
color = this.color ?? scale.primaryScale.primaryText;
|
||||||
}
|
}
|
||||||
inactiveColor = scale.primaryScale.primaryText;
|
inactiveColor =
|
||||||
|
this.inactiveColor ?? scale.primaryScale.primaryText;
|
||||||
|
|
||||||
iconWidget = SignalStrengthIndicator.bars(
|
iconWidget = SignalStrengthIndicator.bars(
|
||||||
value: value,
|
value: value,
|
||||||
|
|
@ -86,4 +90,16 @@ class SignalStrengthMeterWidget extends StatelessWidget {
|
||||||
child: iconWidget);
|
child: iconWidget);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
final Color? color;
|
||||||
|
final Color? inactiveColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties
|
||||||
|
..add(ColorProperty('color', color))
|
||||||
|
..add(ColorProperty('inactiveColor', inactiveColor));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import mobile_scanner
|
import mobile_scanner
|
||||||
|
import package_info_plus
|
||||||
import pasteboard
|
import pasteboard
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import screen_retriever
|
import screen_retriever
|
||||||
|
|
@ -19,6 +20,7 @@ import window_manager
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||||
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
|
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ PODS:
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
- mobile_scanner (5.1.1):
|
- mobile_scanner (5.1.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- package_info_plus (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- pasteboard (0.0.1):
|
- pasteboard (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
|
|
@ -29,6 +31,7 @@ PODS:
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`)
|
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`)
|
||||||
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
|
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
|
||||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
|
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
|
||||||
|
|
@ -45,6 +48,8 @@ EXTERNAL SOURCES:
|
||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
mobile_scanner:
|
mobile_scanner:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos
|
||||||
|
package_info_plus:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||||
pasteboard:
|
pasteboard:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
|
|
@ -69,6 +74,7 @@ EXTERNAL SOURCES:
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
mobile_scanner: 1efac1e53c294b24e3bb55bcc7f4deee0233a86b
|
mobile_scanner: 1efac1e53c294b24e3bb55bcc7f4deee0233a86b
|
||||||
|
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
|
||||||
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
|
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ dependencies:
|
||||||
path: ../
|
path: ../
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
async_tools: ^0.1.2
|
async_tools: ^0.1.3
|
||||||
integration_test:
|
integration_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
lint_hard: ^4.0.0
|
lint_hard: ^4.0.0
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ message DHTData {
|
||||||
uint32 size = 4;
|
uint32 size = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// DHTLog - represents a ring buffer of many elements with append/truncate semantics
|
// DHTLog - represents a ring buffer of many elements with append/truncate semantics
|
||||||
// Header in subkey 0 of first key follows this structure
|
// Header in subkey 0 of first key follows this structure
|
||||||
message DHTLog {
|
message DHTLog {
|
||||||
|
|
@ -62,27 +61,6 @@ message DHTShortArray {
|
||||||
// calculated through iteration
|
// calculated through iteration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reference to data on the DHT
|
|
||||||
message DHTDataReference {
|
|
||||||
veilid.TypedKey dht_data = 1;
|
|
||||||
veilid.TypedKey hash = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reference to data on the BlockStore
|
|
||||||
message BlockStoreDataReference {
|
|
||||||
veilid.TypedKey block = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DataReference
|
|
||||||
// Pointer to data somewhere in Veilid
|
|
||||||
// Abstraction over DHTData and BlockStore
|
|
||||||
message DataReference {
|
|
||||||
oneof kind {
|
|
||||||
DHTDataReference dht_data = 1;
|
|
||||||
BlockStoreDataReference block_store_data = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A pointer to an child DHT record
|
// A pointer to an child DHT record
|
||||||
message OwnedDHTRecordPointer {
|
message OwnedDHTRecordPointer {
|
||||||
// DHT Record key
|
// DHT Record key
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,12 @@ class _DHTLogSpine {
|
||||||
|
|
||||||
// Write new spine head record to the network
|
// Write new spine head record to the network
|
||||||
await spine.operate((spine) async {
|
await spine.operate((spine) async {
|
||||||
|
// Write first empty subkey
|
||||||
|
final subkeyData = _makeEmptySubkey();
|
||||||
|
final existingSubkeyData =
|
||||||
|
await spineRecord.tryWriteBytes(subkeyData, subkey: 1);
|
||||||
|
assert(existingSubkeyData == null, 'Should never conflict on create');
|
||||||
|
|
||||||
final success = await spine.writeSpineHead();
|
final success = await spine.writeSpineHead();
|
||||||
assert(success, 'false return should never happen on create');
|
assert(success, 'false return should never happen on create');
|
||||||
});
|
});
|
||||||
|
|
@ -603,7 +609,7 @@ class _DHTLogSpine {
|
||||||
// Don't watch for local changes because this class already handles
|
// Don't watch for local changes because this class already handles
|
||||||
// notifying listeners and knows when it makes local changes
|
// notifying listeners and knows when it makes local changes
|
||||||
_subscription ??=
|
_subscription ??=
|
||||||
await _spineRecord.listen(localChanges: false, _onSpineChanged);
|
await _spineRecord.listen(localChanges: true, _onSpineChanged);
|
||||||
} on Exception {
|
} on Exception {
|
||||||
// If anything fails, try to cancel the watches
|
// If anything fails, try to cancel the watches
|
||||||
await cancelWatch();
|
await cancelWatch();
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,6 @@ class DefaultDHTRecordCubit<T> extends DHTRecordCubit<T> {
|
||||||
stateFunction: _makeStateFunction(decodeState),
|
stateFunction: _makeStateFunction(decodeState),
|
||||||
watchFunction: _makeWatchFunction());
|
watchFunction: _makeWatchFunction());
|
||||||
|
|
||||||
// DefaultDHTRecordCubit.value({
|
|
||||||
// required super.record,
|
|
||||||
// required T Function(List<int> data) decodeState,
|
|
||||||
// }) : super.value(
|
|
||||||
// initialStateFunction: _makeInitialStateFunction(decodeState),
|
|
||||||
// stateFunction: _makeStateFunction(decodeState),
|
|
||||||
// watchFunction: _makeWatchFunction());
|
|
||||||
|
|
||||||
static InitialStateFunction<T> _makeInitialStateFunction<T>(
|
static InitialStateFunction<T> _makeInitialStateFunction<T>(
|
||||||
T Function(List<int> data) decodeState) =>
|
T Function(List<int> data) decodeState) =>
|
||||||
(record) async {
|
(record) async {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
part of 'dht_record_pool.dart';
|
part of 'dht_record_pool.dart';
|
||||||
|
|
||||||
const _sfListen = 'listen';
|
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class DHTRecordWatchChange extends Equatable {
|
class DHTRecordWatchChange extends Equatable {
|
||||||
const DHTRecordWatchChange(
|
const DHTRecordWatchChange(
|
||||||
|
|
@ -41,7 +39,7 @@ enum DHTRecordRefreshMode {
|
||||||
class DHTRecord implements DHTDeleteable<DHTRecord> {
|
class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||||
DHTRecord._(
|
DHTRecord._(
|
||||||
{required VeilidRoutingContext routingContext,
|
{required VeilidRoutingContext routingContext,
|
||||||
required SharedDHTRecordData sharedDHTRecordData,
|
required _SharedDHTRecordData sharedDHTRecordData,
|
||||||
required int defaultSubkey,
|
required int defaultSubkey,
|
||||||
required KeyPair? writer,
|
required KeyPair? writer,
|
||||||
required VeilidCrypto crypto,
|
required VeilidCrypto crypto,
|
||||||
|
|
@ -241,7 +239,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||||
// if so, shortcut and don't bother decrypting it
|
// if so, shortcut and don't bother decrypting it
|
||||||
if (newValueData.data.equals(encryptedNewValue)) {
|
if (newValueData.data.equals(encryptedNewValue)) {
|
||||||
if (isUpdated) {
|
if (isUpdated) {
|
||||||
DHTRecordPool.instance.processLocalValueChange(key, newValue, subkey);
|
DHTRecordPool.instance._processLocalValueChange(key, newValue, subkey);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +249,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||||
await (crypto ?? _crypto).decrypt(newValueData.data);
|
await (crypto ?? _crypto).decrypt(newValueData.data);
|
||||||
if (isUpdated) {
|
if (isUpdated) {
|
||||||
DHTRecordPool.instance
|
DHTRecordPool.instance
|
||||||
.processLocalValueChange(key, decryptedNewValue, subkey);
|
._processLocalValueChange(key, decryptedNewValue, subkey);
|
||||||
}
|
}
|
||||||
return decryptedNewValue;
|
return decryptedNewValue;
|
||||||
}
|
}
|
||||||
|
|
@ -298,7 +296,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||||
|
|
||||||
final isUpdated = newValueData.seq != lastSeq;
|
final isUpdated = newValueData.seq != lastSeq;
|
||||||
if (isUpdated) {
|
if (isUpdated) {
|
||||||
DHTRecordPool.instance.processLocalValueChange(key, newValue, subkey);
|
DHTRecordPool.instance._processLocalValueChange(key, newValue, subkey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,7 +306,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||||
/// Each attempt to write the value calls an update function with the
|
/// Each attempt to write the value calls an update function with the
|
||||||
/// old value to determine what new value should be attempted for that write.
|
/// old value to determine what new value should be attempted for that write.
|
||||||
Future<void> eventualUpdateBytes(
|
Future<void> eventualUpdateBytes(
|
||||||
Future<Uint8List> Function(Uint8List? oldValue) update,
|
Future<Uint8List?> Function(Uint8List? oldValue) update,
|
||||||
{int subkey = -1,
|
{int subkey = -1,
|
||||||
VeilidCrypto? crypto,
|
VeilidCrypto? crypto,
|
||||||
KeyPair? writer,
|
KeyPair? writer,
|
||||||
|
|
@ -323,7 +321,10 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||||
do {
|
do {
|
||||||
// Update the data
|
// Update the data
|
||||||
final updatedValue = await update(oldValue);
|
final updatedValue = await update(oldValue);
|
||||||
|
if (updatedValue == null) {
|
||||||
|
// If null is returned from the update, stop trying to do the update
|
||||||
|
break;
|
||||||
|
}
|
||||||
// Try to write it back to the network
|
// Try to write it back to the network
|
||||||
oldValue = await tryWriteBytes(updatedValue,
|
oldValue = await tryWriteBytes(updatedValue,
|
||||||
subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum);
|
subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum);
|
||||||
|
|
@ -389,7 +390,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||||
|
|
||||||
/// Like 'eventualUpdateBytes' but with JSON marshal/unmarshal of the value
|
/// Like 'eventualUpdateBytes' but with JSON marshal/unmarshal of the value
|
||||||
Future<void> eventualUpdateJson<T>(
|
Future<void> eventualUpdateJson<T>(
|
||||||
T Function(dynamic) fromJson, Future<T> Function(T?) update,
|
T Function(dynamic) fromJson, Future<T?> Function(T?) update,
|
||||||
{int subkey = -1,
|
{int subkey = -1,
|
||||||
VeilidCrypto? crypto,
|
VeilidCrypto? crypto,
|
||||||
KeyPair? writer,
|
KeyPair? writer,
|
||||||
|
|
@ -399,7 +400,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||||
|
|
||||||
/// Like 'eventualUpdateBytes' but with protobuf marshal/unmarshal of the value
|
/// Like 'eventualUpdateBytes' but with protobuf marshal/unmarshal of the value
|
||||||
Future<void> eventualUpdateProtobuf<T extends GeneratedMessage>(
|
Future<void> eventualUpdateProtobuf<T extends GeneratedMessage>(
|
||||||
T Function(List<int>) fromBuffer, Future<T> Function(T?) update,
|
T Function(List<int>) fromBuffer, Future<T?> Function(T?) update,
|
||||||
{int subkey = -1,
|
{int subkey = -1,
|
||||||
VeilidCrypto? crypto,
|
VeilidCrypto? crypto,
|
||||||
KeyPair? writer,
|
KeyPair? writer,
|
||||||
|
|
@ -416,7 +417,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||||
// Set up watch requirements which will get picked up by the next tick
|
// Set up watch requirements which will get picked up by the next tick
|
||||||
final oldWatchState = watchState;
|
final oldWatchState = watchState;
|
||||||
watchState =
|
watchState =
|
||||||
WatchState(subkeys: subkeys, expiration: expiration, count: count);
|
_WatchState(subkeys: subkeys, expiration: expiration, count: count);
|
||||||
if (oldWatchState != watchState) {
|
if (oldWatchState != watchState) {
|
||||||
_sharedDHTRecordData.needsWatchStateUpdate = true;
|
_sharedDHTRecordData.needsWatchStateUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
@ -541,7 +542,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
final SharedDHTRecordData _sharedDHTRecordData;
|
final _SharedDHTRecordData _sharedDHTRecordData;
|
||||||
final VeilidRoutingContext _routingContext;
|
final VeilidRoutingContext _routingContext;
|
||||||
final int _defaultSubkey;
|
final int _defaultSubkey;
|
||||||
final KeyPair? _writer;
|
final KeyPair? _writer;
|
||||||
|
|
@ -551,5 +552,5 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||||
int _openCount;
|
int _openCount;
|
||||||
StreamController<DHTRecordWatchChange>? _watchController;
|
StreamController<DHTRecordWatchChange>? _watchController;
|
||||||
@internal
|
@internal
|
||||||
WatchState? watchState;
|
_WatchState? watchState;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,20 +29,6 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// DHTRecordCubit.value({
|
|
||||||
// required DHTRecord record,
|
|
||||||
// required InitialStateFunction<T> initialStateFunction,
|
|
||||||
// required StateFunction<T> stateFunction,
|
|
||||||
// required WatchFunction watchFunction,
|
|
||||||
// }) : _record = record,
|
|
||||||
// _stateFunction = stateFunction,
|
|
||||||
// _wantsCloseRecord = false,
|
|
||||||
// super(const AsyncValue.loading()) {
|
|
||||||
// Future.delayed(Duration.zero, () async {
|
|
||||||
// await _init(initialStateFunction, stateFunction, watchFunction);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
Future<void> _init(
|
Future<void> _init(
|
||||||
InitialStateFunction<T> initialStateFunction,
|
InitialStateFunction<T> initialStateFunction,
|
||||||
StateFunction<T> stateFunction,
|
StateFunction<T> stateFunction,
|
||||||
|
|
|
||||||
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);
|
$core.List<$core.int> get seqs => $_getList(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DHTDataReference extends $pb.GeneratedMessage {
|
|
||||||
factory DHTDataReference() => create();
|
|
||||||
DHTDataReference._() : super();
|
|
||||||
factory DHTDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
|
||||||
factory DHTDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DHTDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create)
|
|
||||||
..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: $0.TypedKey.create)
|
|
||||||
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'hash', subBuilder: $0.TypedKey.create)
|
|
||||||
..hasRequiredFields = false
|
|
||||||
;
|
|
||||||
|
|
||||||
@$core.Deprecated(
|
|
||||||
'Using this can add significant overhead to your binary. '
|
|
||||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
|
||||||
'Will be removed in next major version')
|
|
||||||
DHTDataReference clone() => DHTDataReference()..mergeFromMessage(this);
|
|
||||||
@$core.Deprecated(
|
|
||||||
'Using this can add significant overhead to your binary. '
|
|
||||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
|
||||||
'Will be removed in next major version')
|
|
||||||
DHTDataReference copyWith(void Function(DHTDataReference) updates) => super.copyWith((message) => updates(message as DHTDataReference)) as DHTDataReference;
|
|
||||||
|
|
||||||
$pb.BuilderInfo get info_ => _i;
|
|
||||||
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static DHTDataReference create() => DHTDataReference._();
|
|
||||||
DHTDataReference createEmptyInstance() => create();
|
|
||||||
static $pb.PbList<DHTDataReference> createRepeated() => $pb.PbList<DHTDataReference>();
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static DHTDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DHTDataReference>(create);
|
|
||||||
static DHTDataReference? _defaultInstance;
|
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$0.TypedKey get dhtData => $_getN(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
set dhtData($0.TypedKey v) { setField(1, v); }
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.bool hasDhtData() => $_has(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
void clearDhtData() => clearField(1);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$0.TypedKey ensureDhtData() => $_ensure(0);
|
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
$0.TypedKey get hash => $_getN(1);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
set hash($0.TypedKey v) { setField(2, v); }
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
$core.bool hasHash() => $_has(1);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
void clearHash() => clearField(2);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
$0.TypedKey ensureHash() => $_ensure(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
class BlockStoreDataReference extends $pb.GeneratedMessage {
|
|
||||||
factory BlockStoreDataReference() => create();
|
|
||||||
BlockStoreDataReference._() : super();
|
|
||||||
factory BlockStoreDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
|
||||||
factory BlockStoreDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BlockStoreDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create)
|
|
||||||
..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'block', subBuilder: $0.TypedKey.create)
|
|
||||||
..hasRequiredFields = false
|
|
||||||
;
|
|
||||||
|
|
||||||
@$core.Deprecated(
|
|
||||||
'Using this can add significant overhead to your binary. '
|
|
||||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
|
||||||
'Will be removed in next major version')
|
|
||||||
BlockStoreDataReference clone() => BlockStoreDataReference()..mergeFromMessage(this);
|
|
||||||
@$core.Deprecated(
|
|
||||||
'Using this can add significant overhead to your binary. '
|
|
||||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
|
||||||
'Will be removed in next major version')
|
|
||||||
BlockStoreDataReference copyWith(void Function(BlockStoreDataReference) updates) => super.copyWith((message) => updates(message as BlockStoreDataReference)) as BlockStoreDataReference;
|
|
||||||
|
|
||||||
$pb.BuilderInfo get info_ => _i;
|
|
||||||
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static BlockStoreDataReference create() => BlockStoreDataReference._();
|
|
||||||
BlockStoreDataReference createEmptyInstance() => create();
|
|
||||||
static $pb.PbList<BlockStoreDataReference> createRepeated() => $pb.PbList<BlockStoreDataReference>();
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static BlockStoreDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BlockStoreDataReference>(create);
|
|
||||||
static BlockStoreDataReference? _defaultInstance;
|
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$0.TypedKey get block => $_getN(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
set block($0.TypedKey v) { setField(1, v); }
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.bool hasBlock() => $_has(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
void clearBlock() => clearField(1);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$0.TypedKey ensureBlock() => $_ensure(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DataReference_Kind {
|
|
||||||
dhtData,
|
|
||||||
blockStoreData,
|
|
||||||
notSet
|
|
||||||
}
|
|
||||||
|
|
||||||
class DataReference extends $pb.GeneratedMessage {
|
|
||||||
factory DataReference() => create();
|
|
||||||
DataReference._() : super();
|
|
||||||
factory DataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
|
||||||
factory DataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
|
||||||
|
|
||||||
static const $core.Map<$core.int, DataReference_Kind> _DataReference_KindByTag = {
|
|
||||||
1 : DataReference_Kind.dhtData,
|
|
||||||
2 : DataReference_Kind.blockStoreData,
|
|
||||||
0 : DataReference_Kind.notSet
|
|
||||||
};
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create)
|
|
||||||
..oo(0, [1, 2])
|
|
||||||
..aOM<DHTDataReference>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: DHTDataReference.create)
|
|
||||||
..aOM<BlockStoreDataReference>(2, _omitFieldNames ? '' : 'blockStoreData', subBuilder: BlockStoreDataReference.create)
|
|
||||||
..hasRequiredFields = false
|
|
||||||
;
|
|
||||||
|
|
||||||
@$core.Deprecated(
|
|
||||||
'Using this can add significant overhead to your binary. '
|
|
||||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
|
||||||
'Will be removed in next major version')
|
|
||||||
DataReference clone() => DataReference()..mergeFromMessage(this);
|
|
||||||
@$core.Deprecated(
|
|
||||||
'Using this can add significant overhead to your binary. '
|
|
||||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
|
||||||
'Will be removed in next major version')
|
|
||||||
DataReference copyWith(void Function(DataReference) updates) => super.copyWith((message) => updates(message as DataReference)) as DataReference;
|
|
||||||
|
|
||||||
$pb.BuilderInfo get info_ => _i;
|
|
||||||
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static DataReference create() => DataReference._();
|
|
||||||
DataReference createEmptyInstance() => create();
|
|
||||||
static $pb.PbList<DataReference> createRepeated() => $pb.PbList<DataReference>();
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static DataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DataReference>(create);
|
|
||||||
static DataReference? _defaultInstance;
|
|
||||||
|
|
||||||
DataReference_Kind whichKind() => _DataReference_KindByTag[$_whichOneof(0)]!;
|
|
||||||
void clearKind() => clearField($_whichOneof(0));
|
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
DHTDataReference get dhtData => $_getN(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
set dhtData(DHTDataReference v) { setField(1, v); }
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.bool hasDhtData() => $_has(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
void clearDhtData() => clearField(1);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
DHTDataReference ensureDhtData() => $_ensure(0);
|
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
BlockStoreDataReference get blockStoreData => $_getN(1);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
set blockStoreData(BlockStoreDataReference v) { setField(2, v); }
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
$core.bool hasBlockStoreData() => $_has(1);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
void clearBlockStoreData() => clearField(2);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
BlockStoreDataReference ensureBlockStoreData() => $_ensure(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
class OwnedDHTRecordPointer extends $pb.GeneratedMessage {
|
class OwnedDHTRecordPointer extends $pb.GeneratedMessage {
|
||||||
factory OwnedDHTRecordPointer() => create();
|
factory OwnedDHTRecordPointer() => create();
|
||||||
OwnedDHTRecordPointer._() : super();
|
OwnedDHTRecordPointer._() : super();
|
||||||
|
|
|
||||||
|
|
@ -60,51 +60,6 @@ final $typed_data.Uint8List dHTShortArrayDescriptor = $convert.base64Decode(
|
||||||
'Cg1ESFRTaG9ydEFycmF5EiQKBGtleXMYASADKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGtleXMSFA'
|
'Cg1ESFRTaG9ydEFycmF5EiQKBGtleXMYASADKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGtleXMSFA'
|
||||||
'oFaW5kZXgYAiABKAxSBWluZGV4EhIKBHNlcXMYAyADKA1SBHNlcXM=');
|
'oFaW5kZXgYAiABKAxSBWluZGV4EhIKBHNlcXMYAyADKA1SBHNlcXM=');
|
||||||
|
|
||||||
@$core.Deprecated('Use dHTDataReferenceDescriptor instead')
|
|
||||||
const DHTDataReference$json = {
|
|
||||||
'1': 'DHTDataReference',
|
|
||||||
'2': [
|
|
||||||
{'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'dhtData'},
|
|
||||||
{'1': 'hash', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'hash'},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Descriptor for `DHTDataReference`. Decode as a `google.protobuf.DescriptorProto`.
|
|
||||||
final $typed_data.Uint8List dHTDataReferenceDescriptor = $convert.base64Decode(
|
|
||||||
'ChBESFREYXRhUmVmZXJlbmNlEisKCGRodF9kYXRhGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5Ug'
|
|
||||||
'dkaHREYXRhEiQKBGhhc2gYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGhhc2g=');
|
|
||||||
|
|
||||||
@$core.Deprecated('Use blockStoreDataReferenceDescriptor instead')
|
|
||||||
const BlockStoreDataReference$json = {
|
|
||||||
'1': 'BlockStoreDataReference',
|
|
||||||
'2': [
|
|
||||||
{'1': 'block', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'block'},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Descriptor for `BlockStoreDataReference`. Decode as a `google.protobuf.DescriptorProto`.
|
|
||||||
final $typed_data.Uint8List blockStoreDataReferenceDescriptor = $convert.base64Decode(
|
|
||||||
'ChdCbG9ja1N0b3JlRGF0YVJlZmVyZW5jZRImCgVibG9jaxgBIAEoCzIQLnZlaWxpZC5UeXBlZE'
|
|
||||||
'tleVIFYmxvY2s=');
|
|
||||||
|
|
||||||
@$core.Deprecated('Use dataReferenceDescriptor instead')
|
|
||||||
const DataReference$json = {
|
|
||||||
'1': 'DataReference',
|
|
||||||
'2': [
|
|
||||||
{'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.dht.DHTDataReference', '9': 0, '10': 'dhtData'},
|
|
||||||
{'1': 'block_store_data', '3': 2, '4': 1, '5': 11, '6': '.dht.BlockStoreDataReference', '9': 0, '10': 'blockStoreData'},
|
|
||||||
],
|
|
||||||
'8': [
|
|
||||||
{'1': 'kind'},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Descriptor for `DataReference`. Decode as a `google.protobuf.DescriptorProto`.
|
|
||||||
final $typed_data.Uint8List dataReferenceDescriptor = $convert.base64Decode(
|
|
||||||
'Cg1EYXRhUmVmZXJlbmNlEjIKCGRodF9kYXRhGAEgASgLMhUuZGh0LkRIVERhdGFSZWZlcmVuY2'
|
|
||||||
'VIAFIHZGh0RGF0YRJIChBibG9ja19zdG9yZV9kYXRhGAIgASgLMhwuZGh0LkJsb2NrU3RvcmVE'
|
|
||||||
'YXRhUmVmZXJlbmNlSABSDmJsb2NrU3RvcmVEYXRhQgYKBGtpbmQ=');
|
|
||||||
|
|
||||||
@$core.Deprecated('Use ownedDHTRecordPointerDescriptor instead')
|
@$core.Deprecated('Use ownedDHTRecordPointerDescriptor instead')
|
||||||
const OwnedDHTRecordPointer$json = {
|
const OwnedDHTRecordPointer$json = {
|
||||||
'1': 'OwnedDHTRecordPointer',
|
'1': 'OwnedDHTRecordPointer',
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,19 @@ Uint8List jsonEncodeBytes(Object? object,
|
||||||
Uint8List.fromList(
|
Uint8List.fromList(
|
||||||
utf8.encode(jsonEncode(object, toEncodable: toEncodable)));
|
utf8.encode(jsonEncode(object, toEncodable: toEncodable)));
|
||||||
|
|
||||||
Future<Uint8List> jsonUpdateBytes<T>(T Function(dynamic) fromJson,
|
Future<Uint8List?> jsonUpdateBytes<T>(T Function(dynamic) fromJson,
|
||||||
Uint8List? oldBytes, Future<T> Function(T?) update) async {
|
Uint8List? oldBytes, Future<T?> Function(T?) update) async {
|
||||||
final oldObj =
|
final oldObj =
|
||||||
oldBytes == null ? null : fromJson(jsonDecode(utf8.decode(oldBytes)));
|
oldBytes == null ? null : fromJson(jsonDecode(utf8.decode(oldBytes)));
|
||||||
final newObj = await update(oldObj);
|
final newObj = await update(oldObj);
|
||||||
|
if (newObj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return jsonEncodeBytes(newObj);
|
return jsonEncodeBytes(newObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> Function(Uint8List?) jsonUpdate<T>(
|
Future<Uint8List?> Function(Uint8List?) jsonUpdate<T>(
|
||||||
T Function(dynamic) fromJson, Future<T> Function(T?) update) =>
|
T Function(dynamic) fromJson, Future<T?> Function(T?) update) =>
|
||||||
(oldBytes) => jsonUpdateBytes(fromJson, oldBytes, update);
|
(oldBytes) => jsonUpdateBytes(fromJson, oldBytes, update);
|
||||||
|
|
||||||
T Function(Object?) genericFromJson<T>(
|
T Function(Object?) genericFromJson<T>(
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@ import 'package:protobuf/protobuf.dart';
|
||||||
import 'table_db.dart';
|
import 'table_db.dart';
|
||||||
|
|
||||||
class PersistentQueue<T extends GeneratedMessage>
|
class PersistentQueue<T extends GeneratedMessage>
|
||||||
/*extends Cubit<AsyncValue<IList<T>>>*/ with
|
with TableDBBackedFromBuffer<IList<T>> {
|
||||||
TableDBBackedFromBuffer<IList<T>> {
|
|
||||||
//
|
//
|
||||||
PersistentQueue(
|
PersistentQueue(
|
||||||
{required String table,
|
{required String table,
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,19 @@ import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:protobuf/protobuf.dart';
|
import 'package:protobuf/protobuf.dart';
|
||||||
|
|
||||||
Future<Uint8List> protobufUpdateBytes<T extends GeneratedMessage>(
|
Future<Uint8List?> protobufUpdateBytes<T extends GeneratedMessage>(
|
||||||
T Function(List<int>) fromBuffer,
|
T Function(List<int>) fromBuffer,
|
||||||
Uint8List? oldBytes,
|
Uint8List? oldBytes,
|
||||||
Future<T> Function(T?) update) async {
|
Future<T?> Function(T?) update) async {
|
||||||
final oldObj = oldBytes == null ? null : fromBuffer(oldBytes);
|
final oldObj = oldBytes == null ? null : fromBuffer(oldBytes);
|
||||||
final newObj = await update(oldObj);
|
final newObj = await update(oldObj);
|
||||||
|
if (newObj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return Uint8List.fromList(newObj.writeToBuffer());
|
return Uint8List.fromList(newObj.writeToBuffer());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> Function(Uint8List?)
|
Future<Uint8List?> Function(Uint8List?)
|
||||||
protobufUpdate<T extends GeneratedMessage>(
|
protobufUpdate<T extends GeneratedMessage>(
|
||||||
T Function(List<int>) fromBuffer, Future<T> Function(T?) update) =>
|
T Function(List<int>) fromBuffer, Future<T?> Function(T?) update) =>
|
||||||
(oldBytes) => protobufUpdateBytes(fromBuffer, oldBytes, update);
|
(oldBytes) => protobufUpdateBytes(fromBuffer, oldBytes, update);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ packages:
|
||||||
path: "../../../dart_async_tools"
|
path: "../../../dart_async_tools"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.1.1"
|
version: "0.1.3"
|
||||||
bloc:
|
bloc:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -54,7 +54,7 @@ packages:
|
||||||
path: "../../../bloc_advanced_tools"
|
path: "../../../bloc_advanced_tools"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.1.1"
|
version: "0.1.3"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ environment:
|
||||||
sdk: '>=3.2.0 <4.0.0'
|
sdk: '>=3.2.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
async_tools: ^0.1.2
|
async_tools: ^0.1.3
|
||||||
bloc: ^8.1.4
|
bloc: ^8.1.4
|
||||||
bloc_advanced_tools: ^0.1.2
|
bloc_advanced_tools: ^0.1.3
|
||||||
charcode: ^1.3.1
|
charcode: ^1.3.1
|
||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
|
|
@ -24,11 +24,11 @@ dependencies:
|
||||||
# veilid: ^0.0.1
|
# veilid: ^0.0.1
|
||||||
path: ../../../veilid/veilid-flutter
|
path: ../../../veilid/veilid-flutter
|
||||||
|
|
||||||
# dependency_overrides:
|
dependency_overrides:
|
||||||
# async_tools:
|
async_tools:
|
||||||
# path: ../../../dart_async_tools
|
path: ../../../dart_async_tools
|
||||||
# bloc_advanced_tools:
|
bloc_advanced_tools:
|
||||||
# path: ../../../bloc_advanced_tools
|
path: ../../../bloc_advanced_tools
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.4.10
|
build_runner: ^2.4.10
|
||||||
|
|
|
||||||
42
pubspec.lock
42
pubspec.lock
|
|
@ -60,11 +60,10 @@ packages:
|
||||||
async_tools:
|
async_tools:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: async_tools
|
path: "../dart_async_tools"
|
||||||
sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684"
|
relative: true
|
||||||
url: "https://pub.dev"
|
source: path
|
||||||
source: hosted
|
version: "0.1.3"
|
||||||
version: "0.1.2"
|
|
||||||
awesome_extensions:
|
awesome_extensions:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -100,11 +99,10 @@ packages:
|
||||||
bloc_advanced_tools:
|
bloc_advanced_tools:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: bloc_advanced_tools
|
path: "../bloc_advanced_tools"
|
||||||
sha256: "0cf9b3a73a67addfe22ec3f97a1ac240f6ad53870d6b21a980260f390d7901cd"
|
relative: true
|
||||||
url: "https://pub.dev"
|
source: path
|
||||||
source: hosted
|
version: "0.1.3"
|
||||||
version: "0.1.2"
|
|
||||||
blurry_modal_progress_hud:
|
blurry_modal_progress_hud:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -585,6 +583,14 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_zoom_drawer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_zoom_drawer
|
||||||
|
sha256: "5a3708548868463fb36e0e3171761ab7cb513df88d2f14053802812d2e855060"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.0"
|
||||||
form_builder_validators:
|
form_builder_validators:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -857,6 +863,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
package_info_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: package_info_plus
|
||||||
|
sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.0.0"
|
||||||
|
package_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus_platform_interface
|
||||||
|
sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
pasteboard:
|
pasteboard:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
16
pubspec.yaml
16
pubspec.yaml
|
|
@ -11,12 +11,12 @@ dependencies:
|
||||||
animated_theme_switcher: ^2.0.10
|
animated_theme_switcher: ^2.0.10
|
||||||
ansicolor: ^2.0.2
|
ansicolor: ^2.0.2
|
||||||
archive: ^3.6.1
|
archive: ^3.6.1
|
||||||
async_tools: ^0.1.2
|
async_tools: ^0.1.3
|
||||||
awesome_extensions: ^2.0.16
|
awesome_extensions: ^2.0.16
|
||||||
badges: ^3.1.2
|
badges: ^3.1.2
|
||||||
basic_utils: ^5.7.0
|
basic_utils: ^5.7.0
|
||||||
bloc: ^8.1.4
|
bloc: ^8.1.4
|
||||||
bloc_advanced_tools: ^0.1.2
|
bloc_advanced_tools: ^0.1.3
|
||||||
blurry_modal_progress_hud: ^1.1.1
|
blurry_modal_progress_hud: ^1.1.1
|
||||||
change_case: ^2.1.0
|
change_case: ^2.1.0
|
||||||
charcode: ^1.3.1
|
charcode: ^1.3.1
|
||||||
|
|
@ -45,6 +45,7 @@ dependencies:
|
||||||
flutter_spinkit: ^5.2.1
|
flutter_spinkit: ^5.2.1
|
||||||
flutter_svg: ^2.0.10+1
|
flutter_svg: ^2.0.10+1
|
||||||
flutter_translate: ^4.1.0
|
flutter_translate: ^4.1.0
|
||||||
|
flutter_zoom_drawer: ^3.2.0
|
||||||
form_builder_validators: ^10.0.1
|
form_builder_validators: ^10.0.1
|
||||||
freezed_annotation: ^2.4.1
|
freezed_annotation: ^2.4.1
|
||||||
go_router: ^14.1.4
|
go_router: ^14.1.4
|
||||||
|
|
@ -56,6 +57,7 @@ dependencies:
|
||||||
meta: ^1.12.0
|
meta: ^1.12.0
|
||||||
mobile_scanner: ^5.1.1
|
mobile_scanner: ^5.1.1
|
||||||
motion_toast: ^2.10.0
|
motion_toast: ^2.10.0
|
||||||
|
package_info_plus: ^8.0.0
|
||||||
pasteboard: ^0.2.0
|
pasteboard: ^0.2.0
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
path_provider: ^2.1.3
|
path_provider: ^2.1.3
|
||||||
|
|
@ -91,11 +93,11 @@ dependencies:
|
||||||
xterm: ^4.0.0
|
xterm: ^4.0.0
|
||||||
zxing2: ^0.2.3
|
zxing2: ^0.2.3
|
||||||
|
|
||||||
# dependency_overrides:
|
dependency_overrides:
|
||||||
# async_tools:
|
async_tools:
|
||||||
# path: ../dart_async_tools
|
path: ../dart_async_tools
|
||||||
# bloc_advanced_tools:
|
bloc_advanced_tools:
|
||||||
# path: ../bloc_advanced_tools
|
path: ../bloc_advanced_tools
|
||||||
# flutter_chat_ui:
|
# flutter_chat_ui:
|
||||||
# path: ../flutter_chat_ui
|
# path: ../flutter_chat_ui
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue