clean up context locators

This commit is contained in:
Christien Rioux 2024-06-15 23:29:15 -04:00
parent 751022e743
commit 2ccad50f9a
31 changed files with 603 additions and 542 deletions

View file

@ -1,5 +1,6 @@
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:bloc_advanced_tools/bloc_advanced_tools.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';
@ -12,8 +13,12 @@ typedef AccountRecordsBlocMapState
class AccountRecordsBlocMapCubit extends BlocMapCubit<TypedKey, class AccountRecordsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<AccountRecordState>, AccountRecordCubit> AsyncValue<AccountRecordState>, AccountRecordCubit>
with StateMapFollower<UserLoginsState, TypedKey, UserLogin> { with StateMapFollower<UserLoginsState, TypedKey, UserLogin> {
AccountRecordsBlocMapCubit(AccountRepository accountRepository) AccountRecordsBlocMapCubit(
: _accountRepository = accountRepository; AccountRepository accountRepository, Locator locator)
: _accountRepository = accountRepository {
// Follow the user logins cubit
follow(locator<UserLoginsCubit>());
}
// Add account record cubit // Add account record cubit
Future<void> _addAccountRecordCubit( Future<void> _addAccountRecordCubit(

View file

@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'unlocked_account_info.dart'; import 'unlocked_account_info.dart';
@ -10,7 +11,7 @@ enum AccountInfoStatus {
} }
@immutable @immutable
class AccountInfo { class AccountInfo extends Equatable {
const AccountInfo({ const AccountInfo({
required this.status, required this.status,
required this.active, required this.active,
@ -20,4 +21,7 @@ class AccountInfo {
final AccountInfoStatus status; final AccountInfoStatus status;
final bool active; final bool active;
final UnlockedAccountInfo? unlockedAccountInfo; final UnlockedAccountInfo? unlockedAccountInfo;
@override
List<Object?> get props => [status, active, unlockedAccountInfo];
} }

View file

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
@ -7,12 +8,14 @@ import 'local_account/local_account.dart';
import 'user_login/user_login.dart'; import 'user_login/user_login.dart';
@immutable @immutable
class UnlockedAccountInfo { class UnlockedAccountInfo extends Equatable {
const UnlockedAccountInfo({ const UnlockedAccountInfo({
required this.localAccount, required this.localAccount,
required this.userLogin, required this.userLogin,
}); });
//
////////////////////////////////////////////////////////////////////////////
// Public Interface
TypedKey get superIdentityRecordKey => localAccount.superIdentity.recordKey; TypedKey get superIdentityRecordKey => localAccount.superIdentity.recordKey;
TypedKey get accountRecordKey => TypedKey get accountRecordKey =>
@ -41,7 +44,12 @@ class UnlockedAccountInfo {
return messagesCrypto; return messagesCrypto;
} }
// ////////////////////////////////////////////////////////////////////////////
// Fields
final LocalAccount localAccount; final LocalAccount localAccount;
final UserLogin userLogin; final UserLogin userLogin;
@override
List<Object?> get props => [localAccount, userLogin];
} }

View file

@ -131,9 +131,8 @@ class VeilidChatApp extends StatelessWidget {
PreferencesCubit(PreferencesRepository.instance), PreferencesCubit(PreferencesRepository.instance),
), ),
BlocProvider<AccountRecordsBlocMapCubit>( BlocProvider<AccountRecordsBlocMapCubit>(
create: (context) => create: (context) => AccountRecordsBlocMapCubit(
AccountRecordsBlocMapCubit(AccountRepository.instance) AccountRepository.instance, context.read)),
..follow(context.read<UserLoginsCubit>())),
], ],
child: BackgroundTicker( child: BackgroundTicker(
child: _buildShortcuts( child: _buildShortcuts(
@ -141,7 +140,7 @@ class VeilidChatApp extends StatelessWidget {
builder: (context) => MaterialApp.router( builder: (context) => MaterialApp.router(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
routerConfig: routerConfig:
context.watch<RouterCubit>().router(), context.read<RouterCubit>().router(),
title: translate('app.title'), title: translate('app.title'),
theme: theme, theme: theme,
localizationsDelegates: [ localizationsDelegates: [

View file

@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:provider/provider.dart';
import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
@ -44,23 +45,28 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
// ignore: prefer_constructors_over_static_methods // ignore: prefer_constructors_over_static_methods
static ChatComponentCubit singleContact( static ChatComponentCubit singleContact(
{required UnlockedAccountInfo activeAccountInfo, {required Locator locator,
required proto.Account accountRecordInfo, required ActiveConversationCubit activeConversationCubit,
required ActiveConversationState activeConversationState,
required SingleContactMessagesCubit messagesCubit}) { required SingleContactMessagesCubit messagesCubit}) {
// Get account info
final unlockedAccountInfo =
locator<ActiveAccountInfoCubit>().state.unlockedAccountInfo!;
final account = locator<AccountRecordCubit>().state.asData!.value;
// Make local 'User' // Make local 'User'
final localUserIdentityKey = activeAccountInfo.identityTypedPublicKey; final localUserIdentityKey = unlockedAccountInfo.identityTypedPublicKey;
final localUser = types.User( final localUser = types.User(
id: localUserIdentityKey.toString(), id: localUserIdentityKey.toString(),
firstName: accountRecordInfo.profile.name, firstName: account.profile.name,
metadata: {metadataKeyIdentityPublicKey: localUserIdentityKey}); metadata: {metadataKeyIdentityPublicKey: localUserIdentityKey});
// Make remote 'User's // Make remote 'User's
final remoteUsers = { final remoteUsers = {
activeConversationState.contact.identityPublicKey.toVeilid(): types.User( activeConversationState.contact.identityPublicKey.toVeilid(): types.User(
id: activeConversationState.contact.identityPublicKey id: activeConversationState.contact.identityPublicKey
.toVeilid() .toVeilid()
.toString(), .toString(),
firstName: activeConversationState.contact.editedProfile.name, firstName: activeConversationState.contact.displayName,
metadata: { metadata: {
metadataKeyIdentityPublicKey: metadataKeyIdentityPublicKey:
activeConversationState.contact.identityPublicKey.toVeilid() activeConversationState.contact.identityPublicKey.toVeilid()

View file

@ -4,6 +4,7 @@ import 'package:async_tools/async_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.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';
@ -50,13 +51,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 UnlockedAccountInfo activeAccountInfo, required Locator locator,
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, }) : _locator = locator,
_remoteIdentityPublicKey = remoteIdentityPublicKey, _remoteIdentityPublicKey = remoteIdentityPublicKey,
_localConversationRecordKey = localConversationRecordKey, _localConversationRecordKey = localConversationRecordKey,
_localMessagesRecordKey = localMessagesRecordKey, _localMessagesRecordKey = localMessagesRecordKey,
@ -86,6 +87,9 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Initialize everything // Initialize everything
Future<void> _init() async { Future<void> _init() async {
_unlockedAccountInfo =
_locator<ActiveAccountInfoCubit>().state.unlockedAccountInfo!;
_unsentMessagesQueue = PersistentQueue<proto.Message>( _unsentMessagesQueue = PersistentQueue<proto.Message>(
table: 'SingleContactUnsentMessages', table: 'SingleContactUnsentMessages',
key: _remoteConversationRecordKey.toString(), key: _remoteConversationRecordKey.toString(),
@ -111,15 +115,15 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Make crypto // Make crypto
Future<void> _initCrypto() async { Future<void> _initCrypto() async {
_conversationCrypto = await _activeAccountInfo _conversationCrypto = await _unlockedAccountInfo
.makeConversationCrypto(_remoteIdentityPublicKey); .makeConversationCrypto(_remoteIdentityPublicKey);
_senderMessageIntegrity = await MessageIntegrity.create( _senderMessageIntegrity = await MessageIntegrity.create(
author: _activeAccountInfo.identityTypedPublicKey); author: _unlockedAccountInfo.identityTypedPublicKey);
} }
// Open local messages key // Open local messages key
Future<void> _initSentMessagesCubit() async { Future<void> _initSentMessagesCubit() async {
final writer = _activeAccountInfo.identityWriter; final writer = _unlockedAccountInfo.identityWriter;
_sentMessagesCubit = DHTLogCubit( _sentMessagesCubit = DHTLogCubit(
open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer, open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer,
@ -149,7 +153,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
Future<VeilidCrypto> _makeLocalMessagesCrypto() async => Future<VeilidCrypto> _makeLocalMessagesCrypto() async =>
VeilidCryptoPrivate.fromTypedKey( VeilidCryptoPrivate.fromTypedKey(
_activeAccountInfo.userLogin.identitySecret, 'tabledb'); _unlockedAccountInfo.userLogin.identitySecret, 'tabledb');
// Open reconciled chat record key // Open reconciled chat record key
Future<void> _initReconciledMessagesCubit() async { Future<void> _initReconciledMessagesCubit() async {
@ -240,8 +244,10 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
return; return;
} }
_reconciliation.reconcileMessages(_activeAccountInfo.identityTypedPublicKey, _reconciliation.reconcileMessages(
sentMessages, _sentMessagesCubit!); _unlockedAccountInfo.identityTypedPublicKey,
sentMessages,
_sentMessagesCubit!);
// Update the view // Update the view
_renderState(); _renderState();
@ -278,7 +284,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Now sign it // Now sign it
await _senderMessageIntegrity.signMessage( await _senderMessageIntegrity.signMessage(
message, _activeAccountInfo.identitySecretKey); message, _unlockedAccountInfo.identitySecretKey);
} }
// Async process to send messages in the background // Async process to send messages in the background
@ -331,7 +337,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
for (final m in reconciledMessages.windowElements) { for (final m in reconciledMessages.windowElements) {
final isLocal = m.content.author.toVeilid() == final isLocal = m.content.author.toVeilid() ==
_activeAccountInfo.identityTypedPublicKey; _unlockedAccountInfo.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;
@ -369,7 +375,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 = _unlockedAccountInfo.identityTypedPublicKey.toProto()
..timestamp = Veilid.instance.now().toInt64(); ..timestamp = Veilid.instance.now().toInt64();
// Put in the queue // Put in the queue
@ -402,7 +408,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
///////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////
final WaitSet<void> _initWait = WaitSet(); final WaitSet<void> _initWait = WaitSet();
final UnlockedAccountInfo _activeAccountInfo; final Locator _locator;
late final UnlockedAccountInfo _unlockedAccountInfo;
final TypedKey _remoteIdentityPublicKey; final TypedKey _remoteIdentityPublicKey;
final TypedKey _localConversationRecordKey; final TypedKey _localConversationRecordKey;
final TypedKey _localMessagesRecordKey; final TypedKey _localMessagesRecordKey;

View file

@ -22,26 +22,15 @@ 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 active conversation cubit
final activeAccountInfo = context.watch<UnlockedAccountInfo>(); final activeConversationCubit = context
final accountRecordInfo = .select<ActiveConversationsBlocMapCubit, ActiveConversationCubit?>(
context.watch<AccountRecordCubit>().state.asData?.value; (x) => x.tryOperate(localConversationRecordKey,
if (accountRecordInfo == null) { closure: (cubit) => cubit));
return debugPage('should always have an account record here'); if (activeConversationCubit == null) {
}
final avconversation = context.select<ActiveConversationsBlocMapCubit,
AsyncValue<ActiveConversationState>?>(
(x) => x.state[localConversationRecordKey]);
if (avconversation == 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,
@ -55,9 +44,8 @@ class ChatComponentWidget extends StatelessWidget {
// Make chat component state // Make chat component state
return BlocProvider( return BlocProvider(
create: (context) => ChatComponentCubit.singleContact( create: (context) => ChatComponentCubit.singleContact(
activeAccountInfo: activeAccountInfo, locator: context.read,
accountRecordInfo: accountRecordInfo, activeConversationCubit: activeConversationCubit,
activeConversationState: activeConversationState,
messagesCubit: messagesCubit, messagesCubit: messagesCubit,
), ),
child: ChatComponentWidget._(key: key)); child: ChatComponentWidget._(key: key));

View file

@ -3,9 +3,9 @@ import 'dart:async';
import 'package:bloc_advanced_tools/bloc_advanced_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:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.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'; import '../../tools/tools.dart';
@ -19,21 +19,17 @@ 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 UnlockedAccountInfo unlockedAccountInfo, required Locator locator,
required proto.Account account, required TypedKey accountRecordKey,
required this.activeChatCubit, required OwnedDHTRecordPointer chatListRecordPointer,
}) : super( }) : _locator = locator,
open: () => _open(unlockedAccountInfo, account), super(
open: () => _open(locator, accountRecordKey, chatListRecordPointer),
decodeElement: proto.Chat.fromBuffer); decodeElement: proto.Chat.fromBuffer);
static Future<DHTShortArray> _open( static Future<DHTShortArray> _open(Locator locator, TypedKey accountRecordKey,
UnlockedAccountInfo activeAccountInfo, proto.Account account) async { OwnedDHTRecordPointer chatListRecordPointer) async {
final accountRecordKey = final dhtRecord = await DHTShortArray.openOwned(chatListRecordPointer,
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final chatListRecordKey = account.chatList.toVeilid();
final dhtRecord = await DHTShortArray.openOwned(chatListRecordKey,
debugName: 'ChatListCubit::_open::ChatList', parent: accountRecordKey); debugName: 'ChatListCubit::_open::ChatList', parent: accountRecordKey);
return dhtRecord; return dhtRecord;
@ -41,11 +37,11 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
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;
} }
@ -99,6 +95,7 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
final deletedItem = final deletedItem =
// Ensure followers get their changes before we return // Ensure followers get their changes before we return
await syncFollowers(() => operateWrite((writer) async { await syncFollowers(() => operateWrite((writer) async {
final activeChatCubit = _locator<ActiveChatCubit>();
if (activeChatCubit.state == localConversationRecordKey) { if (activeChatCubit.state == localConversationRecordKey) {
activeChatCubit.setActiveChat(null); activeChatCubit.setActiveChat(null);
} }
@ -142,5 +139,5 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
final ActiveChatCubit activeChatCubit; final Locator _locator;
} }

View file

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

View file

@ -53,10 +53,13 @@ class ChatSingleContactListWidget extends StatelessWidget {
if (contact == null) { if (contact == null) {
return false; return false;
} }
return contact.editedProfile.name return contact.nickname
.toLowerCase() .toLowerCase()
.contains(lowerValue) || .contains(lowerValue) ||
contact.editedProfile.pronouns contact.profile.name
.toLowerCase()
.contains(lowerValue) ||
contact.profile.pronouns
.toLowerCase() .toLowerCase()
.contains(lowerValue); .contains(lowerValue);
}).toList(); }).toList();

View file

@ -4,6 +4,7 @@ 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/foundation.dart'; import 'package:flutter/foundation.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';
@ -36,22 +37,18 @@ class ContactInvitationListCubit
StateMapFollowable<ContactInvitiationListState, TypedKey, StateMapFollowable<ContactInvitiationListState, TypedKey,
proto.ContactInvitationRecord> { proto.ContactInvitationRecord> {
ContactInvitationListCubit({ ContactInvitationListCubit({
required UnlockedAccountInfo unlockedAccountInfo, required Locator locator,
required proto.Account account, required TypedKey accountRecordKey,
}) : _activeAccountInfo = unlockedAccountInfo, required OwnedDHTRecordPointer contactInvitationListRecordPointer,
_account = account, }) : _locator = locator,
_accountRecordKey = accountRecordKey,
super( super(
open: () => _open(unlockedAccountInfo, account), open: () =>
_open(accountRecordKey, contactInvitationListRecordPointer),
decodeElement: proto.ContactInvitationRecord.fromBuffer); decodeElement: proto.ContactInvitationRecord.fromBuffer);
static Future<DHTShortArray> _open( static Future<DHTShortArray> _open(TypedKey accountRecordKey,
UnlockedAccountInfo 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',
@ -71,8 +68,12 @@ 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 activeAccountInfo =
final identityWriter = _activeAccountInfo.identityWriter; _locator<ActiveAccountInfoCubit>().state.unlockedAccountInfo!;
final profile = _locator<AccountRecordCubit>().state.asData!.value.profile;
final idcs = await activeAccountInfo.identityCryptoSystem;
final identityWriter = activeAccountInfo.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 +91,7 @@ class ContactInvitationListCubit
await (await pool.createRecord( await (await pool.createRecord(
debugName: 'ContactInvitationListCubit::createInvitation::' debugName: 'ContactInvitationListCubit::createInvitation::'
'LocalConversation', 'LocalConversation',
parent: _activeAccountInfo.accountRecordKey, parent: _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 +100,9 @@ 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 =
_activeAccountInfo.userLogin.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 +120,7 @@ class ContactInvitationListCubit
await (await pool.createRecord( await (await pool.createRecord(
debugName: 'ContactInvitationListCubit::createInvitation::' debugName: 'ContactInvitationListCubit::createInvitation::'
'ContactRequestInbox', 'ContactRequestInbox',
parent: _activeAccountInfo.accountRecordKey, parent: _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)
]), ]),
@ -172,8 +173,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 +197,7 @@ class ContactInvitationListCubit
await (await pool.openRecordOwned(contactRequestInbox, await (await pool.openRecordOwned(contactRequestInbox,
debugName: 'ContactInvitationListCubit::deleteInvitation::' debugName: 'ContactInvitationListCubit::deleteInvitation::'
'ContactRequestInbox', 'ContactRequestInbox',
parent: accountRecordKey)) parent: _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));
@ -248,7 +247,7 @@ class ContactInvitationListCubit
await (await pool.openRecordRead(contactRequestInboxKey, await (await pool.openRecordRead(contactRequestInboxKey,
debugName: 'ContactInvitationListCubit::validateInvitation::' debugName: 'ContactInvitationListCubit::validateInvitation::'
'ContactRequestInbox', 'ContactRequestInbox',
parent: _activeAccountInfo.accountRecordKey)) parent: _accountRecordKey))
.maybeDeleteScope(!isSelf, (contactRequestInbox) async { .maybeDeleteScope(!isSelf, (contactRequestInbox) async {
// //
final contactRequest = await contactRequestInbox final contactRequest = await contactRequestInbox
@ -293,8 +292,7 @@ class ContactInvitationListCubit
secret: writerSecret); secret: writerSecret);
out = ValidContactInvitation( out = ValidContactInvitation(
activeAccountInfo: _activeAccountInfo, locator: _locator,
account: _account,
contactRequestInboxKey: contactRequestInboxKey, contactRequestInboxKey: contactRequestInboxKey,
contactRequestPrivate: contactRequestPrivate, contactRequestPrivate: contactRequestPrivate,
contactSuperIdentity: contactSuperIdentity, contactSuperIdentity: contactSuperIdentity,
@ -318,6 +316,6 @@ class ContactInvitationListCubit
} }
// //
final UnlockedAccountInfo _activeAccountInfo; final Locator _locator;
final proto.Account _account; final TypedKey _accountRecordKey;
} }

View file

@ -1,3 +1,4 @@
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';
@ -7,27 +8,24 @@ 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 Locator locator, required this.contactInvitationRecord})
: super( : super(
open: () => _open( open: () => _open(
activeAccountInfo: activeAccountInfo, locator: locator,
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 UnlockedAccountInfo activeAccountInfo, {required Locator locator,
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 unlockedAccountInfo =
locator<ActiveAccountInfoCubit>().state.unlockedAccountInfo!;
final accountRecordKey = unlockedAccountInfo.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 +40,5 @@ class ContactRequestInboxCubit
defaultSubkey: 1); defaultSubkey: 1);
} }
final UnlockedAccountInfo activeAccountInfo;
final proto.ContactInvitationRecord contactInvitationRecord; final proto.ContactInvitationRecord contactInvitationRecord;
} }

View file

@ -4,9 +4,9 @@ import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.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 '../../conversation/conversation.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';
@ -25,24 +25,22 @@ class InvitationStatus extends Equatable {
class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus, class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
proto.SignedContactResponse?> { proto.SignedContactResponse?> {
WaitingInvitationCubit(ContactRequestInboxCubit super.input, WaitingInvitationCubit(ContactRequestInboxCubit super.input,
{required UnlockedAccountInfo activeAccountInfo, {required Locator locator,
required proto.Account account,
required proto.ContactInvitationRecord contactInvitationRecord}) required proto.ContactInvitationRecord contactInvitationRecord})
: super( : super(
transform: (signedContactResponse) => _transform( transform: (signedContactResponse) => _transform(
signedContactResponse, signedContactResponse,
activeAccountInfo: activeAccountInfo, locator: locator,
account: account,
contactInvitationRecord: contactInvitationRecord)); contactInvitationRecord: contactInvitationRecord));
static Future<AsyncValue<InvitationStatus>> _transform( static Future<AsyncValue<InvitationStatus>> _transform(
proto.SignedContactResponse? signedContactResponse, proto.SignedContactResponse? signedContactResponse,
{required UnlockedAccountInfo activeAccountInfo, {required Locator locator,
required proto.Account account,
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 =
@ -71,7 +69,7 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
contactResponse.remoteConversationRecordKey.toVeilid(); contactResponse.remoteConversationRecordKey.toVeilid();
final conversation = ConversationCubit( final conversation = ConversationCubit(
activeAccountInfo: activeAccountInfo, locator: locator,
remoteIdentityPublicKey: remoteIdentityPublicKey:
contactSuperIdentity.currentInstance.typedPublicKey, contactSuperIdentity.currentInstance.typedPublicKey,
remoteConversationRecordKey: remoteConversationRecordKey); remoteConversationRecordKey: remoteConversationRecordKey);
@ -99,15 +97,11 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
contactInvitationRecord.localConversationRecordKey.toVeilid(); contactInvitationRecord.localConversationRecordKey.toVeilid();
return conversation.initLocalConversation( return conversation.initLocalConversation(
existingConversationRecordKey: localConversationRecordKey, existingConversationRecordKey: localConversationRecordKey,
profile: account.profile, callback: (localConversation) async => AsyncValue.data(InvitationStatus(
// ignore: prefer_expression_function_bodies
callback: (localConversation) async {
return AsyncValue.data(InvitationStatus(
acceptedContact: AcceptedContact( acceptedContact: AcceptedContact(
remoteProfile: remoteProfile, remoteProfile: remoteProfile,
remoteIdentity: contactSuperIdentity, remoteIdentity: contactSuperIdentity,
remoteConversationRecordKey: remoteConversationRecordKey, remoteConversationRecordKey: remoteConversationRecordKey,
localConversationRecordKey: localConversationRecordKey))); localConversationRecordKey: localConversationRecordKey))));
});
} }
} }

View file

@ -1,8 +1,8 @@
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:bloc_advanced_tools/bloc_advanced_tools.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 '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import 'cubits.dart'; import 'cubits.dart';
@ -17,8 +17,12 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
with with
StateMapFollower<DHTShortArrayBusyState<proto.ContactInvitationRecord>, StateMapFollower<DHTShortArrayBusyState<proto.ContactInvitationRecord>,
TypedKey, proto.ContactInvitationRecord> { TypedKey, proto.ContactInvitationRecord> {
WaitingInvitationsBlocMapCubit( WaitingInvitationsBlocMapCubit({
{required this.unlockedAccountInfo, required this.account}); required Locator locator,
}) : _locator = locator {
// Follow the contact invitation list cubit
follow(locator<ContactInvitationListCubit>());
}
Future<void> _addWaitingInvitation( Future<void> _addWaitingInvitation(
{required proto.ContactInvitationRecord {required proto.ContactInvitationRecord
@ -27,10 +31,9 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(), contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(),
WaitingInvitationCubit( WaitingInvitationCubit(
ContactRequestInboxCubit( ContactRequestInboxCubit(
activeAccountInfo: unlockedAccountInfo, locator: _locator,
contactInvitationRecord: contactInvitationRecord), contactInvitationRecord: contactInvitationRecord),
activeAccountInfo: unlockedAccountInfo, locator: _locator,
account: account,
contactInvitationRecord: contactInvitationRecord))); contactInvitationRecord: contactInvitationRecord)));
/// StateFollower ///////////////////////// /// StateFollower /////////////////////////
@ -43,6 +46,5 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
_addWaitingInvitation(contactInvitationRecord: value); _addWaitingInvitation(contactInvitationRecord: value);
//// ////
final UnlockedAccountInfo unlockedAccountInfo; final Locator _locator;
final proto.Account account;
} }

View file

@ -1,4 +1,5 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.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';
@ -13,14 +14,12 @@ import 'models.dart';
class ValidContactInvitation { class ValidContactInvitation {
@internal @internal
ValidContactInvitation( ValidContactInvitation(
{required UnlockedAccountInfo activeAccountInfo, {required Locator locator,
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, : _locator = locator,
_account = account,
_contactRequestInboxKey = contactRequestInboxKey, _contactRequestInboxKey = contactRequestInboxKey,
_contactRequestPrivate = contactRequestPrivate, _contactRequestPrivate = contactRequestPrivate,
_contactSuperIdentity = contactSuperIdentity, _contactSuperIdentity = contactSuperIdentity,
@ -31,11 +30,15 @@ class ValidContactInvitation {
Future<AcceptedContact?> accept() async { Future<AcceptedContact?> accept() async {
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
try { try {
final unlockedAccountInfo =
_locator<ActiveAccountInfoCubit>().state.unlockedAccountInfo!;
final accountRecordKey = unlockedAccountInfo.accountRecordKey;
final identityPublicKey = unlockedAccountInfo.identityPublicKey;
// 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 =
_activeAccountInfo.identityPublicKey; _contactSuperIdentity.currentInstance.publicKey == identityPublicKey;
final accountRecordKey = _activeAccountInfo.accountRecordKey;
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
debugName: 'ValidContactInvitation::accept::' debugName: 'ValidContactInvitation::accept::'
@ -46,25 +49,24 @@ class ValidContactInvitation {
// 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, locator: _locator,
remoteIdentityPublicKey: remoteIdentityPublicKey:
_contactSuperIdentity.currentInstance.typedPublicKey); _contactSuperIdentity.currentInstance.typedPublicKey);
return conversation.initLocalConversation( return conversation.initLocalConversation(
profile: _account.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(); unlockedAccountInfo.superIdentityRecordKey.toProto();
final contactResponseBytes = contactResponse.writeToBuffer(); final contactResponseBytes = contactResponse.writeToBuffer();
final cs = await pool.veilid final cs =
.getCryptoSystem(_contactRequestInboxKey.kind); await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind);
final identitySignature = await cs.sign( final identitySignature = await cs.sign(
_activeAccountInfo.identityWriter.key, unlockedAccountInfo.identityWriter.key,
_activeAccountInfo.identityWriter.secret, unlockedAccountInfo.identityWriter.secret,
contactResponseBytes); contactResponseBytes);
final signedContactResponse = proto.SignedContactResponse() final signedContactResponse = proto.SignedContactResponse()
@ -72,8 +74,8 @@ class ValidContactInvitation {
..identitySignature = identitySignature.toProto(); ..identitySignature = identitySignature.toProto();
// Write the acceptance to the inbox // Write the acceptance to the inbox
await contactRequestInbox await contactRequestInbox.eventualWriteProtobuf(signedContactResponse,
.eventualWriteProtobuf(signedContactResponse, subkey: 1); subkey: 1);
return AcceptedContact( return AcceptedContact(
remoteProfile: _contactRequestPrivate.profile, remoteProfile: _contactRequestPrivate.profile,
@ -93,10 +95,14 @@ class ValidContactInvitation {
Future<bool> reject() async { Future<bool> reject() async {
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
final unlockedAccountInfo =
_locator<ActiveAccountInfoCubit>().state.unlockedAccountInfo!;
final accountRecordKey = unlockedAccountInfo.accountRecordKey;
final identityPublicKey = unlockedAccountInfo.identityPublicKey;
// 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 =
_activeAccountInfo.identityPublicKey; _contactSuperIdentity.currentInstance.publicKey == identityPublicKey;
final accountRecordKey = _activeAccountInfo.accountRecordKey;
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
debugName: 'ValidContactInvitation::reject::' debugName: 'ValidContactInvitation::reject::'
@ -109,12 +115,12 @@ class ValidContactInvitation {
final contactResponse = proto.ContactResponse() final contactResponse = proto.ContactResponse()
..accept = false ..accept = false
..superIdentityRecordKey = ..superIdentityRecordKey =
_activeAccountInfo.superIdentityRecordKey.toProto(); unlockedAccountInfo.superIdentityRecordKey.toProto();
final contactResponseBytes = contactResponse.writeToBuffer(); final contactResponseBytes = contactResponse.writeToBuffer();
final identitySignature = await cs.sign( final identitySignature = await cs.sign(
_activeAccountInfo.identityWriter.key, unlockedAccountInfo.identityWriter.key,
_activeAccountInfo.identityWriter.secret, unlockedAccountInfo.identityWriter.secret,
contactResponseBytes); contactResponseBytes);
final signedContactResponse = proto.SignedContactResponse() final signedContactResponse = proto.SignedContactResponse()
@ -129,8 +135,7 @@ class ValidContactInvitation {
} }
// //
final UnlockedAccountInfo _activeAccountInfo; final Locator _locator;
final proto.Account _account;
final TypedKey _contactRequestInboxKey; final TypedKey _contactRequestInboxKey;
final SuperIdentity _contactSuperIdentity; final SuperIdentity _contactSuperIdentity;
final KeyPair _writer; final KeyPair _writer;

View file

@ -3,8 +3,8 @@ import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
@ -15,13 +15,14 @@ import '../contact_invitation.dart';
class InvitationDialog extends StatefulWidget { class InvitationDialog extends StatefulWidget {
const InvitationDialog( const InvitationDialog(
{required this.modalContext, {required Locator locator,
required this.onValidationCancelled, required this.onValidationCancelled,
required this.onValidationSuccess, required this.onValidationSuccess,
required this.onValidationFailed, required this.onValidationFailed,
required this.inviteControlIsValid, required this.inviteControlIsValid,
required this.buildInviteControl, required this.buildInviteControl,
super.key}); super.key})
: _locator = locator;
final void Function() onValidationCancelled; final void Function() onValidationCancelled;
final void Function() onValidationSuccess; final void Function() onValidationSuccess;
@ -32,7 +33,7 @@ class InvitationDialog extends StatefulWidget {
InvitationDialogState dialogState, InvitationDialogState dialogState,
Future<void> Function({required Uint8List inviteData}) Future<void> Function({required Uint8List inviteData})
validateInviteData) buildInviteControl; validateInviteData) buildInviteControl;
final BuildContext modalContext; final Locator _locator;
@override @override
InvitationDialogState createState() => InvitationDialogState(); InvitationDialogState createState() => InvitationDialogState();
@ -54,8 +55,7 @@ class InvitationDialog extends StatefulWidget {
InvitationDialogState dialogState, InvitationDialogState dialogState,
Future<void> Function({required Uint8List inviteData}) Future<void> Function({required Uint8List inviteData})
validateInviteData)>.has( validateInviteData)>.has(
'buildInviteControl', buildInviteControl)) 'buildInviteControl', buildInviteControl));
..add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
} }
} }
@ -74,8 +74,8 @@ 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<UnlockedAccountInfo>(); final activeAccountInfo = widget._locator<UnlockedAccountInfo>();
final contactList = widget.modalContext.read<ContactListCubit>(); final contactList = widget._locator<ContactListCubit>();
setState(() { setState(() {
_isAccepting = true; _isAccepting = true;
@ -90,7 +90,7 @@ class InvitationDialogState extends State<InvitationDialog> {
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 +137,7 @@ class InvitationDialogState extends State<InvitationDialog> {
}) async { }) async {
try { try {
final contactInvitationListCubit = final contactInvitationListCubit =
widget.modalContext.read<ContactInvitationListCubit>(); widget._locator<ContactInvitationListCubit>();
setState(() { setState(() {
_isValidating = true; _isValidating = true;

View file

@ -3,33 +3,29 @@ import 'dart:convert';
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.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 '../../conversation/conversation.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../../conversation/cubits/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 UnlockedAccountInfo unlockedAccountInfo, required Locator locator,
required proto.Account account, required TypedKey accountRecordKey,
}) : _activeAccountInfo = unlockedAccountInfo, required OwnedDHTRecordPointer contactListRecordPointer,
}) : _locator = locator,
super( super(
open: () => _open(unlockedAccountInfo, account), open: () => _open(accountRecordKey, contactListRecordPointer),
decodeElement: proto.Contact.fromBuffer); decodeElement: proto.Contact.fromBuffer);
static Future<DHTShortArray> _open( static Future<DHTShortArray> _open(TypedKey accountRecordKey,
UnlockedAccountInfo 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);
@ -52,15 +48,15 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
if (remoteProfile == null) { if (remoteProfile == null) {
return; return;
} }
return updateContactRemoteProfile( return updateContactProfile(
localConversationRecordKey: localConversationRecordKey, localConversationRecordKey: localConversationRecordKey,
remoteProfile: remoteProfile); profile: remoteProfile);
}); });
} }
Future<void> updateContactRemoteProfile({ Future<void> updateContactProfile({
required TypedKey localConversationRecordKey, required TypedKey localConversationRecordKey,
required proto.Profile remoteProfile, required proto.Profile profile,
}) async { }) async {
// Update contact's remoteProfile // Update contact's remoteProfile
await operateWriteEventual((writer) async { await operateWriteEventual((writer) async {
@ -69,36 +65,36 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
if (c != null && if (c != null &&
c.localConversationRecordKey.toVeilid() == c.localConversationRecordKey.toVeilid() ==
localConversationRecordKey) { localConversationRecordKey) {
if (c.remoteProfile == remoteProfile) { if (c.profile == profile) {
// Unchanged // Unchanged
break; break;
} }
final newContact = c.deepCopy()..remoteProfile = remoteProfile; final newContact = c.deepCopy()..profile = profile;
final updated = await writer.tryWriteItemProtobuf( final updated = await writer.tryWriteItemProtobuf(
proto.Contact.fromBuffer, pos, newContact); proto.Contact.fromBuffer, pos, newContact);
if (!updated) { if (!updated) {
throw DHTExceptionTryAgain(); throw DHTExceptionTryAgain();
} }
break;
} }
} }
}); });
} }
Future<void> createContact({ Future<void> createContact({
required proto.Profile remoteProfile, required proto.Profile profile,
required SuperIdentity remoteSuperIdentity, required SuperIdentity remoteSuperIdentity,
required TypedKey remoteConversationRecordKey,
required TypedKey localConversationRecordKey, 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
@ -108,13 +104,8 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
}); });
} }
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 operateWrite((writer) async {
for (var i = 0; i < writer.length; i++) { for (var i = 0; i < writer.length; i++) {
@ -122,8 +113,8 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
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;
} }
@ -135,10 +126,12 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
try { try {
// Make a conversation cubit to manipulate the conversation // Make a conversation cubit to manipulate the conversation
final conversationCubit = ConversationCubit( final conversationCubit = ConversationCubit(
activeAccountInfo: _activeAccountInfo, locator: _locator,
remoteIdentityPublicKey: remoteIdentityPublicKey, remoteIdentityPublicKey: deletedItem.identityPublicKey.toVeilid(),
localConversationRecordKey: localConversationRecordKey, localConversationRecordKey:
remoteConversationRecordKey: remoteConversationRecordKey, deletedItem.localConversationRecordKey.toVeilid(),
remoteConversationRecordKey:
deletedItem.remoteConversationRecordKey.toVeilid(),
); );
// Delete the local and remote conversation records // Delete the local and remote conversation records
@ -149,7 +142,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
} }
} }
final UnlockedAccountInfo _activeAccountInfo;
final _contactProfileUpdateMap = final _contactProfileUpdateMap =
SingleStateProcessorMap<TypedKey, proto.Profile?>(); SingleStateProcessorMap<TypedKey, proto.Profile?>();
final Locator _locator;
} }

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.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';
@ -44,12 +45,11 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<ActiveConversationState>, ActiveConversationCubit> AsyncValue<ActiveConversationState>, ActiveConversationCubit>
with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> { with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> {
ActiveConversationsBlocMapCubit({ ActiveConversationsBlocMapCubit({
required UnlockedAccountInfo unlockedAccountInfo, required Locator locator,
required ContactListCubit contactListCubit, }) : _locator = locator {
required AccountRecordCubit accountRecordCubit, // Follow the chat list cubit
}) : _activeAccountInfo = unlockedAccountInfo, follow(locator<ChatListCubit>());
_contactListCubit = contactListCubit, }
_accountRecordCubit = accountRecordCubit;
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Public Interface // Public Interface
@ -69,13 +69,20 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
// Conversation cubit the tracks the state between the local // Conversation cubit the tracks the state between the local
// and remote halves of a contact's relationship with this account // and remote halves of a contact's relationship with this account
final conversationCubit = ConversationCubit( final conversationCubit = ConversationCubit(
activeAccountInfo: _activeAccountInfo, locator: _locator,
remoteIdentityPublicKey: remoteIdentityPublicKey, remoteIdentityPublicKey: remoteIdentityPublicKey,
localConversationRecordKey: localConversationRecordKey, localConversationRecordKey: localConversationRecordKey,
remoteConversationRecordKey: remoteConversationRecordKey, remoteConversationRecordKey: remoteConversationRecordKey,
)..watchAccountChanges( );
_accountRecordCubit.stream, _accountRecordCubit.state);
_contactListCubit.followContactProfileChanges( // When our local account profile changes, send it to the conversation
final accountRecordCubit = _locator<AccountRecordCubit>();
conversationCubit.watchAccountChanges(
accountRecordCubit.stream, accountRecordCubit.state);
// When remote conversation changes its profile,
// update our local contact
_locator<ContactListCubit>().followContactProfileChanges(
localConversationRecordKey, localConversationRecordKey,
conversationCubit.stream.map((x) => x.map( conversationCubit.stream.map((x) => x.map(
data: (d) => d.value.remoteConversation?.profile, data: (d) => d.value.remoteConversation?.profile,
@ -112,7 +119,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
@override @override
Future<void> updateState(TypedKey key, proto.Chat value) async { Future<void> updateState(TypedKey key, proto.Chat value) async {
final contactList = _contactListCubit.state.state.asData?.value; final contactList = _locator<ContactListCubit>().state.state.asData?.value;
if (contactList == null) { if (contactList == null) {
await addState(key, const AsyncValue.loading()); await addState(key, const AsyncValue.loading());
return; return;
@ -129,7 +136,5 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
//// ////
final UnlockedAccountInfo _activeAccountInfo; final Locator _locator;
final ContactListCubit _contactListCubit;
final AccountRecordCubit _accountRecordCubit;
} }

View file

@ -2,14 +2,14 @@ import 'dart:async';
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:bloc_advanced_tools/bloc_advanced_tools.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 '../../chat/chat.dart'; import '../../chat/chat.dart';
import '../../chat_list/cubits/chat_list_cubit.dart';
import '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import 'active_conversations_bloc_map_cubit.dart'; import 'active_conversations_bloc_map_cubit.dart';
import '../../chat_list/cubits/chat_list_cubit.dart';
// Map of localConversationRecordKey to MessagesCubit // Map of localConversationRecordKey to MessagesCubit
// Wraps a MessagesCubit to stream the latest messages to the state // Wraps a MessagesCubit to stream the latest messages to the state
@ -19,13 +19,11 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
with with
StateMapFollower<ActiveConversationsBlocMapState, TypedKey, StateMapFollower<ActiveConversationsBlocMapState, TypedKey,
AsyncValue<ActiveConversationState>> { AsyncValue<ActiveConversationState>> {
ActiveSingleContactChatBlocMapCubit( ActiveSingleContactChatBlocMapCubit({required Locator locator})
{required UnlockedAccountInfo unlockedAccountInfo, : _locator = locator {
required ContactListCubit contactListCubit, // Follow the active conversations bloc map cubit
required ChatListCubit chatListCubit}) follow(locator<ActiveConversationsBlocMapCubit>());
: _activeAccountInfo = unlockedAccountInfo, }
_contactListCubit = contactListCubit,
_chatListCubit = chatListCubit;
Future<void> _addConversationMessages( Future<void> _addConversationMessages(
{required proto.Contact contact, {required proto.Contact contact,
@ -35,7 +33,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
add(() => MapEntry( add(() => MapEntry(
contact.localConversationRecordKey.toVeilid(), contact.localConversationRecordKey.toVeilid(),
SingleContactMessagesCubit( SingleContactMessagesCubit(
activeAccountInfo: _activeAccountInfo, locator: _locator,
remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(),
localConversationRecordKey: localConversationRecordKey:
contact.localConversationRecordKey.toVeilid(), contact.localConversationRecordKey.toVeilid(),
@ -54,7 +52,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
Future<void> updateState( Future<void> updateState(
TypedKey key, AsyncValue<ActiveConversationState> value) async { TypedKey key, AsyncValue<ActiveConversationState> value) async {
// Get the contact object for this single contact chat // Get the contact object for this single contact chat
final contactList = _contactListCubit.state.state.asData?.value; final contactList = _locator<ContactListCubit>().state.state.asData?.value;
if (contactList == null) { if (contactList == null) {
await addState(key, const AsyncValue.loading()); await addState(key, const AsyncValue.loading());
return; return;
@ -69,7 +67,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
final contact = contactList[contactIndex].value; final contact = contactList[contactIndex].value;
// Get the chat object for this single contact chat // Get the chat object for this single contact chat
final chatList = _chatListCubit.state.state.asData?.value; final chatList = _locator<ChatListCubit>().state.state.asData?.value;
if (chatList == null) { if (chatList == null) {
await addState(key, const AsyncValue.loading()); await addState(key, const AsyncValue.loading());
return; return;
@ -95,7 +93,5 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
//// ////
final UnlockedAccountInfo _activeAccountInfo; final Locator _locator;
final ContactListCubit _contactListCubit;
final ChatListCubit _chatListCubit;
} }

View file

@ -10,6 +10,7 @@ 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:protobuf/protobuf.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';
@ -35,29 +36,31 @@ class ConversationState extends Equatable {
/// 1-1 chats /// 1-1 chats
class ConversationCubit extends Cubit<AsyncValue<ConversationState>> { class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
ConversationCubit( ConversationCubit(
{required UnlockedAccountInfo activeAccountInfo, {required Locator locator,
required TypedKey remoteIdentityPublicKey, required TypedKey remoteIdentityPublicKey,
TypedKey? localConversationRecordKey, TypedKey? localConversationRecordKey,
TypedKey? remoteConversationRecordKey}) TypedKey? remoteConversationRecordKey})
: _unlockedAccountInfo = activeAccountInfo, : _locator = locator,
_localConversationRecordKey = localConversationRecordKey, _localConversationRecordKey = localConversationRecordKey,
_remoteIdentityPublicKey = remoteIdentityPublicKey, _remoteIdentityPublicKey = remoteIdentityPublicKey,
_remoteConversationRecordKey = remoteConversationRecordKey, _remoteConversationRecordKey = remoteConversationRecordKey,
super(const AsyncValue.loading()) { super(const AsyncValue.loading()) {
final unlockedAccountInfo =
_locator<ActiveAccountInfoCubit>().state.unlockedAccountInfo!;
_accountRecordKey = unlockedAccountInfo.accountRecordKey;
_identityWriter = unlockedAccountInfo.identityWriter;
if (_localConversationRecordKey != null) { if (_localConversationRecordKey != null) {
_initWait.add(() async { _initWait.add(() async {
await _setLocalConversation(() async { await _setLocalConversation(() async {
final accountRecordKey = _unlockedAccountInfo
.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 = _unlockedAccountInfo.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: _accountRecordKey,
crypto: crypto); crypto: crypto);
return record; return record;
@ -68,15 +71,12 @@ 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 = _unlockedAccountInfo
.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: _accountRecordKey,
crypto: crypto); crypto: crypto);
return record; return record;
}); });
@ -107,18 +107,19 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
/// The callback allows for more initialization to occur and for /// The callback allows for more initialization to occur and for
/// cleanup to delete records upon failure of the callback /// cleanup to delete records upon failure of the callback
Future<T> initLocalConversation<T>( Future<T> initLocalConversation<T>(
{required proto.Profile profile, {required FutureOr<T> Function(DHTRecord) callback,
required FutureOr<T> Function(DHTRecord) callback,
TypedKey? existingConversationRecordKey}) async { TypedKey? existingConversationRecordKey}) async {
assert(_localConversationRecordKey == null, assert(_localConversationRecordKey == null,
'must not have a local conversation yet'); 'must not have a local conversation yet');
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
final accountRecordKey = _unlockedAccountInfo
.userLogin.accountRecordInfo.accountRecord.recordKey;
final crypto = await _cachedConversationCrypto(); final crypto = await _cachedConversationCrypto();
final writer = _unlockedAccountInfo.identityWriter; final account = _locator<AccountRecordCubit>().state.asData!.value;
final unlockedAccountInfo =
_locator<ActiveAccountInfoCubit>().state.unlockedAccountInfo!;
final accountRecordKey = unlockedAccountInfo.accountRecordKey;
final writer = unlockedAccountInfo.identityWriter;
// Open with SMPL scheme for identity writer // Open with SMPL scheme for identity writer
late final DHTRecord localConversationRecord; late final DHTRecord localConversationRecord;
@ -144,15 +145,13 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
.deleteScope((localConversation) async { .deleteScope((localConversation) async {
// Make messages log // Make messages log
return _initLocalMessages( return _initLocalMessages(
activeAccountInfo: _unlockedAccountInfo,
remoteIdentityPublicKey: _remoteIdentityPublicKey,
localConversationKey: localConversation.key, localConversationKey: localConversation.key,
callback: (messages) async { callback: (messages) async {
// Create initial local conversation key contents // Create initial local conversation key contents
final conversation = proto.Conversation() final conversation = proto.Conversation()
..profile = profile ..profile = account.profile
..superIdentityJson = jsonEncode( ..superIdentityJson = jsonEncode(
_unlockedAccountInfo.localAccount.superIdentity.toJson()) unlockedAccountInfo.localAccount.superIdentity.toJson())
..messages = messages.recordKey.toProto(); ..messages = messages.recordKey.toProto();
// Write initial conversation to record // Write initial conversation to record
@ -340,14 +339,11 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
// Initialize local messages // Initialize local messages
Future<T> _initLocalMessages<T>({ Future<T> _initLocalMessages<T>({
required UnlockedAccountInfo 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',
@ -362,17 +358,19 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
if (conversationCrypto != null) { if (conversationCrypto != null) {
return conversationCrypto; return conversationCrypto;
} }
conversationCrypto = await _unlockedAccountInfo final unlockedAccountInfo =
_locator<ActiveAccountInfoCubit>().state.unlockedAccountInfo!;
conversationCrypto = await unlockedAccountInfo
.makeConversationCrypto(_remoteIdentityPublicKey); .makeConversationCrypto(_remoteIdentityPublicKey);
_conversationCrypto = conversationCrypto; _conversationCrypto = conversationCrypto;
return conversationCrypto; return conversationCrypto;
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Fields // Fields
final Locator _locator;
final UnlockedAccountInfo _unlockedAccountInfo; late final TypedKey _accountRecordKey;
late final KeyPair _identityWriter;
final TypedKey _remoteIdentityPublicKey; final TypedKey _remoteIdentityPublicKey;
TypedKey? _localConversationRecordKey; TypedKey? _localConversationRecordKey;
final TypedKey? _remoteConversationRecordKey; final TypedKey? _remoteConversationRecordKey;

View file

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

View file

@ -6,6 +6,7 @@ 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,7 +30,8 @@ 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>()!;
@ -50,9 +52,7 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
await ctrl.toggle?.call(); await ctrl.toggle?.call();
//await GoRouterHelper(context).push('/settings'); //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()
]); ]);
@ -73,7 +73,7 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
} }
return ChatComponentWidget.builder( return ChatComponentWidget.builder(
localConversationRecordKey: activeChatLocalConversationKey, localConversationRecordKey: activeChatLocalConversationKey,
); key: ValueKey(activeChatLocalConversationKey));
} }
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies

View file

@ -1,158 +0,0 @@
import 'package:async_tools/async_tools.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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 '../../../conversation/conversation.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 unlockedAccountInfo = context.watch<UnlockedAccountInfo>();
final routerCubit = context.read<RouterCubit>();
return HomeAccountReadyShell._(
unlockedAccountInfo: unlockedAccountInfo,
routerCubit: routerCubit,
key: key,
child: child);
}
const HomeAccountReadyShell._(
{required this.unlockedAccountInfo,
required this.routerCubit,
required this.child,
super.key});
@override
HomeAccountReadyShellState createState() => HomeAccountReadyShellState();
final Widget child;
final UnlockedAccountInfo unlockedAccountInfo;
final RouterCubit routerCubit;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<UnlockedAccountInfo>(
'unlockedAccountInfo', unlockedAccountInfo))
..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) {
// XXX: Should probably eliminate this in favor
// of streaming changes into other cubits. Too much rebuilding!
// should not need to 'watch' all these cubits
final account = context.watch<AccountRecordCubit>().state.asData?.value;
if (account == null) {
return waitingPage();
}
return MultiBlocProvider(
providers: [
// Contact Cubits
BlocProvider(
create: (context) => ContactInvitationListCubit(
unlockedAccountInfo: widget.unlockedAccountInfo,
account: account)),
BlocProvider(
create: (context) => ContactListCubit(
unlockedAccountInfo: widget.unlockedAccountInfo,
account: account)),
BlocProvider(
create: (context) => WaitingInvitationsBlocMapCubit(
unlockedAccountInfo: widget.unlockedAccountInfo,
account: account)
..follow(context.read<ContactInvitationListCubit>())),
// Chat Cubits
BlocProvider(
create: (context) => ActiveChatCubit(null,
routerCubit: context.read<RouterCubit>())),
BlocProvider(
create: (context) => ChatListCubit(
unlockedAccountInfo: widget.unlockedAccountInfo,
activeChatCubit: context.read<ActiveChatCubit>(),
account: account)),
// Conversation Cubits
BlocProvider(
create: (context) => ActiveConversationsBlocMapCubit(
unlockedAccountInfo: widget.unlockedAccountInfo,
contactListCubit: context.read<ContactListCubit>(),
accountRecordCubit: context.read<AccountRecordCubit>())
..follow(context.read<ChatListCubit>())),
BlocProvider(
create: (context) => ActiveSingleContactChatBlocMapCubit(
unlockedAccountInfo: widget.unlockedAccountInfo,
contactListCubit: context.read<ContactListCubit>(),
chatListCubit: context.read<ChatListCubit>())
..follow(context.read<ActiveConversationsBlocMapCubit>())),
],
child: MultiBlocListener(listeners: [
BlocListener<WaitingInvitationsBlocMapCubit,
WaitingInvitationsBlocMapState>(
listener: _invitationStatusListener,
)
], child: widget.child));
}
}

View file

@ -1,11 +1,20 @@
import 'dart:math'; import 'dart:math';
import 'package:async_tools/async_tools.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_zoom_drawer/flutter_zoom_drawer.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
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_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 '../../router/router.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import 'drawer_menu/drawer_menu.dart'; import 'drawer_menu/drawer_menu.dart';
import 'home_account_invalid.dart'; import 'home_account_invalid.dart';
@ -33,25 +42,133 @@ class HomeShellState extends State<HomeShell> {
super.dispose(); super.dispose();
} }
Widget buildWithLogin(BuildContext context) { // Process all accepted or rejected invitations
final accountInfo = context.watch<ActiveAccountInfoCubit>().state; void _invitationStatusListener(
final accountRecordsCubit = context.watch<AccountRecordsBlocMapCubit>(); BuildContext context, WaitingInvitationsBlocMapState state) {
if (!accountInfo.active) { _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(
profile: acceptedContact.remoteProfile,
remoteSuperIdentity: acceptedContact.remoteIdentity,
remoteConversationRecordKey:
acceptedContact.remoteConversationRecordKey,
localConversationRecordKey:
acceptedContact.localConversationRecordKey,
);
} else {
// Reject
await contactInvitationListCubit.deleteInvitation(
accepted: false,
contactRequestInboxRecordKey: contactRequestInboxRecordKey);
}
}
});
}
Widget _buildActiveAccount(BuildContext context) {
final accountRecordKey = context.select<ActiveAccountInfoCubit, TypedKey>(
(c) => c.state.unlockedAccountInfo!.accountRecordKey);
final contactListRecordPointer =
context.select<AccountRecordCubit, OwnedDHTRecordPointer?>(
(c) => c.state.asData?.value.contactList.toVeilid());
final contactInvitationListRecordPointer =
context.select<AccountRecordCubit, OwnedDHTRecordPointer?>(
(c) => c.state.asData?.value.contactInvitationRecords.toVeilid());
final chatListRecordPointer =
context.select<AccountRecordCubit, OwnedDHTRecordPointer?>(
(c) => c.state.asData?.value.chatList.toVeilid());
if (contactListRecordPointer == null ||
contactInvitationListRecordPointer == null ||
chatListRecordPointer == null) {
return waitingPage();
}
return MultiBlocProvider(
providers: [
// Contact Cubits
BlocProvider(
create: (context) => ContactInvitationListCubit(
locator: context.read,
accountRecordKey: accountRecordKey,
contactInvitationListRecordPointer:
contactInvitationListRecordPointer,
)),
BlocProvider(
create: (context) => ContactListCubit(
locator: context.read,
accountRecordKey: accountRecordKey,
contactListRecordPointer: contactListRecordPointer)),
BlocProvider(
create: (context) => WaitingInvitationsBlocMapCubit(
locator: context.read,
)),
// Chat Cubits
BlocProvider(
create: (context) => ActiveChatCubit(null,
routerCubit: context.read<RouterCubit>())),
BlocProvider(
create: (context) => ChatListCubit(
locator: context.read,
accountRecordKey: accountRecordKey,
chatListRecordPointer: chatListRecordPointer)),
// Conversation Cubits
BlocProvider(
create: (context) => ActiveConversationsBlocMapCubit(
locator: context.read,
)),
BlocProvider(
create: (context) => ActiveSingleContactChatBlocMapCubit(
locator: context.read,
)),
],
child: MultiBlocListener(listeners: [
BlocListener<WaitingInvitationsBlocMapCubit,
WaitingInvitationsBlocMapState>(
listener: _invitationStatusListener,
)
], child: widget.child));
}
Widget _buildWithLogin(BuildContext context) {
// Get active account info status
final (
accountInfoStatus,
accountInfoActive,
superIdentityRecordKey
) = context
.select<ActiveAccountInfoCubit, (AccountInfoStatus, bool, TypedKey?)>(
(c) => (
c.state.status,
c.state.active,
c.state.unlockedAccountInfo?.superIdentityRecordKey
));
if (!accountInfoActive) {
// If no logged in user is active, show the loading panel // If no logged in user is active, show the loading panel
return const HomeNoActive(); return const HomeNoActive();
} }
final superIdentityRecordKey = switch (accountInfoStatus) {
accountInfo.unlockedAccountInfo?.superIdentityRecordKey;
final activeCubit = superIdentityRecordKey == null
? null
: accountRecordsCubit.tryOperate(superIdentityRecordKey,
closure: (c) => c);
if (activeCubit == null) {
return waitingPage();
}
switch (accountInfo.status) {
case AccountInfoStatus.noAccount: case AccountInfoStatus.noAccount:
return const HomeAccountMissing(); return const HomeAccountMissing();
case AccountInfoStatus.accountInvalid: case AccountInfoStatus.accountInvalid:
@ -59,17 +176,21 @@ class HomeShellState extends State<HomeShell> {
case AccountInfoStatus.accountLocked: case AccountInfoStatus.accountLocked:
return const HomeAccountLocked(); return const HomeAccountLocked();
case AccountInfoStatus.accountReady: case AccountInfoStatus.accountReady:
return MultiBlocProvider(
providers: [ // Get the current active account record cubit
BlocProvider<AccountRecordCubit>.value(value: activeCubit), final activeAccountRecordCubit =
], context.select<AccountRecordsBlocMapCubit, AccountRecordCubit?>(
child: MultiProvider(providers: [ (c) => superIdentityRecordKey == null
Provider<UnlockedAccountInfo>.value( ? null
value: accountInfo.unlockedAccountInfo!, : c.tryOperate(superIdentityRecordKey, closure: (x) => x));
), if (activeAccountRecordCubit == null) {
Provider<ZoomDrawerController>.value( return waitingPage();
value: _zoomDrawerController), }
], child: widget.child));
return MultiBlocProvider(providers: [
BlocProvider<AccountRecordCubit>.value(
value: activeAccountRecordCubit),
], child: Builder(builder: _buildActiveAccount));
} }
} }
@ -96,7 +217,9 @@ class HomeShellState extends State<HomeShell> {
mainScreen: DecoratedBox( mainScreen: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: scale.primaryScale.activeElementBackground), color: scale.primaryScale.activeElementBackground),
child: buildWithLogin(context)), child: Provider<ZoomDrawerController>.value(
value: _zoomDrawerController,
child: Builder(builder: _buildWithLogin))),
borderRadius: 24, borderRadius: 24,
showShadow: true, showShadow: true,
angle: 0, angle: 0,
@ -112,5 +235,54 @@ class HomeShellState extends State<HomeShell> {
))); )));
} }
final ZoomDrawerController _zoomDrawerController = ZoomDrawerController(); final _zoomDrawerController = ZoomDrawerController();
final _singleInvitationStatusProcessor =
SingleStateProcessor<WaitingInvitationsBlocMapState>();
} }
// 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 unlockedAccountInfo = context.watch<UnlockedAccountInfo>();
// final routerCubit = context.read<RouterCubit>();
// return HomeAccountReadyShell._(
// unlockedAccountInfo: unlockedAccountInfo,
// routerCubit: routerCubit,
// key: key,
// child: child);
// }
// const HomeAccountReadyShell._(
// {required this.unlockedAccountInfo,
// required this.routerCubit,
// required this.child,
// super.key});
// @override
// HomeAccountReadyShellState createState() => HomeAccountReadyShellState();
// final Widget child;
// final UnlockedAccountInfo unlockedAccountInfo;
// final RouterCubit routerCubit;
// @override
// void debugFillProperties(DiagnosticPropertiesBuilder properties) {
// super.debugFillProperties(properties);
// properties
// ..add(DiagnosticsProperty<UnlockedAccountInfo>(
// 'unlockedAccountInfo', unlockedAccountInfo))
// ..add(DiagnosticsProperty<RouterCubit>('routerCubit', routerCubit));
// }
// }
// class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
// final SingleStateProcessor<WaitingInvitationsBlocMapState>
// _singleInvitationStatusProcessor = SingleStateProcessor();
// @override
// void initState() {
// super.initState();
// }
// }

View file

@ -29,3 +29,8 @@ 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;
}

View file

@ -1606,13 +1606,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
; ;
@ -1638,26 +1639,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);
@ -1709,6 +1708,15 @@ class Contact extends $pb.GeneratedMessage {
$core.bool hasShowAvailability() => $_has(6); $core.bool hasShowAvailability() => $_has(6);
@$pb.TagNumber(7) @$pb.TagNumber(7)
void clearShowAvailability() => clearField(7); void clearShowAvailability() => clearField(7);
@$pb.TagNumber(8)
$core.String get notes => $_getSZ(7);
@$pb.TagNumber(8)
set notes($core.String v) { $_setString(7, v); }
@$pb.TagNumber(8)
$core.bool hasNotes() => $_has(7);
@$pb.TagNumber(8)
void clearNotes() => clearField(8);
} }
class ContactInvitation extends $pb.GeneratedMessage { class ContactInvitation extends $pb.GeneratedMessage {

View file

@ -455,27 +455,27 @@ final $typed_data.Uint8List accountDescriptor = $convert.base64Decode(
const Contact$json = { const Contact$json = {
'1': 'Contact', '1': 'Contact',
'2': [ '2': [
{'1': 'edited_profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'editedProfile'}, {'1': 'nickname', '3': 1, '4': 1, '5': 9, '10': 'nickname'},
{'1': 'remote_profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'remoteProfile'}, {'1': 'profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'},
{'1': 'super_identity_json', '3': 3, '4': 1, '5': 9, '10': 'superIdentityJson'}, {'1': 'super_identity_json', '3': 3, '4': 1, '5': 9, '10': 'superIdentityJson'},
{'1': 'identity_public_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityPublicKey'}, {'1': 'identity_public_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityPublicKey'},
{'1': 'remote_conversation_record_key', '3': 5, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, {'1': 'remote_conversation_record_key', '3': 5, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
{'1': 'local_conversation_record_key', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, {'1': 'local_conversation_record_key', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
{'1': 'show_availability', '3': 7, '4': 1, '5': 8, '10': 'showAvailability'}, {'1': 'show_availability', '3': 7, '4': 1, '5': 8, '10': 'showAvailability'},
{'1': 'notes', '3': 8, '4': 1, '5': 9, '10': 'notes'},
], ],
}; };
/// Descriptor for `Contact`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `Contact`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List contactDescriptor = $convert.base64Decode( final $typed_data.Uint8List contactDescriptor = $convert.base64Decode(
'CgdDb250YWN0EjoKDmVkaXRlZF9wcm9maWxlGAEgASgLMhMudmVpbGlkY2hhdC5Qcm9maWxlUg' 'CgdDb250YWN0EhoKCG5pY2tuYW1lGAEgASgJUghuaWNrbmFtZRItCgdwcm9maWxlGAIgASgLMh'
'1lZGl0ZWRQcm9maWxlEjoKDnJlbW90ZV9wcm9maWxlGAIgASgLMhMudmVpbGlkY2hhdC5Qcm9m' 'MudmVpbGlkY2hhdC5Qcm9maWxlUgdwcm9maWxlEi4KE3N1cGVyX2lkZW50aXR5X2pzb24YAyAB'
'aWxlUg1yZW1vdGVQcm9maWxlEi4KE3N1cGVyX2lkZW50aXR5X2pzb24YAyABKAlSEXN1cGVySW' 'KAlSEXN1cGVySWRlbnRpdHlKc29uEkAKE2lkZW50aXR5X3B1YmxpY19rZXkYBCABKAsyEC52ZW'
'RlbnRpdHlKc29uEkAKE2lkZW50aXR5X3B1YmxpY19rZXkYBCABKAsyEC52ZWlsaWQuVHlwZWRL' 'lsaWQuVHlwZWRLZXlSEWlkZW50aXR5UHVibGljS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25f'
'ZXlSEWlkZW50aXR5UHVibGljS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleR' 'cmVjb3JkX2tleRgFIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUm'
'gFIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5ElMK' 'Vjb3JkS2V5ElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAYgASgLMhAudmVpbGlk'
'HWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAYgASgLMhAudmVpbGlkLlR5cGVkS2V5Uh' 'LlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRIrChFzaG93X2F2YWlsYWJpbG'
'psb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRIrChFzaG93X2F2YWlsYWJpbGl0eRgHIAEoCFIQ' 'l0eRgHIAEoCFIQc2hvd0F2YWlsYWJpbGl0eRIUCgVub3RlcxgIIAEoCVIFbm90ZXM=');
'c2hvd0F2YWlsYWJpbGl0eQ==');
@$core.Deprecated('Use contactInvitationDescriptor instead') @$core.Deprecated('Use contactInvitationDescriptor instead')
const ContactInvitation$json = { const ContactInvitation$json = {

View file

@ -349,10 +349,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
@ -363,6 +363,8 @@ message Contact {
veilid.TypedKey local_conversation_record_key = 6; veilid.TypedKey local_conversation_record_key = 6;
// Show availability to this contact // Show availability to this contact
bool show_availability = 7; bool show_availability = 7;
// Notes about this friend
string notes = 8;
} }
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////

View file

@ -21,7 +21,6 @@ part 'router_cubit.g.dart';
final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'rootNavKey'); final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'rootNavKey');
final _homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'homeNavKey'); final _homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'homeNavKey');
final _activeNavKey = GlobalKey<NavigatorState>(debugLabel: 'activeNavKey');
@freezed @freezed
class RouterState with _$RouterState { class RouterState with _$RouterState {
@ -68,11 +67,6 @@ class RouterCubit extends Cubit<RouterState> {
ShellRoute( ShellRoute(
navigatorKey: _homeNavKey, navigatorKey: _homeNavKey,
builder: (context, state, child) => HomeShell(child: child), builder: (context, state, child) => HomeShell(child: child),
routes: [
ShellRoute(
navigatorKey: _activeNavKey,
builder: (context, state, child) =>
HomeAccountReadyShell(context: context, child: child),
routes: [ routes: [
GoRoute( GoRoute(
path: '/', path: '/',
@ -82,7 +76,6 @@ class RouterCubit extends Cubit<RouterState> {
path: '/chat', path: '/chat',
builder: (context, state) => const HomeAccountReadyChat(), builder: (context, state) => const HomeAccountReadyChat(),
), ),
]),
], ],
), ),
GoRoute( GoRoute(