From ff14969ffafdcabdc707b09d1bfb32b6d4ad80d2 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 11 Feb 2024 14:17:10 -0500 Subject: [PATCH] more messages work --- lib/chat/views/chat_component.dart | 299 +++++++++--------- .../active_conversation_messages_cubit.dart | 2 + .../chat_single_contact_item_widget.dart | 4 - .../views/contact_invitation_display.dart | 2 +- .../views/invite_dialog.dart | 4 +- lib/layout/home/home.dart | 4 +- .../home/home_account_ready/chat_only.dart | 13 +- .../home_account_ready.dart | 12 +- .../main_pager/main_pager.dart | 2 +- lib/layout/home/home_no_active.dart | 2 +- lib/tools/widget_helpers.dart | 60 ++-- .../async_tools/lib/src/single_async.dart | 14 +- 12 files changed, 226 insertions(+), 192 deletions(-) diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 9076a49..ac4fc81 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -18,55 +18,94 @@ import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../chat.dart'; -class ChatComponent extends StatefulWidget { - const ChatComponent({required this.remoteConversationRecordKey, super.key}); +class ChatComponent extends StatelessWidget { + const ChatComponent._( + {required TypedKey localUserIdentityKey, + required TypedKey remoteConversationRecordKey, + required IList messages, + required types.User localUser, + required types.User remoteUser, + super.key}) + : _localUserIdentityKey = localUserIdentityKey, + _remoteConversationRecordKey = remoteConversationRecordKey, + _messages = messages, + _localUser = localUser, + _remoteUser = remoteUser; - @override - ChatComponentState createState() => ChatComponentState(); + final TypedKey _localUserIdentityKey; + final TypedKey _remoteConversationRecordKey; + final IList _messages; + final types.User _localUser; + final types.User _remoteUser; - final TypedKey remoteConversationRecordKey; + // Builder wrapper function that takes care of state management requirements + static Widget builder( + {required TypedKey remoteConversationRecordKey, Key? key}) => + Builder(builder: (context) { + // Get all watched dependendies + final activeAccountInfo = context.watch(); + final accountRecordInfo = + context.watch().state.data?.value; + if (accountRecordInfo == null) { + return debugPage('should always have an account record here'); + } + final contactList = context.watch().state.data?.value; + if (contactList == null) { + return debugPage('should always have a contact list here'); + } + final avconversation = context.select?>( + (x) => x.state[remoteConversationRecordKey]); + if (avconversation == null) { + return debugPage('should always have an active conversation here'); + } + final conversation = avconversation.data?.value; + if (conversation == null) { + return avconversation.buildNotData(); + } - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty( - 'chatRemoteConversationKey', remoteConversationRecordKey)); - } -} + // Make flutter_chat_ui 'User's + final localUserIdentityKey = activeAccountInfo + .localAccount.identityMaster + .identityPublicTypedKey(); -class ChatComponentState extends State { - final _unfocusNode = FocusNode(); - late final types.User _localUser; - late final types.User _remoteUser; + final localUser = types.User( + id: localUserIdentityKey.toString(), + firstName: accountRecordInfo.profile.name, + ); + final editedName = conversation.contact.editedProfile.name; + final remoteUser = types.User( + id: proto.TypedKeyProto.fromProto( + conversation.contact.identityPublicKey) + .toString(), + firstName: editedName); - @override - void initState() { - super.initState(); + // Get the messages to display + // and ensure it is safe to operate() on the MessageCubit for this chat + final avmessages = context.select>?>( + (x) => x.state[remoteConversationRecordKey]); + if (avmessages == null) { + return waitingPage(); + } + final messages = avmessages.data?.value; + if (messages == null) { + return avmessages.buildNotData(); + } - _localUser = types.User( - id: widget.activeAccountInfo.localAccount.identityMaster - .identityPublicTypedKey() - .toString(), - firstName: widget.activeAccountInfo.account.profile.name, - ); - _remoteUser = types.User( - id: proto.TypedKeyProto.fromProto( - widget.activeChatContact.identityPublicKey) - .toString(), - firstName: widget.activeChatContact.remoteProfile.name); - } + return ChatComponent._( + localUserIdentityKey: localUserIdentityKey, + remoteConversationRecordKey: remoteConversationRecordKey, + messages: messages, + localUser: localUser, + remoteUser: remoteUser, + key: key); + }); - @override - void dispose() { - _unfocusNode.dispose(); - super.dispose(); - } + ///////////////////////////////////////////////////////////////////// - types.Message protoMessageToMessage(proto.Message message) { - final isLocal = message.author == - widget.activeAccountInfo.localAccount.identityMaster - .identityPublicTypedKey() - .toProto(); + types.Message messageToChatMessage(proto.Message message) { + final isLocal = message.author == _localUserIdentityKey.toProto(); final textMessage = types.TextMessage( author: isLocal ? _localUser : _remoteUser, @@ -77,142 +116,98 @@ class ChatComponentState extends State { return textMessage; } - Future _addMessage(proto.Message protoMessage) async { - if (protoMessage.text.isEmpty) { + Future _addMessage(BuildContext context, proto.Message message) async { + if (message.text.isEmpty) { return; } - - final message = protoMessageToMessage(protoMessage); - - // setState(() { - // _messages.insert(0, message); - // }); - - // Now add the message to the conversation messages - final localConversationRecordKey = proto.TypedKeyProto.fromProto( - widget.activeChatContact.localConversationRecordKey); - final remoteIdentityPublicKey = proto.TypedKeyProto.fromProto( - widget.activeChatContact.identityPublicKey); - - await addLocalConversationMessage( - activeAccountInfo: widget.activeAccountInfo, - localConversationRecordKey: localConversationRecordKey, - remoteIdentityPublicKey: remoteIdentityPublicKey, - message: protoMessage); - - ref.invalidate(activeConversationMessagesProvider); + await context.read().operate( + _remoteConversationRecordKey, + closure: (messagesCubit) => messagesCubit.addMessage(message: message)); } - Future _handleSendPressed(types.PartialText message) async { + Future _handleSendPressed( + BuildContext context, types.PartialText message) async { final protoMessage = proto.Message() - ..author = widget.activeAccountInfo.localAccount.identityMaster - .identityPublicTypedKey() - .toProto() - ..timestamp = (await eventualVeilid.future).now().toInt64() + ..author = _localUserIdentityKey.toProto() + ..timestamp = Veilid.instance.now().toInt64() ..text = message.text; //..signature = signature; - await _addMessage(protoMessage); + await _addMessage(context, protoMessage); } - void _handleAttachmentPressed() { + Future _handleAttachmentPressed() async { // } @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; final textTheme = Theme.of(context).textTheme; final chatTheme = makeChatTheme(scale, textTheme); - final contactListCubit = context.watch(); + // Convert protobuf messages to chat messages + final chatMessages = []; + for (final message in _messages) { + final chatMessage = messageToChatMessage(message); + chatMessages.insert(0, chatMessage); + } - return contactListCubit.state.builder((context, contactList) { - // Get active chat contact profile - final activeChatContactIdx = contactList.indexWhere((c) => - widget.remoteConversationRecordKey == c.remoteConversationRecordKey); - late final proto.Contact activeChatContact; - if (activeChatContactIdx == -1) { - // xxx: error, no contact for conversation... - return const NoConversationWidget(); - } else { - activeChatContact = contactList[activeChatContactIdx]; - } - final contactName = activeChatContact.editedProfile.name; - - final messages = context.select>?>( - (x) => x.state[widget.remoteConversationRecordKey]); - if (messages == null) { - // xxx: error, no messages for conversation... - return const NoConversationWidget(); - } - return messages.builder((context, protoMessages) { - final messages = []; - 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( + return DefaultTextStyle( + style: textTheme.bodySmall!, + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: Stack( + children: [ + Column( children: [ - 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() - .setActiveChat(null); - }).paddingLTRB(16, 0, 16, 0) - ]), + 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(_remoteUser.firstName!, + textAlign: TextAlign.start, + style: textTheme.titleMedium), + )), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + context.read().setActiveChat(null); + }).paddingLTRB(16, 0, 16, 0) + ]), + ), + Expanded( + child: DecoratedBox( + decoration: const BoxDecoration(), + child: Chat( + theme: chatTheme, + messages: chatMessages, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, + onSendPressed: (message) { + singleFuture(this, + () async => _handleSendPressed(context, message)); + }, + //showUserAvatars: false, + //showUserNames: true, + user: _localUser, ), - 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, - ), - ), - ), - ], + ), ), ], ), - )); - }); - }); + ], + ), + )); } } diff --git a/lib/chat_list/cubits/active_conversation_messages_cubit.dart b/lib/chat_list/cubits/active_conversation_messages_cubit.dart index d7db7a5..18c4c6e 100644 --- a/lib/chat_list/cubits/active_conversation_messages_cubit.dart +++ b/lib/chat_list/cubits/active_conversation_messages_cubit.dart @@ -100,6 +100,8 @@ class ActiveConversationMessagesCubit extends BlocMapCubit()!; final activeChatCubit = context.watch(); - // final activeConversation = context.select(); - // final activeConversationMessagesCubit = - // context.watch(); xxx does this need to be here? - final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(_contact.remoteConversationRecordKey); final selected = activeChatCubit.state == remoteConversationRecordKey; diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 7b6fed5..f54994e 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -89,7 +89,7 @@ class ContactInvitationDisplayDialogState minHeight: cardsize, maxHeight: cardsize), child: signedContactInvitationBytesV.when( - loading: () => buildProgressIndicator(context), + loading: buildProgressIndicator, data: (data) => Form( key: formKey, child: Column(children: [ diff --git a/lib/contact_invitation/views/invite_dialog.dart b/lib/contact_invitation/views/invite_dialog.dart index f09a1e5..f4fe055 100644 --- a/lib/contact_invitation/views/invite_dialog.dart +++ b/lib/contact_invitation/views/invite_dialog.dart @@ -242,7 +242,7 @@ class InviteDialogState extends State { return SizedBox( height: 300, width: 300, - child: buildProgressIndicator(context).toCenter()) + child: buildProgressIndicator().toCenter()) .paddingAll(16); } return ConstrainedBox( @@ -258,7 +258,7 @@ class InviteDialogState extends State { Column(children: [ Text(translate('invite_dialog.validating')) .paddingLTRB(0, 0, 0, 16), - buildProgressIndicator(context).paddingAll(16), + buildProgressIndicator().paddingAll(16), ]).toCenter(), if (_validInvitation == null && !_isValidating && diff --git a/lib/layout/home/home.dart b/lib/layout/home/home.dart index f7adeaf..74cfd54 100644 --- a/lib/layout/home/home.dart +++ b/lib/layout/home/home.dart @@ -56,8 +56,8 @@ class HomePageState extends State with TickerProviderStateMixin { case AccountInfoStatus.accountLocked: return const HomeAccountLocked(); case AccountInfoStatus.accountReady: - return Provider.value( - value: accountInfo.activeAccountInfo, + return Provider.value( + value: accountInfo.activeAccountInfo!, child: BlocProvider( create: (context) => AccountRecordCubit( record: accountInfo.activeAccountInfo!.accountRecord), diff --git a/lib/layout/home/home_account_ready/chat_only.dart b/lib/layout/home/home_account_ready/chat_only.dart index 9e89b40..62d07d8 100644 --- a/lib/layout/home/home_account_ready/chat_only.dart +++ b/lib/layout/home/home_account_ready/chat_only.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../chat/chat.dart'; import '../../../tools/tools.dart'; @@ -31,10 +32,20 @@ class ChatOnlyPageState extends State super.dispose(); } + Widget buildChatComponent(BuildContext context) { + final activeChatRemoteConversationKey = + context.watch().state; + if (activeChatRemoteConversationKey == null) { + return const EmptyChatWidget(); + } + return ChatComponent.builder( + remoteConversationRecordKey: activeChatRemoteConversationKey); + } + @override Widget build(BuildContext context) => SafeArea( child: GestureDetector( onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), - child: const ChatComponent(), + child: buildChatComponent(context), )); } diff --git a/lib/layout/home/home_account_ready/home_account_ready.dart b/lib/layout/home/home_account_ready/home_account_ready.dart index f49f5db..dd75d87 100644 --- a/lib/layout/home/home_account_ready/home_account_ready.dart +++ b/lib/layout/home/home_account_ready/home_account_ready.dart @@ -74,7 +74,15 @@ class HomeAccountReadyState extends State builder: (context) => Material(color: Colors.transparent, child: buildUserPanel())); - Widget buildTabletRightPane(BuildContext context) => const ChatComponent(); + Widget buildTabletRightPane(BuildContext context) { + final activeChatRemoteConversationKey = + context.watch().state; + if (activeChatRemoteConversationKey == null) { + return const EmptyChatWidget(); + } + return ChatComponent.builder( + remoteConversationRecordKey: activeChatRemoteConversationKey); + } // ignore: prefer_expression_function_bodies Widget buildTablet(BuildContext context) { @@ -106,7 +114,7 @@ class HomeAccountReadyState extends State final accountData = context.watch().state.data; if (accountData == null) { - return waitingPage(context); + return waitingPage(); } return MultiBlocProvider( diff --git a/lib/layout/home/home_account_ready/main_pager/main_pager.dart b/lib/layout/home/home_account_ready/main_pager/main_pager.dart index ee5709d..2cbee9c 100644 --- a/lib/layout/home/home_account_ready/main_pager/main_pager.dart +++ b/lib/layout/home/home_account_ready/main_pager/main_pager.dart @@ -143,7 +143,7 @@ class MainPagerState extends State with TickerProviderStateMixin { return _onNewChatBottomSheetBuilder(context); } else { // Unknown error - return waitingPage(context); + return debugPage('unknown page'); } } diff --git a/lib/layout/home/home_no_active.dart b/lib/layout/home/home_no_active.dart index 31e3378..e61fe0e 100644 --- a/lib/layout/home/home_no_active.dart +++ b/lib/layout/home/home_no_active.dart @@ -21,5 +21,5 @@ class HomeNoActiveState extends State { } @override - Widget build(BuildContext context) => waitingPage(context); + Widget build(BuildContext context) => waitingPage(); } diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index 9eddc83..c9ffa41 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -24,56 +24,72 @@ extension ModalProgressExt on Widget { return BlurryModalProgressHUD( inAsyncCall: isLoading, blurEffectIntensity: 4, - progressIndicator: buildProgressIndicator(context), + progressIndicator: buildProgressIndicator(), color: scale.tertiaryScale.appBackground.withAlpha(64), child: this); } } -Widget buildProgressIndicator(BuildContext context) { - final theme = Theme.of(context); - final scale = theme.extension()!; - return SpinKitFoldingCube( - color: scale.tertiaryScale.background, - size: 80, - ); -} +Widget buildProgressIndicator() => Builder(builder: (context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + return SpinKitFoldingCube( + color: scale.tertiaryScale.background, + size: 80, + ); + }); -Widget waitingPage(BuildContext context) => ColoredBox( - color: Theme.of(context).scaffoldBackgroundColor, - child: Center(child: buildProgressIndicator(context))); +Widget waitingPage({String? text}) => Builder( + builder: (context) => ColoredBox( + color: Theme.of(context).scaffoldBackgroundColor, + child: Center( + child: Column(children: [ + buildProgressIndicator(), + if (text != null) Text(text) + ])))); -Widget errorPage(BuildContext context, Object err, StackTrace? st) => - ColoredBox( +Widget debugPage(String text) => Builder( + builder: (context) => ColoredBox( color: Theme.of(context).colorScheme.error, - child: Center(child: Text(err.toString()))); + child: Center(child: Text(text)))); + +Widget errorPage(Object err, StackTrace? st) => Builder( + builder: (context) => ColoredBox( + color: Theme.of(context).colorScheme.error, + child: Center(child: ErrorWidget(err)))); Widget asyncValueBuilder( AsyncValue av, Widget Function(BuildContext, T) builder) => av.when( - loading: () => const Builder(builder: waitingPage), - error: (e, st) => - Builder(builder: (context) => errorPage(context, e, st)), + loading: waitingPage, + error: errorPage, data: (d) => Builder(builder: (context) => builder(context, d))); extension AsyncValueBuilderExt on AsyncValue { Widget builder(Widget Function(BuildContext, T) builder) => asyncValueBuilder(this, builder); + Widget buildNotData( + {Widget Function()? loading, + Widget Function(Object, StackTrace?)? error}) => + when( + loading: () => (loading ?? waitingPage)(), + error: (e, st) => (error ?? errorPage)(e, st), + data: (d) => debugPage('AsyncValue should not be data here')); } class AsyncBlocBuilder>, S> extends BlocBuilder> { AsyncBlocBuilder({ required BlocWidgetBuilder builder, - Widget Function(BuildContext)? loading, - Widget Function(BuildContext, Object, StackTrace?)? error, + Widget Function()? loading, + Widget Function(Object, StackTrace?)? error, super.key, super.bloc, super.buildWhen, }) : super( builder: (context, state) => state.when( - loading: () => (loading ?? waitingPage)(context), - error: (e, st) => (error ?? errorPage)(context, e, st), + loading: () => (loading ?? waitingPage)(), + error: (e, st) => (error ?? errorPage)(e, st), data: (d) => builder(context, d))); } diff --git a/packages/async_tools/lib/src/single_async.dart b/packages/async_tools/lib/src/single_async.dart index aee9bc2..82334d7 100644 --- a/packages/async_tools/lib/src/single_async.dart +++ b/packages/async_tools/lib/src/single_async.dart @@ -4,8 +4,8 @@ import 'async_tag_lock.dart'; AsyncTagLock _keys = AsyncTagLock(); -void singleFuture(Object tag, Future Function() closure, - {void Function()? onBusy}) { +void singleFuture(Object tag, Future Function() closure, + {void Function()? onBusy, void Function(T)? onDone}) { if (!_keys.tryLock(tag)) { if (onBusy != null) { onBusy(); @@ -13,7 +13,13 @@ void singleFuture(Object tag, Future Function() closure, return; } unawaited(() async { - await closure(); - _keys.unlockTag(tag); + try { + final out = await closure(); + if (onDone != null) { + onDone(out); + } + } finally { + _keys.unlockTag(tag); + } }()); }