From 8ab7f38cb43a37d47b0a9d314081026a0e7c95e3 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Fri, 28 Mar 2025 16:14:40 -0400 Subject: [PATCH] =?UTF-8?q?chore:=20move=20chat=20input=20row=20into=20new?= =?UTF-8?q?=20file,=20determine=20contents=20based=20on=E2=80=A6=20(#2203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: move chat input row into new file, determine contents based on overlayController * fix: don't show toolbar buttons if message is not yet sent --- lib/pages/chat/chat.dart | 10 +- lib/pages/chat/chat_input_row.dart | 719 ++++++++---------- lib/pages/chat/events/message_content.dart | 3 +- lib/pangea/chat/widgets/chat_input_bar.dart | 4 +- .../chat/widgets/input_bar_wrapper.dart | 56 -- .../chat/widgets/pangea_chat_input_row.dart | 326 ++++++++ .../event_wrappers/pangea_message_event.dart | 6 +- .../overlay_footer.dart | 8 +- 8 files changed, 650 insertions(+), 482 deletions(-) delete mode 100644 lib/pangea/chat/widgets/input_bar_wrapper.dart create mode 100644 lib/pangea/chat/widgets/pangea_chat_input_row.dart diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 1a4c06016..bb3ec2209 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1215,7 +1215,10 @@ class ChatController extends State for (final event in selectedEvents) { await event.cancelSend(); } - setState(selectedEvents.clear); + // #Pangea + // setState(selectedEvents.clear); + clearSelectedEvents(); + // Pangea# } catch (e, s) { ErrorReporter( context, @@ -1350,6 +1353,9 @@ class ChatController extends State } // Pangea# final event = selectedEvents.first; + // #Pangea + clearSelectedEvents(); + // Pangea# if (event.status.isError) { event.sendAgain(); } @@ -1855,7 +1861,7 @@ class ChatController extends State Event? nextEvent, Event? prevEvent, }) { - if (event.redacted) return; + if (event.redacted || event.status == EventStatus.sending) return; // Close keyboard, if open if (inputFocus.hasFocus && PlatformInfos.isMobile) { diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 62bb9ecc3..82f80d394 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:animations/animations.dart'; @@ -6,30 +5,18 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; -import 'package:fluffychat/pangea/learning_settings/constants/language_constants.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/other_party_can_receive.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import '../../config/themes.dart'; import 'chat.dart'; import 'input_bar.dart'; class ChatInputRow extends StatelessWidget { final ChatController controller; - // #Pangea - final MessageOverlayController? overlayController; - // Pangea# - const ChatInputRow( - this.controller, { - // #Pangea - this.overlayController, - // Pangea# - super.key, - }); + const ChatInputRow(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -53,422 +40,320 @@ class ChatInputRow extends StatelessWidget { ); } - // #Pangea - 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( - // #Pangea - mainAxisSize: MainAxisSize.min, - // Pangea# - children: [ - // if (!controller.selectMode) WritingAssistanceInputRow(controller), - CompositedTransformTarget( - link: controller.choreographer.inputLayerLinkAndKey.link, - child: Row( - key: overlayController != null - ? null - : controller.choreographer.inputLayerLinkAndKey.key, - // crossAxisAlignment: CrossAxisAlignment.end, - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - // Pangea# - children: controller.selectMode - ? [ - if (controller.selectedEvents - .every((event) => event.status == EventStatus.error)) - SizedBox( - height: height, - child: TextButton( - style: TextButton.styleFrom( - foregroundColor: theme.colorScheme.error, + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: controller.selectMode + ? [ + 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), + ], + ), + ), + ) + else + SizedBox( + height: height, + child: TextButton( + onPressed: controller.forwardEventsAction, + child: Row( + children: [ + const Icon(Icons.keyboard_arrow_left_outlined), + Text(L10n.of(context).forward), + ], + ), + ), + ), + controller.selectedEvents.length == 1 + ? controller.selectedEvents.first + .getDisplayEvent(controller.timeline!) + .status + .isSent + ? SizedBox( + height: height, + child: TextButton( + onPressed: controller.replyAction, + child: Row( + children: [ + Text(L10n.of(context).reply), + const Icon(Icons.keyboard_arrow_right), + ], + ), ), - onPressed: controller.deleteErrorEventsAction, - child: Row( - children: [ - const Icon(Icons.delete), - Text(L10n.of(context).delete), - ], + ) + : 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), + ], + ), ), + ) + : const SizedBox.shrink(), + ] + : [ + 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), + ), + ), + 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) + 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), ), ), - // #Pangea - // else - // SizedBox( - // height: height, - // child: TextButton( - // onPressed: controller.forwardEventsAction, - // child: Row( - // children: [ - // const Icon(Icons.keyboard_arrow_left_outlined), - // Text(L10n.of(context).forward), - // ], - // ), - // ), - // ), - // controller.selectedEvents.length == 1 - // ? controller.selectedEvents.first - // .getDisplayEvent(controller.timeline!) - // .status - // .isSent - // ? SizedBox( - // height: height, - // child: TextButton( - // onPressed: controller.replyAction, - // child: Row( - // children: [ - // Text(L10n.of(context).reply), - // const Icon(Icons.keyboard_arrow_right), - // ], - // ), - // ), - // ) - // : 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), - // ], - // ), - // ), - // ) - // : const SizedBox.shrink(), - if (controller.selectedEvents.first - .getDisplayEvent(controller.timeline!) - .status - .isSent && - !controller.selectedEvents.every( - (event) => event.status == EventStatus.error, - )) - overlayController != null - ? ReadingAssistanceInputBar( - controller, - overlayController!, - ) - : const SizedBox(height: height), - if (controller.selectedEvents.length == 1 && - !controller.selectedEvents.first - .getDisplayEvent(controller.timeline!) - .status - .isSent) - 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 (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), ), ), - // Pangea# - ] - : [ - 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 (PlatformInfos.isMobile) + PopupMenuItem( + value: 'location', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.brown, + foregroundColor: Colors.white, + child: Icon(Icons.gps_fixed_outlined), ), - 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) - 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), - ), - ), - 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), - ), - ), - ], - ), - ), - // #Pangea - kIsWeb - ? - // Pangea# - 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, - ), - ) - // #Pangea - : const SizedBox.shrink(), - // if (Matrix.of(context).isMultiAccount && - // Matrix.of(context).hasComplexBundles && - // Matrix.of(context).currentBundle!.length > 1) - // Container( - // width: height, - // height: height, - // alignment: Alignment.center, - // child: _ChatAccountPicker(controller), - // ), - // Pangea# - 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, - // #Pangea - // textInputAction: AppConfig.sendOnEnter == true && - textInputAction: AppConfig.sendOnEnter ?? - true && - // Pangea# - PlatformInfos.isMobile - ? TextInputAction.send - : null, - // #Pangea - // onSubmitted: controller.onInputBarSubmitted, - onSubmitted: (String value) => - controller.onInputBarSubmitted(value, context), - // Pangea# - 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, - ), - // #Pangea - // hintText: L10n.of(context)!.writeAMessage, - hintText: hintText(), - disabledBorder: InputBorder.none, - // Pangea# - hintMaxLines: 1, - border: InputBorder.none, - enabledBorder: InputBorder.none, - filled: false, - ), - onChanged: controller.onInputBarChanged, + title: Text(L10n.of(context).shareLocation), + contentPadding: const EdgeInsets.all(0), ), ), - ), - // #Pangea - StartIGCButton( - controller: controller, - ), - // Pangea# - Container( - height: height, - width: height, - alignment: Alignment.center, - child: PlatformInfos.platformCanRecord && - controller.sendController.text.isEmpty - // #Pangea - && - !controller.choreographer.itController.willOpen - // Pangea# - ? 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), - ) - // #Pangea - // : FloatingActionButton.small( - // tooltip: L10n.of(context).send, - // onPressed: controller.send, - // elevation: 0, - // heroTag: null, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(height), - // ), - // backgroundColor: - // theme.colorScheme.onPrimaryContainer, - // foregroundColor: theme.colorScheme.onPrimary, - // child: const Icon(Icons.send_outlined), - // ), - : ChoreographerSendButton(controller: controller), - // Pangea# - ), ], - ), - ), - ], + ), + ), + 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, + ), + ), + if (Matrix.of(context).isMultiAccount && + Matrix.of(context).hasComplexBundles && + Matrix.of(context).currentBundle!.length > 1) + Container( + width: height, + height: height, + alignment: Alignment.center, + child: _ChatAccountPicker(controller), + ), + 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, + // #Pangea + // onSubmitted: controller.onInputBarSubmitted, + onSubmitted: (value) => + controller.onInputBarSubmitted(value, context), + // Pangea# + 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: L10n.of(context).writeAMessage, + hintMaxLines: 1, + border: InputBorder.none, + enabledBorder: InputBorder.none, + filled: false, + ), + onChanged: controller.onInputBarChanged, + ), + ), + ), + Container( + height: height, + width: height, + alignment: Alignment.center, + child: PlatformInfos.platformCanRecord && + controller.sendController.text.isEmpty + ? 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), + ) + : FloatingActionButton.small( + tooltip: L10n.of(context).send, + onPressed: controller.send, + elevation: 0, + heroTag: null, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(height), + ), + backgroundColor: theme.bubbleColor, + foregroundColor: theme.onBubbleColor, + child: const Icon(Icons.send_outlined), + ), + ), + ], ); } } -// #Pangea -// class _ChatAccountPicker extends StatelessWidget { -// final ChatController controller; +class _ChatAccountPicker extends StatelessWidget { + final ChatController controller; -// const _ChatAccountPicker(this.controller); + const _ChatAccountPicker(this.controller); -// void _popupMenuButtonSelected(String mxid, BuildContext context) { -// final client = Matrix.of(context) -// .currentBundle! -// .firstWhere((cl) => cl!.userID == mxid, orElse: () => null); -// if (client == null) { -// Logs().w('Attempted to switch to a non-existing client $mxid'); -// return; -// } -// controller.setSendingClient(client); -// } + void _popupMenuButtonSelected(String mxid, BuildContext context) { + final client = Matrix.of(context) + .currentBundle! + .firstWhere((cl) => cl!.userID == mxid, orElse: () => null); + if (client == null) { + Logs().w('Attempted to switch to a non-existing client $mxid'); + return; + } + controller.setSendingClient(client); + } -// @override -// Widget build(BuildContext context) { -// final clients = controller.currentRoomBundle; -// return Padding( -// padding: const EdgeInsets.all(8.0), -// child: FutureBuilder( -// future: controller.sendingClient.fetchOwnProfile(), -// builder: (context, snapshot) => PopupMenuButton( -// onSelected: (mxid) => _popupMenuButtonSelected(mxid, context), -// itemBuilder: (BuildContext context) => clients -// .map( -// (client) => PopupMenuItem( -// value: client!.userID, -// child: FutureBuilder( -// future: client.fetchOwnProfile(), -// builder: (context, snapshot) => ListTile( -// leading: Avatar( -// mxContent: snapshot.data?.avatarUrl, -// name: snapshot.data?.displayName ?? -// client.userID!.localpart, -// size: 20, -// ), -// title: Text(snapshot.data?.displayName ?? client.userID!), -// contentPadding: const EdgeInsets.all(0), -// ), -// ), -// ), -// ) -// .toList(), -// child: Avatar( -// mxContent: snapshot.data?.avatarUrl, -// name: snapshot.data?.displayName ?? -// Matrix.of(context).client.userID!.localpart, -// size: 20, -// ), -// ), -// ), -// ); -// } -// } -// Pangea# + @override + Widget build(BuildContext context) { + final clients = controller.currentRoomBundle; + return Padding( + padding: const EdgeInsets.all(8.0), + child: FutureBuilder( + future: controller.sendingClient.fetchOwnProfile(), + builder: (context, snapshot) => PopupMenuButton( + onSelected: (mxid) => _popupMenuButtonSelected(mxid, context), + itemBuilder: (BuildContext context) => clients + .map( + (client) => PopupMenuItem( + value: client!.userID, + child: FutureBuilder( + future: client.fetchOwnProfile(), + builder: (context, snapshot) => ListTile( + leading: Avatar( + mxContent: snapshot.data?.avatarUrl, + name: snapshot.data?.displayName ?? + client.userID!.localpart, + size: 20, + ), + title: Text(snapshot.data?.displayName ?? client.userID!), + contentPadding: const EdgeInsets.all(0), + ), + ), + ), + ) + .toList(), + child: Avatar( + mxContent: snapshot.data?.avatarUrl, + name: snapshot.data?.displayName ?? + Matrix.of(context).client.userID!.localpart, + size: 20, + ), + ), + ), + ); + } +} diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 19cc32e74..b07c501b5 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -372,7 +372,8 @@ class MessageContent extends StatelessWidget { ); } - if (pangeaMessageEvent != null) { + if (pangeaMessageEvent != null && + pangeaMessageEvent!.shouldShowToolbar) { return MessageTokenText( pangeaMessageEvent: pangeaMessageEvent!, tokens: diff --git a/lib/pangea/chat/widgets/chat_input_bar.dart b/lib/pangea/chat/widgets/chat_input_bar.dart index 968b7f53b..ae13b985c 100644 --- a/lib/pangea/chat/widgets/chat_input_bar.dart +++ b/lib/pangea/chat/widgets/chat_input_bar.dart @@ -4,7 +4,7 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_emoji_picker.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; -import 'package:fluffychat/pangea/chat/widgets/input_bar_wrapper.dart'; +import 'package:fluffychat/pangea/chat/widgets/pangea_chat_input_row.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; class ChatInputBar extends StatefulWidget { @@ -63,7 +63,7 @@ class ChatInputBarState extends State { child: Column( children: [ ReplyDisplay(widget.controller), - ChatInputRowWrapper( + PangeaChatInputRow( controller: widget.controller, ), ChatEmojiPicker(widget.controller), diff --git a/lib/pangea/chat/widgets/input_bar_wrapper.dart b/lib/pangea/chat/widgets/input_bar_wrapper.dart deleted file mode 100644 index 85835e630..000000000 --- a/lib/pangea/chat/widgets/input_bar_wrapper.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pages/chat/chat_input_row.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_text_controller.dart'; - -class ChatInputRowWrapper extends StatefulWidget { - final ChatController controller; - - const ChatInputRowWrapper({ - required this.controller, - super.key, - }); - - @override - State createState() => ChatInputRowWrapperState(); -} - -class ChatInputRowWrapperState extends State { - StreamSubscription? _choreoSub; - String _currentText = ''; - - @override - void initState() { - // Rebuild the widget each time there's an update from choreo - _choreoSub = widget.controller.choreographer.stateStream.stream.listen((_) { - setState(() {}); - }); - super.initState(); - } - - @override - void dispose() { - _choreoSub?.cancel(); - super.dispose(); - } - - void refreshOnChange(String text) { - final bool decreasedFromMaxLength = - _currentText.length >= PangeaTextController.maxLength && - text.length < PangeaTextController.maxLength; - final bool reachedMaxLength = - _currentText.length < PangeaTextController.maxLength && - text.length < PangeaTextController.maxLength; - - if (decreasedFromMaxLength || reachedMaxLength) { - setState(() {}); - } - _currentText = text; - } - - @override - Widget build(BuildContext context) => ChatInputRow(widget.controller); -} diff --git a/lib/pangea/chat/widgets/pangea_chat_input_row.dart b/lib/pangea/chat/widgets/pangea_chat_input_row.dart new file mode 100644 index 000000000..29587af6d --- /dev/null +++ b/lib/pangea/chat/widgets/pangea_chat_input_row.dart @@ -0,0 +1,326 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:animations/animations.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +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/learning_settings/constants/language_constants.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/platform_infos.dart'; + +class PangeaChatInputRow extends StatefulWidget { + final ChatController controller; + final MessageOverlayController? overlayController; + + const PangeaChatInputRow({ + required this.controller, + this.overlayController, + super.key, + }); + + @override + State createState() => PangeaChatInputRowState(); +} + +class PangeaChatInputRowState extends State { + StreamSubscription? _choreoSub; + + @override + void initState() { + // Rebuild the widget each time there's an update from choreo + _choreoSub = widget.controller.choreographer.stateStream.stream.listen((_) { + setState(() {}); + }); + super.initState(); + } + + @override + void dispose() { + _choreoSub?.cancel(); + super.dispose(); + } + + ChatController get _controller => widget.controller; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + if (_controller.showEmojiPicker && + _controller.emojiPickerType == EmojiPickerType.reaction) { + return const SizedBox.shrink(); + } + 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), + ], + ), + ), + ), + 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), + ], + ), + ), + ), + ] + : [ + 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), + ), + ), + 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) + 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), + ), + ), + 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), + ), + ), + 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, + ), + hintText: hintText(), + disabledBorder: InputBorder.none, + hintMaxLines: 1, + border: InputBorder.none, + enabledBorder: InputBorder.none, + filled: false, + ), + 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), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pangea/events/event_wrappers/pangea_message_event.dart b/lib/pangea/events/event_wrappers/pangea_message_event.dart index 7d1ac3f35..1864ab76a 100644 --- a/lib/pangea/events/event_wrappers/pangea_message_event.dart +++ b/lib/pangea/events/event_wrappers/pangea_message_event.dart @@ -744,7 +744,11 @@ class PangeaMessageEvent { List get practiceActivities => l2Code == null ? [] : practiceActivitiesByLangCode(l2Code!); - bool get shouldShowToolbar => !event.isActivityMessage && !event.redacted; + bool get shouldShowToolbar => + !event.isActivityMessage && + !event.redacted && + event.status != EventStatus.sending && + event.status != EventStatus.error; bool shouldDoActivity({ required PangeaToken? token, diff --git a/lib/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart b/lib/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart index 0a1f35866..2dc37a8e8 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pages/chat/chat_input_row.dart'; +import 'package:fluffychat/pangea/chat/widgets/pangea_chat_input_row.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/toolbar_button_column.dart'; @@ -44,8 +44,10 @@ class OverlayFooter extends StatelessWidget { borderRadius: const BorderRadius.all( Radius.circular(AppConfig.borderRadius), ), - child: - ChatInputRow(controller, overlayController: overlayController), + child: PangeaChatInputRow( + controller: controller, + overlayController: overlayController, + ), ), ], ),