active conversations cubit and blocmapcubit

This commit is contained in:
Christien Rioux 2024-02-09 21:17:28 -05:00
parent aa376a449d
commit 43dbf26cc0
9 changed files with 216 additions and 72 deletions

View File

@ -115,96 +115,97 @@ class ChatComponentState extends State<ChatComponent> {
final activeChatCubit = context.watch<ActiveChatCubit>();
final contactListCubit = context.watch<ContactListCubit>();
final activeAccountInfo = context.watch<ActiveAccountInfo>();
final activeChatContactKey = activeChatCubit.state;
if (activeChatContactKey == null) {
return const NoConversationWidget();
}
return contactListCubit.state.builder((context, contactList) {
// Get active chat contact profile
final activeChatContactIdx = contactList.indexWhere(
(c) => activeChatContactKey == c.remoteConversationRecordKey);
late final proto.Contact activeChatContact;
if (activeChatContactIdx == -1) {
activeChatCubit.setActiveChat(null);
return const NoConversationWidget();
} else {
activeChatContact = contactList[activeChatContactIdx];
}
final contactName = activeChatContact.editedProfile.name;
// Make a messages cubit for this conversation
xxx
// final protoMessages =
// ref.watch(activeConversationMessagesProvider).asData?.value;
// if (protoMessages == null) {
// return waitingPage(context);
// }
// final messages = <types.Message>[];
// for (final protoMessage in protoMessages) {
// final message = protoMessageToMessage(protoMessage);
// messages.insert(0, message);
// }
final protoMessages =
ref.watch(activeConversationMessagesProvider).asData?.value;
if (protoMessages == null) {
return waitingPage(context);
}
final messages = <types.Message>[];
for (final protoMessage in protoMessages) {
final message = protoMessageToMessage(protoMessage);
messages.insert(0, message);
}
return DefaultTextStyle(
style: textTheme.bodySmall!,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: Stack(
children: [
Column(
return BlocProvider(
create: (context) => MessagesCubit(
activeAccountInfo: activeAccountInfo,
remoteIdentityPublicKey: activeChatContact.identityPublicKey, localConversationRecordKey: activeChatContact.localConversationRecordKey, localMessagesRecordKey: activeChatContact.,
),
child: DefaultTextStyle(
style: textTheme.bodySmall!,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: Stack(
children: [
Container(
height: 48,
decoration: BoxDecoration(
color: scale.primaryScale.subtleBorder,
),
child: Row(children: [
Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(
16, 0, 16, 0),
child: Text(contactName,
textAlign: TextAlign.start,
style: textTheme.titleMedium),
)),
const Spacer(),
IconButton(
icon: const Icon(Icons.close),
onPressed: () async {
context
.read<ActiveChatCubit>()
.setActiveChat(null);
}).paddingLTRB(16, 0, 16, 0)
]),
),
Expanded(
child: DecoratedBox(
decoration: const BoxDecoration(),
child: Chat(
theme: chatTheme,
messages: messages,
//onAttachmentPressed: _handleAttachmentPressed,
//onMessageTap: _handleMessageTap,
//onPreviewDataFetched: _handlePreviewDataFetched,
onSendPressed: (message) {
unawaited(_handleSendPressed(message));
},
//showUserAvatars: false,
//showUserNames: true,
user: _localUser,
Column(
children: [
Container(
height: 48,
decoration: BoxDecoration(
color: scale.primaryScale.subtleBorder,
),
child: Row(children: [
Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(
16, 0, 16, 0),
child: Text(contactName,
textAlign: TextAlign.start,
style: textTheme.titleMedium),
)),
const Spacer(),
IconButton(
icon: const Icon(Icons.close),
onPressed: () async {
context
.read<ActiveChatCubit>()
.setActiveChat(null);
}).paddingLTRB(16, 0, 16, 0)
]),
),
),
Expanded(
child: DecoratedBox(
decoration: const BoxDecoration(),
child: Chat(
theme: chatTheme,
messages: messages,
//onAttachmentPressed: _handleAttachmentPressed,
//onMessageTap: _handleMessageTap,
//onPreviewDataFetched: _handlePreviewDataFetched,
onSendPressed: (message) {
unawaited(_handleSendPressed(message));
},
//showUserAvatars: false,
//showUserNames: true,
user: _localUser,
),
),
),
],
),
],
),
],
),
));
)));
});
}
}

View File

@ -0,0 +1,24 @@
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
class ActiveConversationsCubit extends BlocMapCubit<TypedKey,
AsyncValue<ConversationState>, ConversationCubit> {
ActiveConversationsCubit({required ActiveAccountInfo activeAccountInfo})
: _activeAccountInfo = activeAccountInfo;
Future<void> addConversation({required proto.Contact contact}) async =>
add(() => MapEntry(
contact.remoteConversationRecordKey,
ConversationCubit(
activeAccountInfo: _activeAccountInfo,
remoteIdentityPublicKey: contact.identityPublicKey,
localConversationRecordKey: contact.localConversationRecordKey,
remoteConversationRecordKey: contact.remoteConversationRecordKey,
)));
final ActiveAccountInfo _activeAccountInfo;
}

View File

@ -1 +1,2 @@
export 'active_conversations_cubit.dart';
export 'chat_list_cubit.dart';

View File

@ -24,6 +24,8 @@ class ChatSingleContactItemWidget extends StatelessWidget {
final scale = theme.extension<ScaleScheme>()!;
final activeChatCubit = context.watch<ActiveChatCubit>();
final activeConversationsCubit = context.watch<ActiveConversationsCubit>();
final remoteConversationRecordKey =
proto.TypedKeyProto.fromProto(_contact.remoteConversationRecordKey);
final selected = activeChatCubit.state == remoteConversationRecordKey;
@ -67,6 +69,8 @@ class ChatSingleContactItemWidget extends StatelessWidget {
// component is not dragged.
child: ListTile(
onTap: () {
xxx deal with async
activeConversationsCubit.addConversation(contact: _contact);
activeChatCubit.setActiveChat(remoteConversationRecordKey);
},
title: Text(_contact.editedProfile.name),

View File

@ -9,6 +9,7 @@ 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 '../../../theme/theme.dart';
import '../../../tools/tools.dart';
import 'main_pager/main_pager.dart';
@ -114,10 +115,17 @@ class HomeAccountReadyState extends State<HomeAccountReady>
create: (context) => ContactInvitationListCubit(
activeAccountInfo: activeAccountInfo,
account: accountData.value)),
BlocProvider(
create: (context) => ContactListCubit(
activeAccountInfo: activeAccountInfo,
account: accountData.value)),
BlocProvider(
create: (context) => ChatListCubit(
activeAccountInfo: activeAccountInfo,
account: accountData.value)),
BlocProvider(
create: (context) => ActiveConversationsCubit(
activeAccountInfo: activeAccountInfo)),
BlocProvider(create: (context) => ActiveChatCubit(null))
],
child: responsiveVisibility(

View File

@ -0,0 +1,96 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:veilid_support/veilid_support.dart';
typedef BlocMapState<K, S> = IMap<K, S>;
class _ItemEntry<S, B> {
_ItemEntry({required this.bloc, required this.subscription});
final B bloc;
final StreamSubscription<S> subscription;
}
abstract class BlocMapCubit<K, S, B extends BlocBase<S>>
extends Cubit<BlocMapState<K, S>> {
BlocMapCubit()
: _entries = {},
_tagLock = AsyncTagLock(),
super(IMap<K, S>());
@override
Future<void> close() async {
await _entries.values.map((e) => e.subscription.cancel()).wait;
await _entries.values.map((e) => e.bloc.close()).wait;
await super.close();
}
Future<void> add(MapEntry<K, B> Function() create) {
// Create new element
final newElement = create();
final key = newElement.key;
final bloc = newElement.value;
return _tagLock.protect(key, closure: () async {
// Remove entry with the same key if it exists
await _internalRemove(key);
// Add entry with this key
_entries[key] = _ItemEntry(
bloc: bloc,
subscription: bloc.stream.listen((data) {
// Add sub-cubit's state to the map state
emit(state.add(key, data));
}));
emit(state.add(key, bloc.state));
});
}
Future<void> _internalRemove(K key) async {
final sub = _entries.remove(key);
if (sub != null) {
await sub.subscription.cancel();
await sub.bloc.close();
}
}
Future<void> remove(K key) => _tagLock.protect(key, closure: () async {
await _internalRemove(key);
emit(state.remove(key));
});
R operate<R>(K key, {required R Function(B bloc) closure}) {
final bloc = _entries[key]!.bloc;
return closure(bloc);
}
R? tryOperate<R>(K key, {required R Function(B bloc) closure}) {
final entry = _entries[key];
if (entry == null) {
return null;
}
return closure(entry.bloc);
}
Future<R> operateAsync<R>(K key,
{required Future<R> Function(B bloc) closure}) =>
_tagLock.protect(key, closure: () async {
final bloc = _entries[key]!.bloc;
return closure(bloc);
});
Future<R?> tryOperateAsync<R>(K key,
{required Future<R> Function(B bloc) closure}) =>
_tagLock.protect(key, closure: () async {
final entry = _entries[key];
if (entry == null) {
return null;
}
return closure(entry.bloc);
});
final Map<K, _ItemEntry<S, B>> _entries;
final AsyncTagLock<K> _tagLock;
}

View File

@ -13,12 +13,12 @@ abstract class StreamWrapperCubit<State> extends Cubit<AsyncValue<State>> {
onError: (Object error, StackTrace stackTrace) {
emit(AsyncValue.error(error, stackTrace));
});
}
@override
Future<void> close() async {
await _subscription.cancel();
await super.close();
}
@override
Future<void> close() async {
await _subscription.cancel();
await super.close();
}
late final StreamSubscription<State> _subscription;

View File

@ -1,4 +1,5 @@
export 'animations.dart';
export 'cubit_map.dart';
export 'enter_password.dart';
export 'enter_pin.dart';
export 'loggy.dart';

View File

@ -39,6 +39,15 @@ class AsyncTagLock<T> {
}
}
Future<R> protect<R>(T tag, {required Future<R> Function() closure}) async {
await lockTag(tag);
try {
return await closure();
} finally {
unlockTag(tag);
}
}
//
final Mutex _tableLock;
final Map<T, _AsyncTagLockEntry> _locks;