From 642dfaf4deeb2b6de17406e0ae7a47b13525a05b Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:35:11 -0400 Subject: [PATCH] chore: decouple selected message list from chat input row rendering (#2300) --- .../chat/widgets/pangea_chat_input_row.dart | 499 ++++++++++-------- .../reading_assistance_input_bar.dart | 10 +- 2 files changed, 279 insertions(+), 230 deletions(-) diff --git a/lib/pangea/chat/widgets/pangea_chat_input_row.dart b/lib/pangea/chat/widgets/pangea_chat_input_row.dart index 29587af6d..6a095ed13 100644 --- a/lib/pangea/chat/widgets/pangea_chat_input_row.dart +++ b/lib/pangea/chat/widgets/pangea_chat_input_row.dart @@ -13,9 +13,12 @@ import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/input_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; +import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/platform_infos.dart'; class PangeaChatInputRow extends StatefulWidget { @@ -52,6 +55,73 @@ class PangeaChatInputRowState extends State { ChatController get _controller => widget.controller; + LanguageModel? get activel1 => + _controller.pangeaController.languageController.activeL1Model(); + LanguageModel? get activel2 => + _controller.pangeaController.languageController.activeL2Model(); + + String hintText() { + if (_controller.choreographer.itController.willOpen) { + return L10n.of(context).buildTranslation; + } + return activel1 != null && + activel2 != null && + activel1!.langCode != LanguageKeys.unknownLanguage && + activel2!.langCode != LanguageKeys.unknownLanguage + ? L10n.of(context).writeAMessageLangCodes( + activel1!.langCodeShort.toUpperCase(), + activel2!.langCodeShort.toUpperCase(), + ) + : L10n.of(context).writeAMessage; + } + + void _deleteErrorEventsAction() async { + try { + if (widget.overlayController == null || + widget.overlayController!.event.status != EventStatus.error) { + throw Exception( + 'Tried to delete failed to send events but one event is not failed to sent', + ); + } + await widget.overlayController!.event.cancelSend(); + _controller.clearSelectedEvents(); + } catch (e, s) { + ErrorReporter( + context, + 'Error while delete error events action', + ).onErrorCallback(e, s); + } + } + + void _sendAgainAction() { + if (widget.overlayController == null) { + ErrorHandler.logError( + e: "No selected events in send again action", + s: StackTrace.current, + data: {"roomId": _controller.room.id}, + ); + _controller.clearSelectedEvents(); + return; + } + + final event = widget.overlayController!.event; + if (event.status.isError) { + event.sendAgain(); + } + + final allEditEvents = event + .aggregatedEvents( + _controller.timeline!, + RelationshipTypes.edit, + ) + .where((e) => e.status.isError); + for (final e in allEditEvents) { + e.sendAgain(); + } + + _controller.clearSelectedEvents(); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -61,263 +131,250 @@ class PangeaChatInputRowState extends State { } const height = 48.0; - final activel1 = - _controller.pangeaController.languageController.activeL1Model(); - final activel2 = - _controller.pangeaController.languageController.activeL2Model(); - - String hintText() { - if (_controller.choreographer.itController.willOpen) { - return L10n.of(context).buildTranslation; - } - return activel1 != null && - activel2 != null && - activel1.langCode != LanguageKeys.unknownLanguage && - activel2.langCode != LanguageKeys.unknownLanguage - ? L10n.of(context).writeAMessageLangCodes( - activel1.langCodeShort.toUpperCase(), - activel2.langCodeShort.toUpperCase(), - ) - : L10n.of(context).writeAMessage; - } - return Column( children: [ // if (!controller.selectMode) WritingAssistanceInputRow(controller), CompositedTransformTarget( link: _controller.choreographer.inputLayerLinkAndKey.link, - child: Row( - key: widget.overlayController != null - ? null - : _controller.choreographer.inputLayerLinkAndKey.key, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: widget.overlayController != null - ? [ - if (_controller.selectedEvents - .every((event) => event.status == EventStatus.error)) - SizedBox( - height: height, - child: TextButton( - style: TextButton.styleFrom( - foregroundColor: theme.colorScheme.error, - ), - onPressed: _controller.deleteErrorEventsAction, - child: Row( - children: [ - const Icon(Icons.delete), - Text(L10n.of(context).delete), - ], + child: Container( + padding: + EdgeInsets.all(widget.overlayController != null ? 8.0 : 0.0), + decoration: BoxDecoration( + color: widget.overlayController != null + ? Theme.of(context).cardColor + : null, + borderRadius: const BorderRadius.all( + Radius.circular(8.0), + ), + ), + child: Row( + key: widget.overlayController != null + ? null + : _controller.choreographer.inputLayerLinkAndKey.key, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: widget.overlayController != null + ? [ + if (widget.overlayController!.event.status == + EventStatus.error) + SizedBox( + height: height, + child: TextButton( + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.error, + ), + onPressed: _deleteErrorEventsAction, + child: Row( + children: [ + const Icon(Icons.delete), + Text(L10n.of(context).delete), + ], + ), ), ), - ), - if (_controller.selectedEvents.isNotEmpty && - _controller.selectedEvents.first - .getDisplayEvent(_controller.timeline!) - .status - .isSent && - !_controller.selectedEvents.every( - (event) => event.status == EventStatus.error, - )) - widget.overlayController != null - ? ReadingAssistanceInputBar( - _controller, - widget.overlayController!, - ) - : const SizedBox(height: height), - if (_controller.selectedEvents.length == 1 && - _controller.selectedEvents.first - .getDisplayEvent(_controller.timeline!) - .status - .isError) - SizedBox( - height: height, - child: TextButton( - onPressed: _controller.sendAgainAction, - child: Row( - children: [ - Text(L10n.of(context).tryToSendAgain), - const SizedBox(width: 4), - const Icon(Icons.send_outlined, size: 16), - ], - ), + if (widget.overlayController!.event + .getDisplayEvent(_controller.timeline!) + .status + .isSent) + ReadingAssistanceInputBar( + _controller, + widget.overlayController!, ), - ), - ] - : [ - const SizedBox(width: 4), - AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - height: height, - width: - _controller.sendController.text.isEmpty ? height : 0, - alignment: Alignment.center, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - child: PopupMenuButton( - icon: const Icon(Icons.add_outlined), - onSelected: _controller.onAddPopupMenuButtonSelected, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: 'file', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - child: Icon(Icons.attachment_outlined), - ), - title: Text(L10n.of(context).sendFile), - contentPadding: const EdgeInsets.all(0), + if (widget.overlayController!.event + .getDisplayEvent(_controller.timeline!) + .status + .isError) + SizedBox( + height: height, + child: TextButton( + onPressed: _sendAgainAction, + child: Row( + children: [ + Text(L10n.of(context).tryToSendAgain), + const SizedBox(width: 4), + const Icon(Icons.send_outlined, size: 16), + ], ), ), - PopupMenuItem( - value: 'image', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - child: Icon(Icons.image_outlined), - ), - title: Text(L10n.of(context).sendImage), - contentPadding: const EdgeInsets.all(0), - ), - ), - if (PlatformInfos.isMobile) + ), + ] + : [ + const SizedBox(width: 4), + AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + height: height, + width: _controller.sendController.text.isEmpty + ? height + : 0, + alignment: Alignment.center, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: PopupMenuButton( + icon: const Icon(Icons.add_outlined), + onSelected: _controller.onAddPopupMenuButtonSelected, + itemBuilder: (BuildContext context) => + >[ PopupMenuItem( - value: 'camera', + value: 'file', child: ListTile( leading: const CircleAvatar( - backgroundColor: Colors.purple, + backgroundColor: Colors.green, foregroundColor: Colors.white, - child: Icon(Icons.camera_alt_outlined), + child: Icon(Icons.attachment_outlined), ), - title: Text(L10n.of(context).openCamera), + title: Text(L10n.of(context).sendFile), contentPadding: const EdgeInsets.all(0), ), ), - if (PlatformInfos.isMobile) PopupMenuItem( - value: 'camera-video', + value: 'image', child: ListTile( leading: const CircleAvatar( - backgroundColor: Colors.red, + backgroundColor: Colors.blue, foregroundColor: Colors.white, - child: Icon(Icons.videocam_outlined), + child: Icon(Icons.image_outlined), ), - title: Text(L10n.of(context).openVideoCamera), + title: Text(L10n.of(context).sendImage), contentPadding: const EdgeInsets.all(0), ), ), - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'location', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.brown, - foregroundColor: Colors.white, - child: Icon(Icons.gps_fixed_outlined), + if (PlatformInfos.isMobile) + PopupMenuItem( + value: 'camera', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.purple, + foregroundColor: Colors.white, + child: Icon(Icons.camera_alt_outlined), + ), + title: Text(L10n.of(context).openCamera), + contentPadding: const EdgeInsets.all(0), ), - title: Text(L10n.of(context).shareLocation), - contentPadding: const EdgeInsets.all(0), ), - ), - ], + if (PlatformInfos.isMobile) + PopupMenuItem( + value: 'camera-video', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + child: Icon(Icons.videocam_outlined), + ), + title: Text(L10n.of(context).openVideoCamera), + contentPadding: const EdgeInsets.all(0), + ), + ), + if (PlatformInfos.isMobile) + PopupMenuItem( + value: 'location', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.brown, + foregroundColor: Colors.white, + child: Icon(Icons.gps_fixed_outlined), + ), + title: Text(L10n.of(context).shareLocation), + contentPadding: const EdgeInsets.all(0), + ), + ), + ], + ), ), - ), - if (kIsWeb) - Container( - height: height, - width: height, - alignment: Alignment.center, - child: IconButton( - tooltip: L10n.of(context).emojis, - icon: PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.scaled, - fillColor: Colors.transparent, - child: child, - ); - }, - child: Icon( - _controller.showEmojiPicker - ? Icons.keyboard - : Icons.add_reaction_outlined, - key: ValueKey(_controller.showEmojiPicker), + if (kIsWeb) + Container( + height: height, + width: height, + alignment: Alignment.center, + child: IconButton( + tooltip: L10n.of(context).emojis, + icon: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: + SharedAxisTransitionType.scaled, + fillColor: Colors.transparent, + child: child, + ); + }, + child: Icon( + _controller.showEmojiPicker + ? Icons.keyboard + : Icons.add_reaction_outlined, + key: ValueKey(_controller.showEmojiPicker), + ), ), + onPressed: _controller.emojiPickerAction, ), - onPressed: _controller.emojiPickerAction, ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 0.0), - child: InputBar( - room: _controller.room, - minLines: 1, - maxLines: 8, - autofocus: !PlatformInfos.isMobile, - keyboardType: TextInputType.multiline, - textInputAction: AppConfig.sendOnEnter ?? - true && PlatformInfos.isMobile - ? TextInputAction.send - : null, - onSubmitted: (String value) => - _controller.onInputBarSubmitted(value, context), - onSubmitImage: _controller.sendImageFromClipBoard, - focusNode: _controller.inputFocus, - controller: _controller.sendController, - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - left: 6.0, - right: 6.0, - bottom: 6.0, - top: 3.0, + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 0.0), + child: InputBar( + room: _controller.room, + minLines: 1, + maxLines: 8, + autofocus: !PlatformInfos.isMobile, + keyboardType: TextInputType.multiline, + textInputAction: AppConfig.sendOnEnter ?? + true && PlatformInfos.isMobile + ? TextInputAction.send + : null, + onSubmitted: (String value) => + _controller.onInputBarSubmitted(value, context), + onSubmitImage: _controller.sendImageFromClipBoard, + focusNode: _controller.inputFocus, + controller: _controller.sendController, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + left: 6.0, + right: 6.0, + bottom: 6.0, + top: 3.0, + ), + hintText: hintText(), + disabledBorder: InputBorder.none, + hintMaxLines: 1, + border: InputBorder.none, + enabledBorder: InputBorder.none, + filled: false, ), - hintText: hintText(), - disabledBorder: InputBorder.none, - hintMaxLines: 1, - border: InputBorder.none, - enabledBorder: InputBorder.none, - filled: false, + onChanged: _controller.onInputBarChanged, ), - onChanged: _controller.onInputBarChanged, ), ), - ), - StartIGCButton( - controller: _controller, - ), - Container( - height: height, - width: height, - alignment: Alignment.center, - child: PlatformInfos.platformCanRecord && - _controller.sendController.text.isEmpty && - !_controller.choreographer.itController.willOpen - ? FloatingActionButton.small( - tooltip: L10n.of(context).voiceMessage, - onPressed: _controller.voiceMessageAction, - elevation: 0, - heroTag: null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(height), - ), - backgroundColor: theme.bubbleColor, - foregroundColor: theme.onBubbleColor, - child: const Icon(Icons.mic_none_outlined), - ) - : ChoreographerSendButton(controller: _controller), - ), - ], + StartIGCButton( + controller: _controller, + ), + Container( + height: height, + width: height, + alignment: Alignment.center, + child: PlatformInfos.platformCanRecord && + _controller.sendController.text.isEmpty && + !_controller.choreographer.itController.willOpen + ? FloatingActionButton.small( + tooltip: L10n.of(context).voiceMessage, + onPressed: _controller.voiceMessageAction, + elevation: 0, + heroTag: null, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(height), + ), + backgroundColor: theme.bubbleColor, + foregroundColor: theme.onBubbleColor, + child: const Icon(Icons.mic_none_outlined), + ) + : ChoreographerSendButton(controller: _controller), + ), + ], + ), ), ), ], diff --git a/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart b/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart index 4b6c24b98..9415eded5 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart @@ -112,19 +112,11 @@ class ReadingAssistanceInputBar extends StatelessWidget { @override Widget build(BuildContext context) { return Expanded( - child: Container( - width: overlayController.maxWidth, + child: ConstrainedBox( constraints: BoxConstraints( maxHeight: (MediaQuery.of(context).size.height / 2) - AppConfig.toolbarButtonsHeight, ), - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: const BorderRadius.all( - Radius.circular(8.0), - ), - ), child: AnimatedSize( duration: const Duration( milliseconds: AppConfig.overlayAnimationDuration,