checkpoint

This commit is contained in:
Christien Rioux 2024-06-20 19:04:39 -04:00
parent 3f8b4d2a41
commit 17211f3515
22 changed files with 701 additions and 353 deletions

View File

@ -38,9 +38,23 @@ class PerAccountCollectionBlocMapCubit extends BlocMapCubit<TypedKey,
Future<void> removeFromState(TypedKey key) => remove(key);
@override
Future<void> updateState(TypedKey key, LocalAccount value) async {
Future<void> updateState(
TypedKey key, LocalAccount? oldValue, LocalAccount newValue) async {
// Don't replace unless this is a totally different account
// The sub-cubit's subscription will update our state later
if (oldValue != null) {
if (oldValue.superIdentity.recordKey !=
newValue.superIdentity.recordKey) {
throw StateError(
'should remove LocalAccount and make a new one, not change it, if '
'the superidentity record key has changed');
}
// This never changes anything that should result in rebuildin the
// sub-cubit
return;
}
await _addPerAccountCollectionCubit(
superIdentityRecordKey: value.superIdentity.recordKey);
superIdentityRecordKey: newValue.superIdentity.recordKey);
}
////////////////////////////////////////////////////////////////////////////

View File

@ -136,15 +136,17 @@ class PerAccountCollectionCubit extends Cubit<PerAccountCollectionState> {
: (accountInfo, contactListRecordPointer));
// WaitingInvitationsBlocMapCubit
final waitingInvitationsBlocMapCubit =
waitingInvitationsBlocMapCubitUpdater.update(
accountInfo.userLogin == null || contactInvitationListCubit == null
? null
: (
accountInfo,
accountRecordCubit!,
contactInvitationListCubit
));
final waitingInvitationsBlocMapCubit = waitingInvitationsBlocMapCubitUpdater
.update(accountInfo.userLogin == null ||
contactInvitationListCubit == null ||
contactListCubit == null
? null
: (
accountInfo,
accountRecordCubit!,
contactInvitationListCubit,
contactListCubit,
));
// ActiveChatCubit
final activeChatCubit = activeChatCubitUpdater
@ -179,15 +181,11 @@ class PerAccountCollectionCubit extends Cubit<PerAccountCollectionState> {
final activeSingleContactChatBlocMapCubit =
activeSingleContactChatBlocMapCubitUpdater.update(
accountInfo.userLogin == null ||
activeConversationsBlocMapCubit == null ||
chatListCubit == null ||
contactListCubit == null
activeConversationsBlocMapCubit == null
? null
: (
accountInfo,
activeConversationsBlocMapCubit,
chatListCubit,
contactListCubit
));
// Update available blocs in our state
@ -260,11 +258,18 @@ class PerAccountCollectionCubit extends Cubit<PerAccountCollectionState> {
));
final waitingInvitationsBlocMapCubitUpdater = BlocUpdater<
WaitingInvitationsBlocMapCubit,
(AccountInfo, AccountRecordCubit, ContactInvitationListCubit)>(
(
AccountInfo,
AccountRecordCubit,
ContactInvitationListCubit,
ContactListCubit
)>(
create: (params) => WaitingInvitationsBlocMapCubit(
accountInfo: params.$1,
accountRecordCubit: params.$2,
contactInvitationListCubit: params.$3));
accountInfo: params.$1,
accountRecordCubit: params.$2,
contactInvitationListCubit: params.$3,
contactListCubit: params.$4,
));
final activeChatCubitUpdater =
BlocUpdater<ActiveChatCubit, bool>(create: (_) => ActiveChatCubit(null));
final chatListCubitUpdater = BlocUpdater<ChatListCubit,
@ -286,13 +291,9 @@ class PerAccountCollectionCubit extends Cubit<PerAccountCollectionState> {
(
AccountInfo,
ActiveConversationsBlocMapCubit,
ChatListCubit,
ContactListCubit
)>(
create: (params) => ActiveSingleContactChatBlocMapCubit(
accountInfo: params.$1,
activeConversationsBlocMapCubit: params.$2,
chatListCubit: params.$3,
contactListCubit: params.$4,
));
}

View File

@ -116,7 +116,14 @@ class _EditAccountPageState extends State<EditAccountPage> {
});
try {
// Look up account cubit for this specific account
final accountRecordCubit = context.read<AccountRecordCubit>();
final perAccountCollectionBlocMapCubit =
context.read<PerAccountCollectionBlocMapCubit>();
final accountRecordCubit = await perAccountCollectionBlocMapCubit
.operate(widget.superIdentityRecordKey,
closure: (c) async => c.accountRecordCubit);
if (accountRecordCubit == null) {
return;
}
// Update account profile DHT record
// This triggers ConversationCubits to update

View File

@ -12,6 +12,7 @@ import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart';
import '../../conversation/conversation.dart';
import '../../proto/proto.dart' as proto;
import '../models/chat_component_state.dart';
@ -28,10 +29,12 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
ChatComponentCubit._({
required AccountInfo accountInfo,
required AccountRecordCubit accountRecordCubit,
required ContactListCubit contactListCubit,
required List<ActiveConversationCubit> conversationCubits,
required SingleContactMessagesCubit messagesCubit,
}) : _accountInfo = accountInfo,
_accountRecordCubit = accountRecordCubit,
_contactListCubit = contactListCubit,
_conversationCubits = conversationCubits,
_messagesCubit = messagesCubit,
super(ChatComponentState(
@ -51,11 +54,13 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
factory ChatComponentCubit.singleContact(
{required AccountInfo accountInfo,
required AccountRecordCubit accountRecordCubit,
required ContactListCubit contactListCubit,
required ActiveConversationCubit activeConversationCubit,
required SingleContactMessagesCubit messagesCubit}) =>
ChatComponentCubit._(
accountInfo: accountInfo,
accountRecordCubit: accountRecordCubit,
contactListCubit: contactListCubit,
conversationCubits: [activeConversationCubit],
messagesCubit: messagesCubit,
);
@ -82,6 +87,7 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
await _initWait();
await _accountRecordSubscription.cancel();
await _messagesSubscription.cancel();
await _conversationSubscriptions.values.map((v) => v.cancel()).wait;
await super.close();
}
@ -146,12 +152,12 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
// Private Implementation
void _onChangedAccountRecord(AsyncValue<proto.Account> avAccount) {
// Update local 'User'
final account = avAccount.asData?.value;
if (account == null) {
emit(state.copyWith(localUser: null));
return;
}
// Make local 'User'
final localUser = types.User(
id: _localUserIdentityKey.toString(),
firstName: account.profile.name,
@ -168,15 +174,40 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
TypedKey remoteIdentityPublicKey,
AsyncValue<ActiveConversationState> avConversationState,
) {
//
// Update remote 'User'
final activeConversationState = avConversationState.asData?.value;
if (activeConversationState == null) {
// Don't change user information on loading state
return;
}
emit(state.copyWith(
remoteUsers: state.remoteUsers.add(
remoteIdentityPublicKey,
_convertRemoteUser(
remoteIdentityPublicKey, activeConversationState))));
}
types.User _convertRemoteUser(TypedKey remoteIdentityPublicKey,
ActiveConversationState activeConversationState) =>
types.User(
id: remoteIdentityPublicKey.toString(),
firstName: activeConversationState.contact.displayName,
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
ActiveConversationState activeConversationState) {
// See if we have a contact for this remote user
final contacts = _contactListCubit.state.state.asData?.value;
if (contacts != null) {
final contactIdx = contacts.indexWhere((x) =>
x.value.identityPublicKey.toVeilid() == remoteIdentityPublicKey);
if (contactIdx != -1) {
final contact = contacts[contactIdx].value;
return types.User(
id: remoteIdentityPublicKey.toString(),
firstName: contact.displayName,
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
}
}
return types.User(
id: remoteIdentityPublicKey.toString(),
firstName: activeConversationState.remoteConversation.profile.name,
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
}
types.User _convertUnknownUser(TypedKey remoteIdentityPublicKey) =>
types.User(
@ -376,6 +407,7 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
final _initWait = WaitSet<void>();
final AccountInfo _accountInfo;
final AccountRecordCubit _accountRecordCubit;
final ContactListCubit _contactListCubit;
final List<ActiveConversationCubit> _conversationCubits;
final SingleContactMessagesCubit _messagesCubit;

View File

@ -9,6 +9,7 @@ import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart';
import '../../conversation/conversation.dart';
import '../../theme/theme.dart';
import '../chat.dart';
@ -28,10 +29,13 @@ class ChatComponentWidget extends StatelessWidget {
// Get the account record cubit
final accountRecordCubit = context.read<AccountRecordCubit>();
// Get the contact list cubit
final contactListCubit = context.watch<ContactListCubit>();
// Get the active conversation cubit
final activeConversationCubit = context
.select<ActiveConversationsBlocMapCubit, ActiveConversationCubit?>(
(x) => x.tryOperate(localConversationRecordKey,
(x) => x.tryOperateSync(localConversationRecordKey,
closure: (cubit) => cubit));
if (activeConversationCubit == null) {
return waitingPage();
@ -41,7 +45,7 @@ class ChatComponentWidget extends StatelessWidget {
final messagesCubit = context.select<
ActiveSingleContactChatBlocMapCubit,
SingleContactMessagesCubit?>(
(x) => x.tryOperate(localConversationRecordKey,
(x) => x.tryOperateSync(localConversationRecordKey,
closure: (cubit) => cubit));
if (messagesCubit == null) {
return waitingPage();
@ -49,9 +53,11 @@ class ChatComponentWidget extends StatelessWidget {
// Make chat component state
return BlocProvider(
key: key,
create: (context) => ChatComponentCubit.singleContact(
accountInfo: accountInfo,
accountRecordCubit: accountRecordCubit,
contactListCubit: contactListCubit,
activeConversationCubit: activeConversationCubit,
messagesCubit: messagesCubit,
),

View File

@ -54,6 +54,7 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
// Make local copy so we don't share the buffer
final localConversationRecordKey =
contact.localConversationRecordKey.toVeilid();
final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
final remoteConversationRecordKey =
contact.remoteConversationRecordKey.toVeilid();
@ -67,18 +68,38 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
throw Exception('Failed to get chat');
}
final c = proto.Chat.fromBuffer(cbuf);
if (c.localConversationRecordKey ==
contact.localConversationRecordKey) {
// Nothing to do here
return;
switch (c.whichKind()) {
case proto.Chat_Kind.direct:
if (c.direct.localConversationRecordKey ==
contact.localConversationRecordKey) {
// Nothing to do here
return;
}
break;
case proto.Chat_Kind.group:
if (c.group.localConversationRecordKey ==
contact.localConversationRecordKey) {
throw StateError('direct conversation record key should'
' not be used for group chats!');
}
break;
case proto.Chat_Kind.notSet:
throw StateError('unknown chat kind');
}
}
// Create 1:1 conversation type Chat
final chat = proto.Chat()
final chatMember = proto.ChatMember()
..remoteIdentityPublicKey = remoteIdentityPublicKey.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto();
final directChat = proto.DirectChat()
..settings = await getDefaultChatSettings(contact)
..localConversationRecordKey = localConversationRecordKey.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto();
..remoteMember = chatMember;
final chat = proto.Chat()..direct = directChat;
// Add chat
await writer.add(chat.writeToBuffer());
@ -88,9 +109,6 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
/// Delete a chat
Future<void> deleteChat(
{required TypedKey localConversationRecordKey}) async {
final localConversationRecordKeyProto =
localConversationRecordKey.toProto();
// Remove Chat from account's list
// if this fails, don't keep retrying, user can try again later
final deletedItem =
@ -104,9 +122,9 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
if (c == null) {
throw Exception('Failed to get chat');
}
if (c.localConversationRecordKey ==
localConversationRecordKeyProto) {
// Found the right chat
localConversationRecordKey) {
await writer.remove(i);
return c;
}
@ -133,7 +151,7 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat>
return IMap();
}
return IMap.fromIterable(stateValue,
keyMapper: (e) => e.value.localConversationRecordKey.toVeilid(),
keyMapper: (e) => e.value.localConversationRecordKey,
valueMapper: (e) => e.value);
}

View File

@ -0,0 +1,96 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:searchable_listview/searchable_listview.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import '../../proto/proto.dart';
import '../../theme/theme.dart';
import '../chat_list.dart';
class ChatListWidget extends StatelessWidget {
const ChatListWidget({super.key});
Widget _itemBuilderDirect(proto.DirectChat direct,
IMap<proto.TypedKey, proto.Contact> contactMap, bool busy) {
final contact = contactMap[direct.localConversationRecordKey];
if (contact == null) {
return const Text('...');
}
return ChatSingleContactItemWidget(contact: contact, disabled: busy)
.paddingLTRB(0, 4, 0, 0);
}
List<proto.Chat> _itemFilter(IMap<proto.TypedKey, proto.Contact> contactMap,
IList<DHTShortArrayElementState<Chat>> chatList, String filter) {
final lowerValue = filter.toLowerCase();
return chatList.map((x) => x.value).where((c) {
switch (c.whichKind()) {
case proto.Chat_Kind.direct:
final contact = contactMap[c.direct.localConversationRecordKey];
if (contact == null) {
return false;
}
return contact.nickname.toLowerCase().contains(lowerValue) ||
contact.profile.name.toLowerCase().contains(lowerValue) ||
contact.profile.pronouns.toLowerCase().contains(lowerValue);
case proto.Chat_Kind.group:
// xxx: how to filter group chats
return true;
case proto.Chat_Kind.notSet:
throw StateError('unknown chat kind');
}
}).toList();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final contactListV = context.watch<ContactListCubit>().state;
return contactListV.builder((context, contactList) {
final contactMap = IMap.fromIterable(contactList,
keyMapper: (c) => c.value.localConversationRecordKey,
valueMapper: (c) => c.value);
final chatListV = context.watch<ChatListCubit>().state;
return chatListV
.builder((context, chatList) => SizedBox.expand(
child: styledTitleContainer(
context: context,
title: translate('chat_list.chats'),
child: SizedBox.expand(
child: (chatList.isEmpty)
? const EmptyChatListWidget()
: SearchableList<proto.Chat>(
initialList: chatList.map((x) => x.value).toList(),
itemBuilder: (c) {
switch (c.whichKind()) {
case proto.Chat_Kind.direct:
return _itemBuilderDirect(
c.direct,
contactMap,
contactListV.busy || chatListV.busy);
case proto.Chat_Kind.group:
return const Text(
'group chats not yet supported!');
case proto.Chat_Kind.notSet:
throw StateError('unknown chat kind');
}
},
filter: (value) =>
_itemFilter(contactMap, chatList, value),
spaceBetweenSearchAndList: 4,
inputDecoration: InputDecoration(
labelText: translate('chat_list.search'),
),
),
).paddingAll(8))))
.paddingLTRB(8, 0, 8, 8);
});
}
}

View File

@ -1,76 +0,0 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:searchable_listview/searchable_listview.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import '../chat_list.dart';
class ChatSingleContactListWidget extends StatelessWidget {
const ChatSingleContactListWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final contactListV = context.watch<ContactListCubit>().state;
return contactListV.builder((context, contactList) {
final contactMap = IMap.fromIterable(contactList,
keyMapper: (c) => c.value.localConversationRecordKey,
valueMapper: (c) => c.value);
final chatListV = context.watch<ChatListCubit>().state;
return chatListV
.builder((context, chatList) => SizedBox.expand(
child: styledTitleContainer(
context: context,
title: translate('chat_list.chats'),
child: SizedBox.expand(
child: (chatList.isEmpty)
? const EmptyChatListWidget()
: SearchableList<proto.Chat>(
initialList: chatList.map((x) => x.value).toList(),
itemBuilder: (c) {
final contact =
contactMap[c.localConversationRecordKey];
if (contact == null) {
return const Text('...');
}
return ChatSingleContactItemWidget(
contact: contact,
disabled: contactListV.busy)
.paddingLTRB(0, 4, 0, 0);
},
filter: (value) {
final lowerValue = value.toLowerCase();
return chatList.map((x) => x.value).where((c) {
final contact =
contactMap[c.localConversationRecordKey];
if (contact == null) {
return false;
}
return contact.nickname
.toLowerCase()
.contains(lowerValue) ||
contact.profile.name
.toLowerCase()
.contains(lowerValue) ||
contact.profile.pronouns
.toLowerCase()
.contains(lowerValue);
}).toList();
},
spaceBetweenSearchAndList: 4,
inputDecoration: InputDecoration(
labelText: translate('chat_list.search'),
),
),
).paddingAll(8))))
.paddingLTRB(8, 0, 8, 8);
});
}
}

View File

@ -1,3 +1,3 @@
export 'chat_list_widget.dart';
export 'chat_single_contact_item_widget.dart';
export 'chat_single_contact_list_widget.dart';
export 'empty_chat_list_widget.dart';

View File

@ -59,8 +59,11 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
// Verify
final idcs = await contactSuperIdentity.currentInstance.cryptoSystem;
final signature = signedContactResponse.identitySignature.toVeilid();
await idcs.verify(contactSuperIdentity.currentInstance.publicKey,
contactResponseBytes, signature);
if (!await idcs.verify(contactSuperIdentity.currentInstance.publicKey,
contactResponseBytes, signature)) {
// Could not verify signature of contact response
return AsyncValue.error('Invalid signature on contact response.');
}
// Check for rejection
if (!contactResponse.accept) {

View File

@ -3,6 +3,7 @@ import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import 'cubits.dart';
@ -20,13 +21,26 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
WaitingInvitationsBlocMapCubit(
{required AccountInfo accountInfo,
required AccountRecordCubit accountRecordCubit,
required ContactInvitationListCubit contactInvitationListCubit})
required ContactInvitationListCubit contactInvitationListCubit,
required ContactListCubit contactListCubit})
: _accountInfo = accountInfo,
_accountRecordCubit = accountRecordCubit {
_accountRecordCubit = accountRecordCubit,
_contactInvitationListCubit = contactInvitationListCubit,
_contactListCubit = contactListCubit {
// React to invitation status changes
_singleInvitationStatusProcessor.follow(
stream, state, _invitationStatusListener);
// Follow the contact invitation list cubit
follow(contactInvitationListCubit);
}
@override
Future<void> close() async {
await _singleInvitationStatusProcessor.unfollow();
await super.close();
}
Future<void> _addWaitingInvitation(
{required proto.ContactInvitationRecord
contactInvitationRecord}) async =>
@ -40,16 +54,60 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
accountRecordCubit: _accountRecordCubit,
contactInvitationRecord: contactInvitationRecord)));
// Process all accepted or rejected invitations
Future<void> _invitationStatusListener(
WaitingInvitationsBlocMapState newState) async {
for (final entry in newState.entries) {
final contactRequestInboxRecordKey = entry.key;
final invStatus = entry.value.asData?.value;
// Skip invitations that have not yet been accepted or rejected
if (invStatus == null) {
continue;
}
// Delete invitation and process the accepted or rejected contact
final acceptedContact = invStatus.acceptedContact;
if (acceptedContact != null) {
await _contactInvitationListCubit.deleteInvitation(
accepted: true,
contactRequestInboxRecordKey: contactRequestInboxRecordKey);
// Accept
await _contactListCubit.createContact(
profile: acceptedContact.remoteProfile,
remoteSuperIdentity: acceptedContact.remoteIdentity,
remoteConversationRecordKey:
acceptedContact.remoteConversationRecordKey,
localConversationRecordKey:
acceptedContact.localConversationRecordKey,
);
} else {
// Reject
await _contactInvitationListCubit.deleteInvitation(
accepted: false,
contactRequestInboxRecordKey: contactRequestInboxRecordKey);
}
}
}
/// StateFollower /////////////////////////
@override
Future<void> removeFromState(TypedKey key) => remove(key);
@override
Future<void> updateState(TypedKey key, proto.ContactInvitationRecord value) =>
_addWaitingInvitation(contactInvitationRecord: value);
Future<void> updateState(
TypedKey key,
proto.ContactInvitationRecord? oldValue,
proto.ContactInvitationRecord newValue) async {
await _addWaitingInvitation(contactInvitationRecord: newValue);
}
////
final AccountInfo _accountInfo;
final AccountRecordCubit _accountRecordCubit;
final ContactInvitationListCubit _contactInvitationListCubit;
final ContactListCubit _contactListCubit;
final _singleInvitationStatusProcessor =
SingleStateProcessor<WaitingInvitationsBlocMapState>();
}

View File

@ -13,17 +13,27 @@ import '../conversation.dart';
@immutable
class ActiveConversationState extends Equatable {
const ActiveConversationState({
required this.contact,
required this.remoteIdentityPublicKey,
required this.localConversationRecordKey,
required this.remoteConversationRecordKey,
required this.localConversation,
required this.remoteConversation,
});
final proto.Contact contact;
final TypedKey remoteIdentityPublicKey;
final TypedKey localConversationRecordKey;
final TypedKey remoteConversationRecordKey;
final proto.Conversation localConversation;
final proto.Conversation remoteConversation;
@override
List<Object?> get props => [contact, localConversation, remoteConversation];
List<Object?> get props => [
remoteIdentityPublicKey,
localConversationRecordKey,
remoteConversationRecordKey,
localConversation,
remoteConversation
];
}
typedef ActiveConversationCubit = TransformerCubit<
@ -37,9 +47,11 @@ typedef ActiveConversationsBlocMapState
// Map of localConversationRecordKey to ActiveConversationCubit
// Wraps a conversation cubit to only expose completely built conversations
// Automatically follows the state of a ChatListCubit.
// Even though 'conversations' are per-contact and not per-chat
// We currently only build the cubits for the chats that are active, not
// archived chats or contacts that are not actively in a chat.
//
// TODO: Polling contacts for new inactive chats is yet to be done
//
class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<ActiveConversationState>, ActiveConversationCubit>
with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> {
@ -62,14 +74,11 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
// Private Implementation
// Add an active conversation to be tracked for changes
Future<void> _addConversation({required proto.Contact contact}) async =>
Future<void> _addDirectConversation(
{required TypedKey remoteIdentityPublicKey,
required TypedKey localConversationRecordKey,
required TypedKey remoteConversationRecordKey}) async =>
add(() {
final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
final localConversationRecordKey =
contact.localConversationRecordKey.toVeilid();
final remoteConversationRecordKey =
contact.remoteConversationRecordKey.toVeilid();
// Conversation cubit the tracks the state between the local
// and remote halves of a contact's relationship with this account
final conversationCubit = ConversationCubit(
@ -105,14 +114,16 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
data.remoteConversation == null)
? const AsyncValue.loading()
: AsyncValue.data(ActiveConversationState(
contact: contact,
localConversation: data.localConversation!,
remoteConversation: data.remoteConversation!)),
remoteConversation: data.remoteConversation!,
remoteIdentityPublicKey: remoteIdentityPublicKey,
localConversationRecordKey: localConversationRecordKey,
remoteConversationRecordKey:
remoteConversationRecordKey)),
loading: AsyncValue.loading,
error: AsyncValue.error));
return MapEntry(
contact.localConversationRecordKey.toVeilid(), transformedCubit);
return MapEntry(localConversationRecordKey, transformedCubit);
});
/// StateFollower /////////////////////////
@ -121,20 +132,44 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
Future<void> removeFromState(TypedKey key) => remove(key);
@override
Future<void> updateState(TypedKey key, proto.Chat value) async {
final contactList = _contactListCubit.state.state.asData?.value;
if (contactList == null) {
await addState(key, const AsyncValue.loading());
return;
Future<void> updateState(
TypedKey key, proto.Chat? oldValue, proto.Chat newValue) async {
switch (newValue.whichKind()) {
case proto.Chat_Kind.notSet:
throw StateError('unknown chat kind');
case proto.Chat_Kind.direct:
final localConversationRecordKey =
newValue.direct.localConversationRecordKey.toVeilid();
final remoteIdentityPublicKey =
newValue.direct.remoteMember.remoteIdentityPublicKey.toVeilid();
final remoteConversationRecordKey =
newValue.direct.remoteMember.remoteConversationRecordKey.toVeilid();
if (oldValue != null) {
final oldLocalConversationRecordKey =
oldValue.direct.localConversationRecordKey.toVeilid();
final oldRemoteIdentityPublicKey =
oldValue.direct.remoteMember.remoteIdentityPublicKey.toVeilid();
final oldRemoteConversationRecordKey = oldValue
.direct.remoteMember.remoteConversationRecordKey
.toVeilid();
if (oldLocalConversationRecordKey == localConversationRecordKey &&
oldRemoteIdentityPublicKey == remoteIdentityPublicKey &&
oldRemoteConversationRecordKey == remoteConversationRecordKey) {
return;
}
}
await _addDirectConversation(
remoteIdentityPublicKey: remoteIdentityPublicKey,
localConversationRecordKey: localConversationRecordKey,
remoteConversationRecordKey: remoteConversationRecordKey);
break;
case proto.Chat_Kind.group:
break;
}
final contactIndex = contactList.indexWhere(
(c) => c.value.localConversationRecordKey.toVeilid() == key);
if (contactIndex == -1) {
await addState(key, AsyncValue.error('Contact not found'));
return;
}
final contact = contactList[contactIndex];
await _addConversation(contact: contact.value);
}
////

View File

@ -2,16 +2,42 @@ import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../chat/chat.dart';
import '../../chat_list/cubits/chat_list_cubit.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import '../conversation.dart';
import 'active_conversations_bloc_map_cubit.dart';
@immutable
class _SingleContactChatState extends Equatable {
const _SingleContactChatState(
{required this.remoteIdentityPublicKey,
required this.localConversationRecordKey,
required this.remoteConversationRecordKey,
required this.localMessagesRecordKey,
required this.remoteMessagesRecordKey});
final TypedKey remoteIdentityPublicKey;
final TypedKey localConversationRecordKey;
final TypedKey remoteConversationRecordKey;
final TypedKey localMessagesRecordKey;
final TypedKey remoteMessagesRecordKey;
@override
// TODO: implement props
List<Object?> get props => [
remoteIdentityPublicKey,
localConversationRecordKey,
remoteConversationRecordKey,
localMessagesRecordKey,
remoteMessagesRecordKey
];
}
// Map of localConversationRecordKey to MessagesCubit
// Wraps a MessagesCubit to stream the latest messages to the state
// Automatically follows the state of a ActiveConversationsBlocMapCubit.
@ -20,36 +46,42 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
with
StateMapFollower<ActiveConversationsBlocMapState, TypedKey,
AsyncValue<ActiveConversationState>> {
ActiveSingleContactChatBlocMapCubit(
{required AccountInfo accountInfo,
required ActiveConversationsBlocMapCubit activeConversationsBlocMapCubit,
required ContactListCubit contactListCubit,
required ChatListCubit chatListCubit})
: _accountInfo = accountInfo,
_contactListCubit = contactListCubit,
_chatListCubit = chatListCubit {
ActiveSingleContactChatBlocMapCubit({
required AccountInfo accountInfo,
required ActiveConversationsBlocMapCubit activeConversationsBlocMapCubit,
}) : _accountInfo = accountInfo {
// Follow the active conversations bloc map cubit
follow(activeConversationsBlocMapCubit);
}
Future<void> _addConversationMessages(
{required proto.Contact contact,
required proto.Chat chat,
required proto.Conversation localConversation,
required proto.Conversation remoteConversation}) async =>
Future<void> _addConversationMessages(_SingleContactChatState state) async =>
add(() => MapEntry(
contact.localConversationRecordKey.toVeilid(),
state.localConversationRecordKey,
SingleContactMessagesCubit(
accountInfo: _accountInfo,
remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(),
localConversationRecordKey:
contact.localConversationRecordKey.toVeilid(),
remoteConversationRecordKey:
contact.remoteConversationRecordKey.toVeilid(),
localMessagesRecordKey: localConversation.messages.toVeilid(),
remoteMessagesRecordKey: remoteConversation.messages.toVeilid(),
remoteIdentityPublicKey: state.remoteIdentityPublicKey,
localConversationRecordKey: state.localConversationRecordKey,
remoteConversationRecordKey: state.remoteConversationRecordKey,
localMessagesRecordKey: state.localMessagesRecordKey,
remoteMessagesRecordKey: state.remoteMessagesRecordKey,
)));
_SingleContactChatState? _mapStateValue(
AsyncValue<ActiveConversationState> avInputState) {
final inputState = avInputState.asData?.value;
if (inputState == null) {
return null;
}
return _SingleContactChatState(
remoteIdentityPublicKey: inputState.remoteIdentityPublicKey,
localConversationRecordKey: inputState.localConversationRecordKey,
remoteConversationRecordKey: inputState.remoteConversationRecordKey,
localMessagesRecordKey:
inputState.localConversation.messages.toVeilid(),
remoteMessagesRecordKey:
inputState.remoteConversation.messages.toVeilid());
}
/// StateFollower /////////////////////////
@override
@ -57,49 +89,27 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit<TypedKey,
@override
Future<void> updateState(
TypedKey key, AsyncValue<ActiveConversationState> value) async {
// Get the contact object for this single contact chat
final contactList = _contactListCubit.state.state.asData?.value;
if (contactList == null) {
TypedKey key,
AsyncValue<ActiveConversationState>? oldValue,
AsyncValue<ActiveConversationState> newValue) async {
final newState = _mapStateValue(newValue);
if (oldValue != null) {
final oldState = _mapStateValue(oldValue);
if (oldState == newState) {
return;
}
}
if (newState != null) {
await _addConversationMessages(newState);
} else if (newValue.isLoading) {
await addState(key, const AsyncValue.loading());
return;
} else {
final (error, stackTrace) =
(newValue.asError!.error, newValue.asError!.stackTrace);
await addState(key, AsyncValue.error(error, stackTrace));
}
final contactIndex = contactList.indexWhere(
(c) => c.value.localConversationRecordKey.toVeilid() == key);
if (contactIndex == -1) {
await addState(
key, AsyncValue.error('Contact not found for conversation'));
return;
}
final contact = contactList[contactIndex].value;
// Get the chat object for this single contact chat
final chatList = _chatListCubit.state.state.asData?.value;
if (chatList == null) {
await addState(key, const AsyncValue.loading());
return;
}
final chatIndex = chatList.indexWhere(
(c) => c.value.localConversationRecordKey.toVeilid() == key);
if (contactIndex == -1) {
await addState(key, AsyncValue.error('Chat not found for conversation'));
return;
}
final chat = chatList[chatIndex].value;
await value.when(
data: (state) => _addConversationMessages(
contact: contact,
chat: chat,
localConversation: state.localConversation,
remoteConversation: state.remoteConversation),
loading: () => addState(key, const AsyncValue.loading()),
error: (error, stackTrace) =>
addState(key, AsyncValue.error(error, stackTrace)));
}
////
final AccountInfo _accountInfo;
final ContactListCubit _contactListCubit;
final ChatListCubit _chatListCubit;
}

View File

@ -25,7 +25,7 @@ class ChatsPageState extends State<ChatsPage> {
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Column(children: <Widget>[
const ChatSingleContactListWidget().expanded(),
const ChatListWidget().expanded(),
]);
}
}

View File

@ -2,15 +2,12 @@ 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 '../../contact_invitation/contact_invitation.dart';
import '../../contacts/contacts.dart';
import '../../theme/theme.dart';
import '../../tools/tools.dart';
import 'active_account_page_controller_wrapper.dart';
@ -39,48 +36,6 @@ class HomeScreenState extends State<HomeScreen> {
super.dispose();
}
// 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 _buildAccountReadyDeviceSpecific(BuildContext context) {
final hasActiveChat = context.watch<ActiveChatCubit>().state != null;
if (responsiveVisibility(
@ -110,24 +65,18 @@ class HomeScreenState extends State<HomeScreen> {
// Re-export all ready blocs to the account display subtree
return perAccountCollectionState.provide(
child: MultiBlocListener(listeners: [
BlocListener<WaitingInvitationsBlocMapCubit,
WaitingInvitationsBlocMapState>(
listener: _invitationStatusListener,
)
], child: Builder(builder: _buildAccountReadyDeviceSpecific)));
child: Builder(builder: _buildAccountReadyDeviceSpecific));
}
}
Widget _buildAccountPageView(BuildContext context) {
final localAccounts = context.watch<LocalAccountsCubit>().state;
final activeLocalAccountCubit =
context.watch<ActiveLocalAccountCubit>().state;
final activeLocalAccount = context.watch<ActiveLocalAccountCubit>().state;
final perAccountCollectionBlocMapState =
context.watch<PerAccountCollectionBlocMapCubit>().state;
final activeIndex = localAccounts.indexWhere(
(x) => x.superIdentity.recordKey == activeLocalAccountCubit);
final activeIndex = localAccounts
.indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount);
if (activeIndex == -1) {
return const HomeNoActive();
}
@ -208,6 +157,4 @@ class HomeScreenState extends State<HomeScreen> {
}
final _zoomDrawerController = ZoomDrawerController();
final _singleInvitationStatusProcessor =
SingleStateProcessor<WaitingInvitationsBlocMapState>();
}

View File

@ -34,3 +34,16 @@ extension ContactExt on proto.Contact {
String get displayName =>
nickname.isNotEmpty ? '$nickname (${profile.name})' : profile.name;
}
extension ChatExt on proto.Chat {
TypedKey get localConversationRecordKey {
switch (whichKind()) {
case proto.Chat_Kind.direct:
return direct.localConversationRecordKey.toVeilid();
case proto.Chat_Kind.group:
return group.localConversationRecordKey.toVeilid();
case proto.Chat_Kind.notSet:
throw StateError('unknown chat kind');
}
}
}

View File

@ -1255,16 +1255,15 @@ class Conversation extends $pb.GeneratedMessage {
$0.TypedKey ensureMessages() => $_ensure(2);
}
class Chat extends $pb.GeneratedMessage {
factory Chat() => create();
Chat._() : super();
factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
class ChatMember extends $pb.GeneratedMessage {
factory ChatMember() => create();
ChatMember._() : super();
factory ChatMember.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory ChatMember.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatMember', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'remoteIdentityPublicKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
..hasRequiredFields = false
;
@ -1272,22 +1271,79 @@ class Chat extends $pb.GeneratedMessage {
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
Chat clone() => Chat()..mergeFromMessage(this);
ChatMember clone() => ChatMember()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat;
ChatMember copyWith(void Function(ChatMember) updates) => super.copyWith((message) => updates(message as ChatMember)) as ChatMember;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Chat create() => Chat._();
Chat createEmptyInstance() => create();
static $pb.PbList<Chat> createRepeated() => $pb.PbList<Chat>();
static ChatMember create() => ChatMember._();
ChatMember createEmptyInstance() => create();
static $pb.PbList<ChatMember> createRepeated() => $pb.PbList<ChatMember>();
@$core.pragma('dart2js:noInline')
static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Chat>(create);
static Chat? _defaultInstance;
static ChatMember getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ChatMember>(create);
static ChatMember? _defaultInstance;
@$pb.TagNumber(1)
$0.TypedKey get remoteIdentityPublicKey => $_getN(0);
@$pb.TagNumber(1)
set remoteIdentityPublicKey($0.TypedKey v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasRemoteIdentityPublicKey() => $_has(0);
@$pb.TagNumber(1)
void clearRemoteIdentityPublicKey() => clearField(1);
@$pb.TagNumber(1)
$0.TypedKey ensureRemoteIdentityPublicKey() => $_ensure(0);
@$pb.TagNumber(2)
$0.TypedKey get remoteConversationRecordKey => $_getN(1);
@$pb.TagNumber(2)
set remoteConversationRecordKey($0.TypedKey v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasRemoteConversationRecordKey() => $_has(1);
@$pb.TagNumber(2)
void clearRemoteConversationRecordKey() => clearField(2);
@$pb.TagNumber(2)
$0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(1);
}
class DirectChat extends $pb.GeneratedMessage {
factory DirectChat() => create();
DirectChat._() : super();
factory DirectChat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory DirectChat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DirectChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
..aOM<ChatMember>(3, _omitFieldNames ? '' : 'remoteMember', subBuilder: ChatMember.create)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
DirectChat clone() => DirectChat()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
DirectChat copyWith(void Function(DirectChat) updates) => super.copyWith((message) => updates(message as DirectChat)) as DirectChat;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static DirectChat create() => DirectChat._();
DirectChat createEmptyInstance() => create();
static $pb.PbList<DirectChat> createRepeated() => $pb.PbList<DirectChat>();
@$core.pragma('dart2js:noInline')
static DirectChat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DirectChat>(create);
static DirectChat? _defaultInstance;
@$pb.TagNumber(1)
ChatSettings get settings => $_getN(0);
@ -1312,15 +1368,15 @@ class Chat extends $pb.GeneratedMessage {
$0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1);
@$pb.TagNumber(3)
$0.TypedKey get remoteConversationRecordKey => $_getN(2);
ChatMember get remoteMember => $_getN(2);
@$pb.TagNumber(3)
set remoteConversationRecordKey($0.TypedKey v) { setField(3, v); }
set remoteMember(ChatMember v) { setField(3, v); }
@$pb.TagNumber(3)
$core.bool hasRemoteConversationRecordKey() => $_has(2);
$core.bool hasRemoteMember() => $_has(2);
@$pb.TagNumber(3)
void clearRemoteConversationRecordKey() => clearField(3);
void clearRemoteMember() => clearField(3);
@$pb.TagNumber(3)
$0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2);
ChatMember ensureRemoteMember() => $_ensure(2);
}
class GroupChat extends $pb.GeneratedMessage {
@ -1331,8 +1387,10 @@ class GroupChat extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GroupChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<ChatSettings>(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
..pc<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKeys', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create)
..aOM<Membership>(2, _omitFieldNames ? '' : 'membership', subBuilder: Membership.create)
..aOM<Permissions>(3, _omitFieldNames ? '' : 'permissions', subBuilder: Permissions.create)
..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
..pc<ChatMember>(5, _omitFieldNames ? '' : 'remoteMembers', $pb.PbFieldType.PM, subBuilder: ChatMember.create)
..hasRequiredFields = false
;
@ -1369,18 +1427,111 @@ class GroupChat extends $pb.GeneratedMessage {
ChatSettings ensureSettings() => $_ensure(0);
@$pb.TagNumber(2)
$0.TypedKey get localConversationRecordKey => $_getN(1);
Membership get membership => $_getN(1);
@$pb.TagNumber(2)
set localConversationRecordKey($0.TypedKey v) { setField(2, v); }
set membership(Membership v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasLocalConversationRecordKey() => $_has(1);
$core.bool hasMembership() => $_has(1);
@$pb.TagNumber(2)
void clearLocalConversationRecordKey() => clearField(2);
void clearMembership() => clearField(2);
@$pb.TagNumber(2)
$0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1);
Membership ensureMembership() => $_ensure(1);
@$pb.TagNumber(3)
$core.List<$0.TypedKey> get remoteConversationRecordKeys => $_getList(2);
Permissions get permissions => $_getN(2);
@$pb.TagNumber(3)
set permissions(Permissions v) { setField(3, v); }
@$pb.TagNumber(3)
$core.bool hasPermissions() => $_has(2);
@$pb.TagNumber(3)
void clearPermissions() => clearField(3);
@$pb.TagNumber(3)
Permissions ensurePermissions() => $_ensure(2);
@$pb.TagNumber(4)
$0.TypedKey get localConversationRecordKey => $_getN(3);
@$pb.TagNumber(4)
set localConversationRecordKey($0.TypedKey v) { setField(4, v); }
@$pb.TagNumber(4)
$core.bool hasLocalConversationRecordKey() => $_has(3);
@$pb.TagNumber(4)
void clearLocalConversationRecordKey() => clearField(4);
@$pb.TagNumber(4)
$0.TypedKey ensureLocalConversationRecordKey() => $_ensure(3);
@$pb.TagNumber(5)
$core.List<ChatMember> get remoteMembers => $_getList(4);
}
enum Chat_Kind {
direct,
group,
notSet
}
class Chat extends $pb.GeneratedMessage {
factory Chat() => create();
Chat._() : super();
factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static const $core.Map<$core.int, Chat_Kind> _Chat_KindByTag = {
1 : Chat_Kind.direct,
2 : Chat_Kind.group,
0 : Chat_Kind.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..oo(0, [1, 2])
..aOM<DirectChat>(1, _omitFieldNames ? '' : 'direct', subBuilder: DirectChat.create)
..aOM<GroupChat>(2, _omitFieldNames ? '' : 'group', subBuilder: GroupChat.create)
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
Chat clone() => Chat()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Chat create() => Chat._();
Chat createEmptyInstance() => create();
static $pb.PbList<Chat> createRepeated() => $pb.PbList<Chat>();
@$core.pragma('dart2js:noInline')
static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Chat>(create);
static Chat? _defaultInstance;
Chat_Kind whichKind() => _Chat_KindByTag[$_whichOneof(0)]!;
void clearKind() => clearField($_whichOneof(0));
@$pb.TagNumber(1)
DirectChat get direct => $_getN(0);
@$pb.TagNumber(1)
set direct(DirectChat v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasDirect() => $_has(0);
@$pb.TagNumber(1)
void clearDirect() => clearField(1);
@$pb.TagNumber(1)
DirectChat ensureDirect() => $_ensure(0);
@$pb.TagNumber(2)
GroupChat get group => $_getN(1);
@$pb.TagNumber(2)
set group(GroupChat v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasGroup() => $_has(1);
@$pb.TagNumber(2)
void clearGroup() => clearField(2);
@$pb.TagNumber(2)
GroupChat ensureGroup() => $_ensure(1);
}
class Profile extends $pb.GeneratedMessage {

View File

@ -365,41 +365,76 @@ final $typed_data.Uint8List conversationDescriptor = $convert.base64Decode(
'JvZmlsZRIuChNzdXBlcl9pZGVudGl0eV9qc29uGAIgASgJUhFzdXBlcklkZW50aXR5SnNvbhIs'
'CghtZXNzYWdlcxgDIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIIbWVzc2FnZXM=');
@$core.Deprecated('Use chatDescriptor instead')
const Chat$json = {
'1': 'Chat',
@$core.Deprecated('Use chatMemberDescriptor instead')
const ChatMember$json = {
'1': 'ChatMember',
'2': [
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'},
{'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
{'1': 'remote_conversation_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
{'1': 'remote_identity_public_key', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteIdentityPublicKey'},
{'1': 'remote_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
],
};
/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatDescriptor = $convert.base64Decode(
'CgRDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3NSCHNldH'
'RpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVpbGlkLlR5'
'cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRJVCh5yZW1vdGVfY29udmVyc2F0aW'
'9uX3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdGlv'
'blJlY29yZEtleQ==');
/// Descriptor for `ChatMember`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatMemberDescriptor = $convert.base64Decode(
'CgpDaGF0TWVtYmVyEk0KGnJlbW90ZV9pZGVudGl0eV9wdWJsaWNfa2V5GAEgASgLMhAudmVpbG'
'lkLlR5cGVkS2V5UhdyZW1vdGVJZGVudGl0eVB1YmxpY0tleRJVCh5yZW1vdGVfY29udmVyc2F0'
'aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdG'
'lvblJlY29yZEtleQ==');
@$core.Deprecated('Use directChatDescriptor instead')
const DirectChat$json = {
'1': 'DirectChat',
'2': [
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'},
{'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
{'1': 'remote_member', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.ChatMember', '10': 'remoteMember'},
],
};
/// Descriptor for `DirectChat`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List directChatDescriptor = $convert.base64Decode(
'CgpEaXJlY3RDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3'
'NSCHNldHRpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVp'
'bGlkLlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRI7Cg1yZW1vdGVfbWVtYm'
'VyGAMgASgLMhYudmVpbGlkY2hhdC5DaGF0TWVtYmVyUgxyZW1vdGVNZW1iZXI=');
@$core.Deprecated('Use groupChatDescriptor instead')
const GroupChat$json = {
'1': 'GroupChat',
'2': [
{'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'},
{'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
{'1': 'remote_conversation_record_keys', '3': 3, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKeys'},
{'1': 'membership', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Membership', '10': 'membership'},
{'1': 'permissions', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.Permissions', '10': 'permissions'},
{'1': 'local_conversation_record_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
{'1': 'remote_members', '3': 5, '4': 3, '5': 11, '6': '.veilidchat.ChatMember', '10': 'remoteMembers'},
],
};
/// Descriptor for `GroupChat`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List groupChatDescriptor = $convert.base64Decode(
'CglHcm91cENoYXQSNAoIc2V0dGluZ3MYASABKAsyGC52ZWlsaWRjaGF0LkNoYXRTZXR0aW5nc1'
'IIc2V0dGluZ3MSUwodbG9jYWxfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWls'
'aWQuVHlwZWRLZXlSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5ElcKH3JlbW90ZV9jb252ZX'
'JzYXRpb25fcmVjb3JkX2tleXMYAyADKAsyEC52ZWlsaWQuVHlwZWRLZXlSHHJlbW90ZUNvbnZl'
'cnNhdGlvblJlY29yZEtleXM=');
'IIc2V0dGluZ3MSNgoKbWVtYmVyc2hpcBgCIAEoCzIWLnZlaWxpZGNoYXQuTWVtYmVyc2hpcFIK'
'bWVtYmVyc2hpcBI5CgtwZXJtaXNzaW9ucxgDIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbn'
'NSC3Blcm1pc3Npb25zElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAQgASgLMhAu'
'dmVpbGlkLlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRI9Cg5yZW1vdGVfbW'
'VtYmVycxgFIAMoCzIWLnZlaWxpZGNoYXQuQ2hhdE1lbWJlclINcmVtb3RlTWVtYmVycw==');
@$core.Deprecated('Use chatDescriptor instead')
const Chat$json = {
'1': 'Chat',
'2': [
{'1': 'direct', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.DirectChat', '9': 0, '10': 'direct'},
{'1': 'group', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.GroupChat', '9': 0, '10': 'group'},
],
'8': [
{'1': 'kind'},
],
};
/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatDescriptor = $convert.base64Decode(
'CgRDaGF0EjAKBmRpcmVjdBgBIAEoCzIWLnZlaWxpZGNoYXQuRGlyZWN0Q2hhdEgAUgZkaXJlY3'
'QSLQoFZ3JvdXAYAiABKAsyFS52ZWlsaWRjaGF0Lkdyb3VwQ2hhdEgAUgVncm91cEIGCgRraW5k');
@$core.Deprecated('Use profileDescriptor instead')
const Profile$json = {

View File

@ -267,15 +267,23 @@ message Conversation {
veilid.TypedKey messages = 3;
}
// Either a 1-1 conversation or a group chat
// A member of chat which may or may not be associated with a contact
message ChatMember {
// The identity public key most recently associated with the chat member
veilid.TypedKey remote_identity_public_key = 1;
// Conversation key for the other party
veilid.TypedKey remote_conversation_record_key = 2;
}
// A 1-1 chat
// Privately encrypted, this is the local user's copy of the chat
message Chat {
message DirectChat {
// Settings
ChatSettings settings = 1;
// Conversation key for this user
veilid.TypedKey local_conversation_record_key = 2;
// Conversation key for the other party
veilid.TypedKey remote_conversation_record_key = 3;
ChatMember remote_member = 3;
}
// A group chat
@ -283,10 +291,22 @@ message Chat {
message GroupChat {
// Settings
ChatSettings settings = 1;
// Membership
Membership membership = 2;
// Permissions
Permissions permissions = 3;
// Conversation key for this user
veilid.TypedKey local_conversation_record_key = 2;
veilid.TypedKey local_conversation_record_key = 4;
// Conversation keys for the other parties
repeated veilid.TypedKey remote_conversation_record_keys = 3;
repeated ChatMember remote_members = 5;
}
// Some kind of chat
message Chat {
oneof kind {
DirectChat direct = 1;
GroupChat group = 2;
}
}
////////////////////////////////////////////////////////////////////////////////////

View File

@ -609,7 +609,7 @@ class _DHTLogSpine {
// Don't watch for local changes because this class already handles
// notifying listeners and knows when it makes local changes
_subscription ??=
await _spineRecord.listen(localChanges: false, _onSpineChanged);
await _spineRecord.listen(localChanges: true, _onSpineChanged);
} on Exception {
// If anything fails, try to cancel the watches
await cancelWatch();

View File

@ -12,14 +12,6 @@ class DefaultDHTRecordCubit<T> extends DHTRecordCubit<T> {
stateFunction: _makeStateFunction(decodeState),
watchFunction: _makeWatchFunction());
// DefaultDHTRecordCubit.value({
// required super.record,
// required T Function(List<int> data) decodeState,
// }) : super.value(
// initialStateFunction: _makeInitialStateFunction(decodeState),
// stateFunction: _makeStateFunction(decodeState),
// watchFunction: _makeWatchFunction());
static InitialStateFunction<T> _makeInitialStateFunction<T>(
T Function(List<int> data) decodeState) =>
(record) async {

View File

@ -29,20 +29,6 @@ class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
});
}
// DHTRecordCubit.value({
// required DHTRecord record,
// required InitialStateFunction<T> initialStateFunction,
// required StateFunction<T> stateFunction,
// required WatchFunction watchFunction,
// }) : _record = record,
// _stateFunction = stateFunction,
// _wantsCloseRecord = false,
// super(const AsyncValue.loading()) {
// Future.delayed(Duration.zero, () async {
// await _init(initialStateFunction, stateFunction, watchFunction);
// });
// }
Future<void> _init(
InitialStateFunction<T> initialStateFunction,
StateFunction<T> stateFunction,