From 29b262a1c66046d6a0def15e2786deeede6ff919 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 8 Aug 2024 09:49:08 +0800 Subject: [PATCH] chore: optimize mobile ai chat page (#5897) * feat: improve chat page UI on mobile * feat: integrate add page menu into chat page on mobile * fix: only display document view in @ menu --- frontend/appflowy_flutter/ios/Podfile.lock | 4 +- .../application/chat_input_action_bloc.dart | 8 +- .../lib/plugins/ai_chat/chat_page.dart | 24 ++- .../presentation/chat_input/chat_input.dart | 169 +++++++++++++----- .../mention/mobile_page_selector_sheet.dart | 25 ++- .../add_block_toolbar_item.dart | 9 +- .../document/presentation/editor_style.dart | 3 +- 7 files changed, 168 insertions(+), 74 deletions(-) diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 72df3b0bd7..c54ae23ed6 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -175,7 +175,7 @@ SPEC CHECKSUMS: file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: 723e187574b149e68e63ca4d39b837586b903cfa + fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 @@ -197,4 +197,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca -COCOAPODS: 1.11.3 +COCOAPODS: 1.15.2 diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_bloc.dart index 3a23106e8f..5787a06ef4 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_bloc.dart @@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'chat_input_action_control.dart'; + part 'chat_input_action_bloc.freezed.dart'; class ChatInputActionBloc @@ -34,7 +35,12 @@ class ChatInputActionBloc final views = result .toNullable() ?.items - .where((v) => v.layout.isDocumentView) + .where( + (v) => + v.layout.isDocumentView && + !v.isSpace && + v.parentViewId.isNotEmpty, + ) .toList() ?? []; if (!isClosed) { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index bd9c46ceae..d33b6ac8cb 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -1,30 +1,28 @@ import 'dart:math'; -import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart'; -import 'package:appflowy/plugins/ai_chat/presentation/other_user_message_bubble.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:desktop_drop/desktop_drop.dart'; -import 'package:flowy_infra/platform_extension.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart'; import 'package:appflowy/plugins/ai_chat/presentation/ai_message_bubble.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/other_user_message_bubble.dart'; import 'package:appflowy/plugins/ai_chat/presentation/user_message_bubble.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:desktop_drop/desktop_drop.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/platform_extension.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; +import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat; import 'package:styled_widget/styled_widget.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input.dart index f342a94299..92b911c406 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input.dart @@ -1,6 +1,9 @@ +import 'package:appflowy/plugins/ai_chat/application/chat_input_action_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_input_action_control.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_input_action_menu.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_input_action_control.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:extended_text_field/extended_text_field.dart'; import 'package:flowy_infra/platform_extension.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -12,8 +15,8 @@ import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'chat_at_button.dart'; -import 'chat_send_button.dart'; import 'chat_input_span.dart'; +import 'chat_send_button.dart'; class ChatInput extends StatefulWidget { /// Creates [ChatInput] widget. @@ -96,19 +99,26 @@ class _ChatInputState extends State { @override Widget build(BuildContext context) { - const textPadding = EdgeInsets.symmetric(horizontal: 16); const buttonPadding = EdgeInsets.symmetric(horizontal: 2); const inputPadding = EdgeInsets.all(6); + final textPadding = isMobile + ? const EdgeInsets.only(left: 8.0, right: 4.0) + : const EdgeInsets.symmetric(horizontal: 16); + final borderRadius = BorderRadius.circular(isMobile ? 10 : 30); + final color = isMobile + ? Colors.transparent + : Theme.of(context).colorScheme.surfaceContainerHighest; + final elevation = isMobile ? 0.0 : 0.6; + final space = isMobile ? 8.0 : 14.0; + return Focus( child: Padding( padding: inputPadding, child: Material( - borderRadius: BorderRadius.circular(30), - color: isMobile - ? Theme.of(context).colorScheme.surfaceContainer - : Theme.of(context).colorScheme.surfaceContainerHighest, - elevation: 0.6, + borderRadius: borderRadius, + color: color, + elevation: elevation, child: Row( children: [ if (widget.onAttachmentPressed != null) @@ -117,14 +127,13 @@ class _ChatInputState extends State { onPressed: widget.onAttachmentPressed, padding: buttonPadding, ), - Expanded(child: _inputTextField(textPadding)), - - // TODO(lucas): support mobile - if (PlatformExtension.isDesktop && - widget.aiType == const AIType.appflowyAI()) + Expanded( + child: _inputTextField(context, textPadding), + ), + if (widget.aiType == const AIType.appflowyAI()) _atButton(buttonPadding), _sendButton(buttonPadding), - const HSpace(14), + HSpace(space), ], ), ), @@ -160,33 +169,24 @@ class _ChatInputState extends State { }); } - Widget _inputTextField(EdgeInsets textPadding) { + Widget _inputTextField(BuildContext context, EdgeInsets textPadding) { return CompositedTransformTarget( link: _layerLink, child: Padding( padding: textPadding, child: ExtendedTextField( key: _textFieldKey, - specialTextSpanBuilder: - ChatInputTextSpanBuilder(inputActionControl: _inputActionControl), controller: _textController, focusNode: _inputFocusNode, - decoration: InputDecoration( - border: InputBorder.none, - hintText: widget.hintText, - focusedBorder: InputBorder.none, - hintStyle: TextStyle( - color: AFThemeExtension.of(context).textColor.withOpacity(0.5), - ), - ), - style: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontSize: 15, - ), + decoration: _buildInputDecoration(context), keyboardType: TextInputType.multiline, textCapitalization: TextCapitalization.sentences, minLines: 1, maxLines: 10, + style: _buildTextStyle(context), + specialTextSpanBuilder: ChatInputTextSpanBuilder( + inputActionControl: _inputActionControl, + ), onChanged: (text) { _handleOnTextChange(context, text); }, @@ -195,27 +195,87 @@ class _ChatInputState extends State { ); } - void _handleOnTextChange(BuildContext context, String text) { + InputDecoration _buildInputDecoration(BuildContext context) { + if (!isMobile) { + return InputDecoration( + border: InputBorder.none, + hintText: widget.hintText, + focusedBorder: InputBorder.none, + hintStyle: TextStyle( + color: AFThemeExtension.of(context).textColor.withOpacity(0.5), + ), + ); + } + + final borderRadius = BorderRadius.circular(10); + return InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + hintText: widget.hintText, + hintStyle: TextStyle( + color: AFThemeExtension.of(context).textColor.withOpacity(0.5), + ), + enabledBorder: OutlineInputBorder( + borderRadius: borderRadius, + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: borderRadius, + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 1.2, + ), + ), + ); + } + + TextStyle? _buildTextStyle(BuildContext context) { + if (!isMobile) { + return TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 15, + ); + } + + return Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 15, + height: 1.2, + ); + } + + Future _handleOnTextChange(BuildContext context, String text) async { if (widget.aiType != const AIType.appflowyAI()) { return; } + if (!_inputActionControl.onTextChanged(text)) { + return; + } + if (PlatformExtension.isDesktop) { - if (_inputActionControl.onTextChanged(text)) { - ChatActionsMenu( - anchor: ChatInputAnchor( - anchorKey: _textFieldKey, - layerLink: _layerLink, - ), - handler: _inputActionControl, - context: context, - style: Theme.of(context).brightness == Brightness.dark - ? const ChatActionsMenuStyle.dark() - : const ChatActionsMenuStyle.light(), - ).show(); - } + ChatActionsMenu( + anchor: ChatInputAnchor( + anchorKey: _textFieldKey, + layerLink: _layerLink, + ), + handler: _inputActionControl, + context: context, + style: Theme.of(context).brightness == Brightness.dark + ? const ChatActionsMenuStyle.dark() + : const ChatActionsMenuStyle.light(), + ).show(); } else { - // TODO(lucas): support mobile + // if the focus node is on focus, unfocus it for better animation + // otherwise, the page sheet animation will be blocked by the keyboard + if (_inputFocusNode.hasFocus) { + _inputFocusNode.unfocus(); + Future.delayed(const Duration(milliseconds: 100), () async { + await _referPage(_inputActionControl); + }); + } else { + await _referPage(_inputActionControl); + } } } @@ -246,13 +306,32 @@ class _ChatInputState extends State { child: ChatInputAtButton( onTap: () { _textController.text += '@'; - _inputFocusNode.requestFocus(); + if (!isMobile) { + _inputFocusNode.requestFocus(); + } _handleOnTextChange(context, _textController.text); }, ), ); } + Future _referPage(ChatActionHandler handler) async { + handler.onEnter(); + final selectedView = await showPageSelectorSheet( + context, + filter: (view) => + view.layout.isDocumentView && + !view.isSpace && + view.parentViewId.isNotEmpty, + ); + if (selectedView == null) { + handler.onExit(); + return; + } + handler.onSelected(ViewActionPage(view: selectedView)); + handler.onExit(); + } + @override void didUpdateWidget(covariant ChatInput oldWidget) { super.didUpdateWidget(oldWidget); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart index d15d24aab7..ae493d402a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart'; @@ -11,13 +9,17 @@ import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; -Future showPageSelectorSheet( +Future showPageSelectorSheet( BuildContext context, { String? currentViewId, String? selectedViewId, + bool Function(ViewPB view)? filter, }) async { - return showMobileBottomSheet( + filter ??= (v) => !v.isSpace && v.parentViewId.isNotEmpty; + + return showMobileBottomSheet( context, title: LocaleKeys.document_mobilePageSelector_title.tr(), showHeader: true, @@ -32,16 +34,22 @@ Future showPageSelectorSheet( child: _MobilePageSelectorBody( currentViewId: currentViewId, selectedViewId: selectedViewId, + filter: filter, ), ), ); } class _MobilePageSelectorBody extends StatefulWidget { - const _MobilePageSelectorBody({this.currentViewId, this.selectedViewId}); + const _MobilePageSelectorBody({ + this.currentViewId, + this.selectedViewId, + this.filter, + }); final String? currentViewId; final String? selectedViewId; + final bool Function(ViewPB view)? filter; @override State<_MobilePageSelectorBody> createState() => @@ -79,7 +87,10 @@ class _MobilePageSelectorBodyState extends State<_MobilePageSelectorBody> { ); } - final views = snapshot.data!; + final views = snapshot.data! + .where((v) => widget.filter?.call(v) ?? true) + .toList(); + if (widget.currentViewId != null) { views.removeWhere((v) => v.id == widget.currentViewId); } @@ -118,7 +129,7 @@ class _MobilePageSelectorBodyState extends State<_MobilePageSelectorBody> { ), text: view.name, isSelected: view.id == widget.selectedViewId, - onTap: () => Navigator.of(context).pop(view.id), + onTap: () => Navigator.of(context).pop(view), ), ) .toList(), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart index 6974d40a24..d09e2be349 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart'; @@ -21,6 +19,7 @@ import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; final addBlockToolbarItem = AppFlowyMobileToolbarItem( @@ -230,16 +229,16 @@ class _AddBlockMenu extends StatelessWidget { AppGlobals.rootNavKey.currentContext?.pop(true); final currentViewId = getIt().latestOpenView?.id; - final viewId = await showPageSelectorSheet( + final view = await showPageSelectorSheet( context, currentViewId: currentViewId, ); - if (viewId != null) { + if (view != null) { Future.delayed(const Duration(milliseconds: 100), () { editorState.insertBlockAfterCurrentSelection( selection, - pageMentionNode(viewId), + pageMentionNode(view.id), ); }); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 7464b81a81..e8c3554e73 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -106,7 +106,8 @@ class EditorStyleCustomizer { final theme = Theme.of(context); final fontSize = pageStyle.fontLayout.fontSize; final lineHeight = pageStyle.lineHeightLayout.lineHeight; - final fontFamily = pageStyle.fontFamily ?? defaultFontFamily; + final fontFamily = pageStyle.fontFamily ?? + context.read().state.font; final defaultTextDirection = context.read().state.defaultTextDirection; final textScaleFactor =