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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
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:veilid_support/veilid_support.dart';
@ -44,23 +45,28 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
// ignore: prefer_constructors_over_static_methods
static ChatComponentCubit singleContact(
{required UnlockedAccountInfo activeAccountInfo,
required proto.Account accountRecordInfo,
required ActiveConversationState activeConversationState,
{required Locator locator,
required ActiveConversationCubit activeConversationCubit,
required SingleContactMessagesCubit messagesCubit}) {
// Get account info
final unlockedAccountInfo =
locator<ActiveAccountInfoCubit>().state.unlockedAccountInfo!;
final account = locator<AccountRecordCubit>().state.asData!.value;
// Make local 'User'
final localUserIdentityKey = activeAccountInfo.identityTypedPublicKey;
final localUserIdentityKey = unlockedAccountInfo.identityTypedPublicKey;
final localUser = types.User(
id: localUserIdentityKey.toString(),
firstName: accountRecordInfo.profile.name,
firstName: account.profile.name,
metadata: {metadataKeyIdentityPublicKey: localUserIdentityKey});
// Make remote 'User's
final remoteUsers = {
activeConversationState.contact.identityPublicKey.toVeilid(): types.User(
id: activeConversationState.contact.identityPublicKey
.toVeilid()
.toString(),
firstName: activeConversationState.contact.editedProfile.name,
firstName: activeConversationState.contact.displayName,
metadata: {
metadataKeyIdentityPublicKey:
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:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.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
class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
SingleContactMessagesCubit({
required UnlockedAccountInfo activeAccountInfo,
required Locator locator,
required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationRecordKey,
required TypedKey localMessagesRecordKey,
required TypedKey remoteConversationRecordKey,
required TypedKey remoteMessagesRecordKey,
}) : _activeAccountInfo = activeAccountInfo,
}) : _locator = locator,
_remoteIdentityPublicKey = remoteIdentityPublicKey,
_localConversationRecordKey = localConversationRecordKey,
_localMessagesRecordKey = localMessagesRecordKey,
@ -86,6 +87,9 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Initialize everything
Future<void> _init() async {
_unlockedAccountInfo =
_locator<ActiveAccountInfoCubit>().state.unlockedAccountInfo!;
_unsentMessagesQueue = PersistentQueue<proto.Message>(
table: 'SingleContactUnsentMessages',
key: _remoteConversationRecordKey.toString(),
@ -111,15 +115,15 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Make crypto
Future<void> _initCrypto() async {
_conversationCrypto = await _activeAccountInfo
_conversationCrypto = await _unlockedAccountInfo
.makeConversationCrypto(_remoteIdentityPublicKey);
_senderMessageIntegrity = await MessageIntegrity.create(
author: _activeAccountInfo.identityTypedPublicKey);
author: _unlockedAccountInfo.identityTypedPublicKey);
}
// Open local messages key
Future<void> _initSentMessagesCubit() async {
final writer = _activeAccountInfo.identityWriter;
final writer = _unlockedAccountInfo.identityWriter;
_sentMessagesCubit = DHTLogCubit(
open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer,
@ -149,7 +153,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
Future<VeilidCrypto> _makeLocalMessagesCrypto() async =>
VeilidCryptoPrivate.fromTypedKey(
_activeAccountInfo.userLogin.identitySecret, 'tabledb');
_unlockedAccountInfo.userLogin.identitySecret, 'tabledb');
// Open reconciled chat record key
Future<void> _initReconciledMessagesCubit() async {
@ -240,8 +244,10 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
return;
}
_reconciliation.reconcileMessages(_activeAccountInfo.identityTypedPublicKey,
sentMessages, _sentMessagesCubit!);
_reconciliation.reconcileMessages(
_unlockedAccountInfo.identityTypedPublicKey,
sentMessages,
_sentMessagesCubit!);
// Update the view
_renderState();
@ -278,7 +284,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Now sign it
await _senderMessageIntegrity.signMessage(
message, _activeAccountInfo.identitySecretKey);
message, _unlockedAccountInfo.identitySecretKey);
}
// Async process to send messages in the background
@ -331,7 +337,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
for (final m in reconciledMessages.windowElements) {
final isLocal = m.content.author.toVeilid() ==
_activeAccountInfo.identityTypedPublicKey;
_unlockedAccountInfo.identityTypedPublicKey;
final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime);
final sm =
isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null;
@ -369,7 +375,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Add common fields
// id and signature will get set by _processMessageToSend
message
..author = _activeAccountInfo.identityTypedPublicKey.toProto()
..author = _unlockedAccountInfo.identityTypedPublicKey.toProto()
..timestamp = Veilid.instance.now().toInt64();
// Put in the queue
@ -402,7 +408,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
/////////////////////////////////////////////////////////////////////////
final WaitSet<void> _initWait = WaitSet();
final UnlockedAccountInfo _activeAccountInfo;
final Locator _locator;
late final UnlockedAccountInfo _unlockedAccountInfo;
final TypedKey _remoteIdentityPublicKey;
final TypedKey _localConversationRecordKey;
final TypedKey _localMessagesRecordKey;

View File

@ -22,26 +22,15 @@ class ChatComponentWidget extends StatelessWidget {
static Widget builder(
{required TypedKey localConversationRecordKey, Key? key}) =>
Builder(builder: (context) {
// Get all watched dependendies
final activeAccountInfo = context.watch<UnlockedAccountInfo>();
final accountRecordInfo =
context.watch<AccountRecordCubit>().state.asData?.value;
if (accountRecordInfo == null) {
return debugPage('should always have an account record here');
}
final avconversation = context.select<ActiveConversationsBlocMapCubit,
AsyncValue<ActiveConversationState>?>(
(x) => x.state[localConversationRecordKey]);
if (avconversation == null) {
// Get the active conversation cubit
final activeConversationCubit = context
.select<ActiveConversationsBlocMapCubit, ActiveConversationCubit?>(
(x) => x.tryOperate(localConversationRecordKey,
closure: (cubit) => cubit));
if (activeConversationCubit == null) {
return waitingPage();
}
final activeConversationState = avconversation.asData?.value;
if (activeConversationState == null) {
return avconversation.buildNotData();
}
// Get the messages cubit
final messagesCubit = context.select<
ActiveSingleContactChatBlocMapCubit,
@ -55,9 +44,8 @@ class ChatComponentWidget extends StatelessWidget {
// Make chat component state
return BlocProvider(
create: (context) => ChatComponentCubit.singleContact(
activeAccountInfo: activeAccountInfo,
accountRecordInfo: accountRecordInfo,
activeConversationState: activeConversationState,
locator: context.read,
activeConversationCubit: activeConversationCubit,
messagesCubit: messagesCubit,
),
child: ChatComponentWidget._(key: key));

View File

@ -3,9 +3,9 @@ import 'dart:async';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../chat/chat.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
@ -19,21 +19,17 @@ typedef ChatListCubitState = DHTShortArrayBusyState<proto.Chat>;
class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
with StateMapFollowable<ChatListCubitState, TypedKey, proto.Chat> {
ChatListCubit({
required UnlockedAccountInfo unlockedAccountInfo,
required proto.Account account,
required this.activeChatCubit,
}) : super(
open: () => _open(unlockedAccountInfo, account),
required Locator locator,
required TypedKey accountRecordKey,
required OwnedDHTRecordPointer chatListRecordPointer,
}) : _locator = locator,
super(
open: () => _open(locator, accountRecordKey, chatListRecordPointer),
decodeElement: proto.Chat.fromBuffer);
static Future<DHTShortArray> _open(
UnlockedAccountInfo activeAccountInfo, proto.Account account) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final chatListRecordKey = account.chatList.toVeilid();
final dhtRecord = await DHTShortArray.openOwned(chatListRecordKey,
static Future<DHTShortArray> _open(Locator locator, TypedKey accountRecordKey,
OwnedDHTRecordPointer chatListRecordPointer) async {
final dhtRecord = await DHTShortArray.openOwned(chatListRecordPointer,
debugName: 'ChatListCubit::_open::ChatList', parent: accountRecordKey);
return dhtRecord;
@ -41,11 +37,11 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
Future<proto.ChatSettings> getDefaultChatSettings(
proto.Contact contact) async {
final pronouns = contact.editedProfile.pronouns.isEmpty
final pronouns = contact.profile.pronouns.isEmpty
? ''
: ' (${contact.editedProfile.pronouns})';
: ' [${contact.profile.pronouns}])';
return proto.ChatSettings()
..title = '${contact.editedProfile.name}$pronouns'
..title = '${contact.displayName}$pronouns'
..description = ''
..defaultExpiration = Int64.ZERO;
}
@ -99,6 +95,7 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
final deletedItem =
// Ensure followers get their changes before we return
await syncFollowers(() => operateWrite((writer) async {
final activeChatCubit = _locator<ActiveChatCubit>();
if (activeChatCubit.state == localConversationRecordKey) {
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();
final selected = activeChatCubit.state == localConversationRecordKey;
late final String title;
late final String subtitle;
if (_contact.nickname.isNotEmpty) {
title = _contact.nickname;
if (_contact.profile.pronouns.isNotEmpty) {
subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})';
} else {
subtitle = _contact.profile.name;
}
} else {
title = _contact.profile.name;
if (_contact.profile.pronouns.isNotEmpty) {
subtitle = '(${_contact.profile.pronouns})';
} else {
subtitle = '';
}
}
return SliderTile(
key: ObjectKey(_contact),
disabled: _disabled,
selected: selected,
tileScale: ScaleKind.secondary,
title: _contact.editedProfile.name,
subtitle: _contact.editedProfile.pronouns,
title: title,
subtitle: subtitle,
icon: Icons.chat,
onTap: () {
singleFuture(activeChatCubit, () async {

View File

@ -53,10 +53,13 @@ class ChatSingleContactListWidget extends StatelessWidget {
if (contact == null) {
return false;
}
return contact.editedProfile.name
return contact.nickname
.toLowerCase()
.contains(lowerValue) ||
contact.editedProfile.pronouns
contact.profile.name
.toLowerCase()
.contains(lowerValue) ||
contact.profile.pronouns
.toLowerCase()
.contains(lowerValue);
}).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:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
@ -36,22 +37,18 @@ class ContactInvitationListCubit
StateMapFollowable<ContactInvitiationListState, TypedKey,
proto.ContactInvitationRecord> {
ContactInvitationListCubit({
required UnlockedAccountInfo unlockedAccountInfo,
required proto.Account account,
}) : _activeAccountInfo = unlockedAccountInfo,
_account = account,
required Locator locator,
required TypedKey accountRecordKey,
required OwnedDHTRecordPointer contactInvitationListRecordPointer,
}) : _locator = locator,
_accountRecordKey = accountRecordKey,
super(
open: () => _open(unlockedAccountInfo, account),
open: () =>
_open(accountRecordKey, contactInvitationListRecordPointer),
decodeElement: proto.ContactInvitationRecord.fromBuffer);
static Future<DHTShortArray> _open(
UnlockedAccountInfo activeAccountInfo, proto.Account account) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final contactInvitationListRecordPointer =
account.contactInvitationRecords.toVeilid();
static Future<DHTShortArray> _open(TypedKey accountRecordKey,
OwnedDHTRecordPointer contactInvitationListRecordPointer) async {
final dhtRecord = await DHTShortArray.openOwned(
contactInvitationListRecordPointer,
debugName: 'ContactInvitationListCubit::_open::ContactInvitationList',
@ -71,8 +68,12 @@ class ContactInvitationListCubit
final crcs = await pool.veilid.bestCryptoSystem();
final contactRequestWriter = await crcs.generateKeyPair();
final idcs = await _activeAccountInfo.identityCryptoSystem;
final identityWriter = _activeAccountInfo.identityWriter;
final activeAccountInfo =
_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
final encryptedSecret = await encryptionKeyType.encryptSecretToBytes(
@ -90,7 +91,7 @@ class ContactInvitationListCubit
await (await pool.createRecord(
debugName: 'ContactInvitationListCubit::createInvitation::'
'LocalConversation',
parent: _activeAccountInfo.accountRecordKey,
parent: _accountRecordKey,
schema: DHTSchema.smpl(
oCnt: 0,
members: [DHTSchemaMember(mKey: identityWriter.key, mCnt: 1)])))
@ -99,9 +100,9 @@ class ContactInvitationListCubit
// Make ContactRequestPrivate and encrypt with the writer secret
final crpriv = proto.ContactRequestPrivate()
..writerKey = contactRequestWriter.key.toProto()
..profile = _account.profile
..profile = profile
..superIdentityRecordKey =
_activeAccountInfo.userLogin.superIdentityRecordKey.toProto()
activeAccountInfo.userLogin.superIdentityRecordKey.toProto()
..chatRecordKey = localConversation.key.toProto()
..expiration = expiration?.toInt64() ?? Int64.ZERO;
final crprivbytes = crpriv.writeToBuffer();
@ -119,7 +120,7 @@ class ContactInvitationListCubit
await (await pool.createRecord(
debugName: 'ContactInvitationListCubit::createInvitation::'
'ContactRequestInbox',
parent: _activeAccountInfo.accountRecordKey,
parent: _accountRecordKey,
schema: DHTSchema.smpl(oCnt: 1, members: [
DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key)
]),
@ -172,8 +173,6 @@ class ContactInvitationListCubit
{required bool accepted,
required TypedKey contactRequestInboxRecordKey}) async {
final pool = DHTRecordPool.instance;
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Remove ContactInvitationRecord from account's list
final deletedItem = await operateWrite((writer) async {
@ -198,7 +197,7 @@ class ContactInvitationListCubit
await (await pool.openRecordOwned(contactRequestInbox,
debugName: 'ContactInvitationListCubit::deleteInvitation::'
'ContactRequestInbox',
parent: accountRecordKey))
parent: _accountRecordKey))
.scope((contactRequestInbox) async {
// Wipe out old invitation so it shows up as invalid
await contactRequestInbox.tryWriteBytes(Uint8List(0));
@ -248,7 +247,7 @@ class ContactInvitationListCubit
await (await pool.openRecordRead(contactRequestInboxKey,
debugName: 'ContactInvitationListCubit::validateInvitation::'
'ContactRequestInbox',
parent: _activeAccountInfo.accountRecordKey))
parent: _accountRecordKey))
.maybeDeleteScope(!isSelf, (contactRequestInbox) async {
//
final contactRequest = await contactRequestInbox
@ -293,8 +292,7 @@ class ContactInvitationListCubit
secret: writerSecret);
out = ValidContactInvitation(
activeAccountInfo: _activeAccountInfo,
account: _account,
locator: _locator,
contactRequestInboxKey: contactRequestInboxKey,
contactRequestPrivate: contactRequestPrivate,
contactSuperIdentity: contactSuperIdentity,
@ -318,6 +316,6 @@ class ContactInvitationListCubit
}
//
final UnlockedAccountInfo _activeAccountInfo;
final proto.Account _account;
final Locator _locator;
final TypedKey _accountRecordKey;
}

View File

@ -1,3 +1,4 @@
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
@ -7,27 +8,24 @@ import '../../proto/proto.dart' as proto;
class ContactRequestInboxCubit
extends DefaultDHTRecordCubit<proto.SignedContactResponse?> {
ContactRequestInboxCubit(
{required this.activeAccountInfo, required this.contactInvitationRecord})
{required Locator locator, required this.contactInvitationRecord})
: super(
open: () => _open(
activeAccountInfo: activeAccountInfo,
locator: locator,
contactInvitationRecord: contactInvitationRecord),
decodeState: (buf) => buf.isEmpty
? null
: proto.SignedContactResponse.fromBuffer(buf));
// ContactRequestInboxCubit.value(
// {required super.record,
// required this.activeAccountInfo,
// required this.contactInvitationRecord})
// : super.value(decodeState: proto.SignedContactResponse.fromBuffer);
static Future<DHTRecord> _open(
{required UnlockedAccountInfo activeAccountInfo,
{required Locator locator,
required proto.ContactInvitationRecord contactInvitationRecord}) async {
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 recordKey =
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid();
@ -42,6 +40,5 @@ class ContactRequestInboxCubit
defaultSubkey: 1);
}
final UnlockedAccountInfo activeAccountInfo;
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:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:provider/provider.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 '../../tools/tools.dart';
@ -25,24 +25,22 @@ class InvitationStatus extends Equatable {
class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
proto.SignedContactResponse?> {
WaitingInvitationCubit(ContactRequestInboxCubit super.input,
{required UnlockedAccountInfo activeAccountInfo,
required proto.Account account,
{required Locator locator,
required proto.ContactInvitationRecord contactInvitationRecord})
: super(
transform: (signedContactResponse) => _transform(
signedContactResponse,
activeAccountInfo: activeAccountInfo,
account: account,
locator: locator,
contactInvitationRecord: contactInvitationRecord));
static Future<AsyncValue<InvitationStatus>> _transform(
proto.SignedContactResponse? signedContactResponse,
{required UnlockedAccountInfo activeAccountInfo,
required proto.Account account,
{required Locator locator,
required proto.ContactInvitationRecord contactInvitationRecord}) async {
if (signedContactResponse == null) {
return const AsyncValue.loading();
}
final contactResponseBytes =
Uint8List.fromList(signedContactResponse.contactResponse);
final contactResponse =
@ -71,7 +69,7 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
contactResponse.remoteConversationRecordKey.toVeilid();
final conversation = ConversationCubit(
activeAccountInfo: activeAccountInfo,
locator: locator,
remoteIdentityPublicKey:
contactSuperIdentity.currentInstance.typedPublicKey,
remoteConversationRecordKey: remoteConversationRecordKey);
@ -99,15 +97,11 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
contactInvitationRecord.localConversationRecordKey.toVeilid();
return conversation.initLocalConversation(
existingConversationRecordKey: localConversationRecordKey,
profile: account.profile,
// ignore: prefer_expression_function_bodies
callback: (localConversation) async {
return AsyncValue.data(InvitationStatus(
acceptedContact: AcceptedContact(
remoteProfile: remoteProfile,
remoteIdentity: contactSuperIdentity,
remoteConversationRecordKey: remoteConversationRecordKey,
localConversationRecordKey: localConversationRecordKey)));
});
callback: (localConversation) async => AsyncValue.data(InvitationStatus(
acceptedContact: AcceptedContact(
remoteProfile: remoteProfile,
remoteIdentity: contactSuperIdentity,
remoteConversationRecordKey: remoteConversationRecordKey,
localConversationRecordKey: localConversationRecordKey))));
}
}

View File

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

View File

@ -1,4 +1,5 @@
import 'package:meta/meta.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
@ -13,14 +14,12 @@ import 'models.dart';
class ValidContactInvitation {
@internal
ValidContactInvitation(
{required UnlockedAccountInfo activeAccountInfo,
required proto.Account account,
{required Locator locator,
required TypedKey contactRequestInboxKey,
required proto.ContactRequestPrivate contactRequestPrivate,
required SuperIdentity contactSuperIdentity,
required KeyPair writer})
: _activeAccountInfo = activeAccountInfo,
_account = account,
: _locator = locator,
_contactRequestInboxKey = contactRequestInboxKey,
_contactRequestPrivate = contactRequestPrivate,
_contactSuperIdentity = contactSuperIdentity,
@ -31,11 +30,15 @@ class ValidContactInvitation {
Future<AcceptedContact?> accept() async {
final pool = DHTRecordPool.instance;
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
// The initiating side will delete the records in deleteInvitation()
final isSelf = _contactSuperIdentity.currentInstance.publicKey ==
_activeAccountInfo.identityPublicKey;
final accountRecordKey = _activeAccountInfo.accountRecordKey;
final isSelf =
_contactSuperIdentity.currentInstance.publicKey == identityPublicKey;
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
debugName: 'ValidContactInvitation::accept::'
@ -46,43 +49,42 @@ class ValidContactInvitation {
// Create local conversation key for this
// contact and send via contact response
final conversation = ConversationCubit(
activeAccountInfo: _activeAccountInfo,
locator: _locator,
remoteIdentityPublicKey:
_contactSuperIdentity.currentInstance.typedPublicKey);
return conversation.initLocalConversation(
profile: _account.profile,
callback: (localConversation) async {
final contactResponse = proto.ContactResponse()
..accept = true
..remoteConversationRecordKey = localConversation.key.toProto()
..superIdentityRecordKey =
_activeAccountInfo.superIdentityRecordKey.toProto();
final contactResponseBytes = contactResponse.writeToBuffer();
final contactResponse = proto.ContactResponse()
..accept = true
..remoteConversationRecordKey = localConversation.key.toProto()
..superIdentityRecordKey =
unlockedAccountInfo.superIdentityRecordKey.toProto();
final contactResponseBytes = contactResponse.writeToBuffer();
final cs = await pool.veilid
.getCryptoSystem(_contactRequestInboxKey.kind);
final cs =
await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind);
final identitySignature = await cs.sign(
_activeAccountInfo.identityWriter.key,
_activeAccountInfo.identityWriter.secret,
contactResponseBytes);
final identitySignature = await cs.sign(
unlockedAccountInfo.identityWriter.key,
unlockedAccountInfo.identityWriter.secret,
contactResponseBytes);
final signedContactResponse = proto.SignedContactResponse()
..contactResponse = contactResponseBytes
..identitySignature = identitySignature.toProto();
final signedContactResponse = proto.SignedContactResponse()
..contactResponse = contactResponseBytes
..identitySignature = identitySignature.toProto();
// Write the acceptance to the inbox
await contactRequestInbox
.eventualWriteProtobuf(signedContactResponse, subkey: 1);
// Write the acceptance to the inbox
await contactRequestInbox.eventualWriteProtobuf(signedContactResponse,
subkey: 1);
return AcceptedContact(
remoteProfile: _contactRequestPrivate.profile,
remoteIdentity: _contactSuperIdentity,
remoteConversationRecordKey:
_contactRequestPrivate.chatRecordKey.toVeilid(),
localConversationRecordKey: localConversation.key,
);
});
return AcceptedContact(
remoteProfile: _contactRequestPrivate.profile,
remoteIdentity: _contactSuperIdentity,
remoteConversationRecordKey:
_contactRequestPrivate.chatRecordKey.toVeilid(),
localConversationRecordKey: localConversation.key,
);
});
});
} on Exception catch (e) {
log.debug('exception: $e', e);
@ -93,10 +95,14 @@ class ValidContactInvitation {
Future<bool> reject() async {
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
final isSelf = _contactSuperIdentity.currentInstance.publicKey ==
_activeAccountInfo.identityPublicKey;
final accountRecordKey = _activeAccountInfo.accountRecordKey;
final isSelf =
_contactSuperIdentity.currentInstance.publicKey == identityPublicKey;
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
debugName: 'ValidContactInvitation::reject::'
@ -109,12 +115,12 @@ class ValidContactInvitation {
final contactResponse = proto.ContactResponse()
..accept = false
..superIdentityRecordKey =
_activeAccountInfo.superIdentityRecordKey.toProto();
unlockedAccountInfo.superIdentityRecordKey.toProto();
final contactResponseBytes = contactResponse.writeToBuffer();
final identitySignature = await cs.sign(
_activeAccountInfo.identityWriter.key,
_activeAccountInfo.identityWriter.secret,
unlockedAccountInfo.identityWriter.key,
unlockedAccountInfo.identityWriter.secret,
contactResponseBytes);
final signedContactResponse = proto.SignedContactResponse()
@ -129,8 +135,7 @@ class ValidContactInvitation {
}
//
final UnlockedAccountInfo _activeAccountInfo;
final proto.Account _account;
final Locator _locator;
final TypedKey _contactRequestInboxKey;
final SuperIdentity _contactSuperIdentity;
final KeyPair _writer;

View File

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

View File

@ -3,33 +3,29 @@ import 'dart:convert';
import 'package:async_tools/async_tools.dart';
import 'package:protobuf/protobuf.dart';
import 'package:provider/provider.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 '../../tools/tools.dart';
import '../../conversation/cubits/conversation_cubit.dart';
//////////////////////////////////////////////////
// Mutable state for per-account contacts
class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
ContactListCubit({
required UnlockedAccountInfo unlockedAccountInfo,
required proto.Account account,
}) : _activeAccountInfo = unlockedAccountInfo,
required Locator locator,
required TypedKey accountRecordKey,
required OwnedDHTRecordPointer contactListRecordPointer,
}) : _locator = locator,
super(
open: () => _open(unlockedAccountInfo, account),
open: () => _open(accountRecordKey, contactListRecordPointer),
decodeElement: proto.Contact.fromBuffer);
static Future<DHTShortArray> _open(
UnlockedAccountInfo activeAccountInfo, proto.Account account) async {
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final contactListRecordKey = account.contactList.toVeilid();
final dhtRecord = await DHTShortArray.openOwned(contactListRecordKey,
static Future<DHTShortArray> _open(TypedKey accountRecordKey,
OwnedDHTRecordPointer contactListRecordPointer) async {
final dhtRecord = await DHTShortArray.openOwned(contactListRecordPointer,
debugName: 'ContactListCubit::_open::ContactList',
parent: accountRecordKey);
@ -52,15 +48,15 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
if (remoteProfile == null) {
return;
}
return updateContactRemoteProfile(
return updateContactProfile(
localConversationRecordKey: localConversationRecordKey,
remoteProfile: remoteProfile);
profile: remoteProfile);
});
}
Future<void> updateContactRemoteProfile({
Future<void> updateContactProfile({
required TypedKey localConversationRecordKey,
required proto.Profile remoteProfile,
required proto.Profile profile,
}) async {
// Update contact's remoteProfile
await operateWriteEventual((writer) async {
@ -69,36 +65,36 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
if (c != null &&
c.localConversationRecordKey.toVeilid() ==
localConversationRecordKey) {
if (c.remoteProfile == remoteProfile) {
if (c.profile == profile) {
// Unchanged
break;
}
final newContact = c.deepCopy()..remoteProfile = remoteProfile;
final newContact = c.deepCopy()..profile = profile;
final updated = await writer.tryWriteItemProtobuf(
proto.Contact.fromBuffer, pos, newContact);
if (!updated) {
throw DHTExceptionTryAgain();
}
break;
}
}
});
}
Future<void> createContact({
required proto.Profile remoteProfile,
required proto.Profile profile,
required SuperIdentity remoteSuperIdentity,
required TypedKey remoteConversationRecordKey,
required TypedKey localConversationRecordKey,
required TypedKey remoteConversationRecordKey,
}) async {
// Create Contact
final contact = proto.Contact()
..editedProfile = remoteProfile
..remoteProfile = remoteProfile
..profile = profile
..superIdentityJson = jsonEncode(remoteSuperIdentity.toJson())
..identityPublicKey =
remoteSuperIdentity.currentInstance.typedPublicKey.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto()
..localConversationRecordKey = localConversationRecordKey.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto()
..showAvailability = false;
// Add Contact to account's list
@ -108,13 +104,8 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
});
}
Future<void> deleteContact({required proto.Contact contact}) async {
final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
final localConversationRecordKey =
contact.localConversationRecordKey.toVeilid();
final remoteConversationRecordKey =
contact.remoteConversationRecordKey.toVeilid();
Future<void> deleteContact(
{required TypedKey localConversationRecordKey}) async {
// Remove Contact from account's list
final deletedItem = await operateWrite((writer) async {
for (var i = 0; i < writer.length; i++) {
@ -122,8 +113,8 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
if (item == null) {
throw Exception('Failed to get contact');
}
if (item.localConversationRecordKey ==
contact.localConversationRecordKey) {
if (item.localConversationRecordKey.toVeilid() ==
localConversationRecordKey) {
await writer.remove(i);
return item;
}
@ -135,10 +126,12 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
try {
// Make a conversation cubit to manipulate the conversation
final conversationCubit = ConversationCubit(
activeAccountInfo: _activeAccountInfo,
remoteIdentityPublicKey: remoteIdentityPublicKey,
localConversationRecordKey: localConversationRecordKey,
remoteConversationRecordKey: remoteConversationRecordKey,
locator: _locator,
remoteIdentityPublicKey: deletedItem.identityPublicKey.toVeilid(),
localConversationRecordKey:
deletedItem.localConversationRecordKey.toVeilid(),
remoteConversationRecordKey:
deletedItem.remoteConversationRecordKey.toVeilid(),
);
// Delete the local and remote conversation records
@ -149,7 +142,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
}
}
final UnlockedAccountInfo _activeAccountInfo;
final _contactProfileUpdateMap =
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_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -11,18 +10,9 @@ import '../contacts.dart';
class ContactItemWidget extends StatelessWidget {
const ContactItemWidget(
{required this.contact, required this.disabled, super.key});
final proto.Contact contact;
final bool disabled;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<proto.Contact>('contact', contact))
..add(DiagnosticsProperty<bool>('disabled', disabled));
}
{required proto.Contact contact, required bool disabled, super.key})
: _disabled = disabled,
_contact = contact;
@override
// ignore: prefer_expression_function_bodies
@ -30,26 +20,44 @@ class ContactItemWidget extends StatelessWidget {
BuildContext context,
) {
final localConversationRecordKey =
contact.localConversationRecordKey.toVeilid();
_contact.localConversationRecordKey.toVeilid();
const selected = false; // xxx: eventually when we have selectable contacts:
// activeContactCubit.state == localConversationRecordKey;
final tileDisabled = disabled || context.watch<ContactListCubit>().isBusy;
final tileDisabled = _disabled || context.watch<ContactListCubit>().isBusy;
late final String title;
late final String subtitle;
if (_contact.nickname.isNotEmpty) {
title = _contact.nickname;
if (_contact.profile.pronouns.isNotEmpty) {
subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})';
} else {
subtitle = _contact.profile.name;
}
} else {
title = _contact.profile.name;
if (_contact.profile.pronouns.isNotEmpty) {
subtitle = '(${_contact.profile.pronouns})';
} else {
subtitle = '';
}
}
return SliderTile(
key: ObjectKey(contact),
key: ObjectKey(_contact),
disabled: tileDisabled,
selected: selected,
tileScale: ScaleKind.primary,
title: contact.editedProfile.name,
subtitle: contact.editedProfile.pronouns,
title: title,
subtitle: subtitle,
icon: Icons.person,
onTap: () async {
// Start a chat
final chatListCubit = context.read<ChatListCubit>();
await chatListCubit.getOrCreateChatSingleContact(contact: contact);
await chatListCubit.getOrCreateChatSingleContact(contact: _contact);
// Click over to chats
if (context.mounted) {
await MainPager.of(context)
@ -71,9 +79,15 @@ class ContactItemWidget extends StatelessWidget {
localConversationRecordKey: localConversationRecordKey);
// 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();
return contactList
.where((element) =>
element.editedProfile.name
element.nickname
.toLowerCase()
.contains(lowerValue) ||
element.editedProfile.pronouns
element.profile.name
.toLowerCase()
.contains(lowerValue) ||
element.profile.pronouns
.toLowerCase()
.contains(lowerValue))
.toList();

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
import '../../../account_manager/account_manager.dart';
import '../../../chat/chat.dart';
import '../../../proto/proto.dart' as proto;
import '../../../theme/theme.dart';
import '../../../tools/tools.dart';
import 'main_pager/main_pager.dart';
@ -29,7 +30,8 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
}
Widget buildUserPanel() => Builder(builder: (context) {
final account = context.watch<AccountRecordCubit>().state;
final profile = context.select<AccountRecordCubit, proto.Profile>(
(c) => c.state.asData!.value.profile);
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
@ -50,9 +52,7 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
await ctrl.toggle?.call();
//await GoRouterHelper(context).push('/settings');
}).paddingLTRB(0, 0, 8, 0),
asyncValueBuilder(account,
(_, account) => ProfileWidget(profile: account.profile))
.expanded(),
ProfileWidget(profile: profile).expanded(),
]).paddingAll(8),
const MainPager().expanded()
]);
@ -72,8 +72,8 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
return const NoConversationWidget();
}
return ChatComponentWidget.builder(
localConversationRecordKey: activeChatLocalConversationKey,
);
localConversationRecordKey: activeChatLocalConversationKey,
key: ValueKey(activeChatLocalConversationKey));
}
// ignore: prefer_expression_function_bodies

View File

@ -1,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 'package:async_tools/async_tools.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../chat/chat.dart';
import '../../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 'drawer_menu/drawer_menu.dart';
import 'home_account_invalid.dart';
@ -33,25 +42,133 @@ class HomeShellState extends State<HomeShell> {
super.dispose();
}
Widget buildWithLogin(BuildContext context) {
final accountInfo = context.watch<ActiveAccountInfoCubit>().state;
final accountRecordsCubit = context.watch<AccountRecordsBlocMapCubit>();
if (!accountInfo.active) {
// 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(
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
return const HomeNoActive();
}
final superIdentityRecordKey =
accountInfo.unlockedAccountInfo?.superIdentityRecordKey;
final activeCubit = superIdentityRecordKey == null
? null
: accountRecordsCubit.tryOperate(superIdentityRecordKey,
closure: (c) => c);
if (activeCubit == null) {
return waitingPage();
}
switch (accountInfo.status) {
switch (accountInfoStatus) {
case AccountInfoStatus.noAccount:
return const HomeAccountMissing();
case AccountInfoStatus.accountInvalid:
@ -59,17 +176,21 @@ class HomeShellState extends State<HomeShell> {
case AccountInfoStatus.accountLocked:
return const HomeAccountLocked();
case AccountInfoStatus.accountReady:
return MultiBlocProvider(
providers: [
BlocProvider<AccountRecordCubit>.value(value: activeCubit),
],
child: MultiProvider(providers: [
Provider<UnlockedAccountInfo>.value(
value: accountInfo.unlockedAccountInfo!,
),
Provider<ZoomDrawerController>.value(
value: _zoomDrawerController),
], child: widget.child));
// Get the current active account record cubit
final activeAccountRecordCubit =
context.select<AccountRecordsBlocMapCubit, AccountRecordCubit?>(
(c) => superIdentityRecordKey == null
? null
: c.tryOperate(superIdentityRecordKey, closure: (x) => x));
if (activeAccountRecordCubit == null) {
return waitingPage();
}
return MultiBlocProvider(providers: [
BlocProvider<AccountRecordCubit>.value(
value: activeAccountRecordCubit),
], child: Builder(builder: _buildActiveAccount));
}
}
@ -96,7 +217,9 @@ class HomeShellState extends State<HomeShell> {
mainScreen: DecoratedBox(
decoration: BoxDecoration(
color: scale.primaryScale.activeElementBackground),
child: buildWithLogin(context)),
child: Provider<ZoomDrawerController>.value(
value: _zoomDrawerController,
child: Builder(builder: _buildWithLogin))),
borderRadius: 24,
showShadow: true,
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) =>
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);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Contact', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<Profile>(1, _omitFieldNames ? '' : 'editedProfile', subBuilder: Profile.create)
..aOM<Profile>(2, _omitFieldNames ? '' : 'remoteProfile', subBuilder: Profile.create)
..aOS(1, _omitFieldNames ? '' : 'nickname')
..aOM<Profile>(2, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create)
..aOS(3, _omitFieldNames ? '' : 'superIdentityJson')
..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
..aOB(7, _omitFieldNames ? '' : 'showAvailability')
..aOS(8, _omitFieldNames ? '' : 'notes')
..hasRequiredFields = false
;
@ -1638,26 +1639,24 @@ class Contact extends $pb.GeneratedMessage {
static Contact? _defaultInstance;
@$pb.TagNumber(1)
Profile get editedProfile => $_getN(0);
$core.String get nickname => $_getSZ(0);
@$pb.TagNumber(1)
set editedProfile(Profile v) { setField(1, v); }
set nickname($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasEditedProfile() => $_has(0);
$core.bool hasNickname() => $_has(0);
@$pb.TagNumber(1)
void clearEditedProfile() => clearField(1);
@$pb.TagNumber(1)
Profile ensureEditedProfile() => $_ensure(0);
void clearNickname() => clearField(1);
@$pb.TagNumber(2)
Profile get remoteProfile => $_getN(1);
Profile get profile => $_getN(1);
@$pb.TagNumber(2)
set remoteProfile(Profile v) { setField(2, v); }
set profile(Profile v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasRemoteProfile() => $_has(1);
$core.bool hasProfile() => $_has(1);
@$pb.TagNumber(2)
void clearRemoteProfile() => clearField(2);
void clearProfile() => clearField(2);
@$pb.TagNumber(2)
Profile ensureRemoteProfile() => $_ensure(1);
Profile ensureProfile() => $_ensure(1);
@$pb.TagNumber(3)
$core.String get superIdentityJson => $_getSZ(2);
@ -1709,6 +1708,15 @@ class Contact extends $pb.GeneratedMessage {
$core.bool hasShowAvailability() => $_has(6);
@$pb.TagNumber(7)
void clearShowAvailability() => clearField(7);
@$pb.TagNumber(8)
$core.String get notes => $_getSZ(7);
@$pb.TagNumber(8)
set notes($core.String v) { $_setString(7, v); }
@$pb.TagNumber(8)
$core.bool hasNotes() => $_has(7);
@$pb.TagNumber(8)
void clearNotes() => clearField(8);
}
class ContactInvitation extends $pb.GeneratedMessage {

View File

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

View File

@ -349,10 +349,10 @@ message Account {
//
// Stored in ContactList DHTList
message Contact {
// Friend's profile as locally edited
Profile edited_profile = 1;
// Friend's nickname
string nickname = 1;
// Copy of friend's profile from remote conversation
Profile remote_profile = 2;
Profile profile = 2;
// Copy of friend's SuperIdentity in JSON from remote conversation
string super_identity_json = 3;
// Copy of friend's most recent identity public key from their identityMaster
@ -363,6 +363,8 @@ message Contact {
veilid.TypedKey local_conversation_record_key = 6;
// Show availability to this contact
bool show_availability = 7;
// Notes about this friend
string notes = 8;
}
////////////////////////////////////////////////////////////////////////////////////

View File

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