From 11200a5b3d90a91f63b4754dad7edd692a79ebab Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 4 Jun 2025 10:59:16 +0800 Subject: [PATCH] feat: enable shared section on mobile (#8020) * feat: enable shared section on mobile * feat: update mobile view item * feat: shared with me section on mobile --- .../presentation/m_shared_section.dart | 91 +++++++++++++++++++ .../presentation/shared_section.dart | 9 +- .../widgets/m_shared_page_list.dart | 38 ++++++++ .../widgets/m_shared_section_header.dart | 38 ++++++++ ..._pages_list.dart => shared_page_list.dart} | 5 +- .../widgets/shared_section_empty.dart | 41 +++++++++ .../presentation/home/mobile_home_page.dart | 2 +- .../home/tab/mobile_space_tab.dart | 21 ++++- .../home/tab/space_order_bloc.dart | 8 +- .../share_section/shared_pages_list_test.dart | 6 +- .../flowy_icons/20x/empty_shared_section.svg | 3 + 11 files changed, 247 insertions(+), 15 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/features/shared_section/presentation/m_shared_section.dart create mode 100644 frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/m_shared_page_list.dart create mode 100644 frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/m_shared_section_header.dart rename frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/{shared_pages_list.dart => shared_page_list.dart} (96%) create mode 100644 frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_section_empty.dart create mode 100644 frontend/resources/flowy_icons/20x/empty_shared_section.svg diff --git a/frontend/appflowy_flutter/lib/features/shared_section/presentation/m_shared_section.dart b/frontend/appflowy_flutter/lib/features/shared_section/presentation/m_shared_section.dart new file mode 100644 index 0000000000..ea642df6d0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/features/shared_section/presentation/m_shared_section.dart @@ -0,0 +1,91 @@ +import 'package:appflowy/features/shared_section/data/repositories/rust_shared_pages_repository_impl.dart'; +import 'package:appflowy/features/shared_section/logic/shared_section_bloc.dart'; +import 'package:appflowy/features/shared_section/presentation/widgets/m_shared_page_list.dart'; +import 'package:appflowy/features/shared_section/presentation/widgets/m_shared_section_header.dart'; +import 'package:appflowy/features/shared_section/presentation/widgets/refresh_button.dart'; +import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_error.dart'; +import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_loading.dart'; +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MSharedSection extends StatelessWidget { + const MSharedSection({ + super.key, + required this.workspaceId, + }); + + final String workspaceId; + + @override + Widget build(BuildContext context) { + final repository = RustSharePagesRepositoryImpl(); + + return BlocProvider( + create: (_) => SharedSectionBloc( + workspaceId: workspaceId, + repository: repository, + enablePolling: true, + )..add(const SharedSectionInitEvent()), + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const SharedSectionLoading(); + } + + if (state.errorMessage.isNotEmpty) { + return SharedSectionError(errorMessage: state.errorMessage); + } + + // hide the shared section if there are no shared pages + if (state.sharedPages.isEmpty) { + return const SizedBox.shrink(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const VSpace(HomeSpaceViewSizes.mVerticalPadding), + + // Shared header + MSharedSectionHeader(), + + Padding( + padding: const EdgeInsets.only( + left: HomeSpaceViewSizes.mHorizontalPadding, + ), + child: MSharedPageList( + sharedPages: state.sharedPages, + onSelected: (view) { + context.pushView( + view, + tabs: [ + PickerTabType.emoji, + PickerTabType.icon, + PickerTabType.custom, + ].map((e) => e.name).toList(), + ); + }, + ), + ), + + // Refresh button, for debugging only + if (kDebugMode) + RefreshSharedSectionButton( + onTap: () { + context.read().add( + const SharedSectionEvent.refresh(), + ); + }, + ), + ], + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/features/shared_section/presentation/shared_section.dart b/frontend/appflowy_flutter/lib/features/shared_section/presentation/shared_section.dart index f26ec3c48b..5fb8ebd928 100644 --- a/frontend/appflowy_flutter/lib/features/shared_section/presentation/shared_section.dart +++ b/frontend/appflowy_flutter/lib/features/shared_section/presentation/shared_section.dart @@ -1,7 +1,7 @@ -import 'package:appflowy/features/shared_section/data/repositories/rust_shared_pages_repository_impl.dart'; +import 'package:appflowy/features/shared_section/data/repositories/local_shared_pages_repository_impl.dart'; import 'package:appflowy/features/shared_section/logic/shared_section_bloc.dart'; import 'package:appflowy/features/shared_section/presentation/widgets/refresh_button.dart'; -import 'package:appflowy/features/shared_section/presentation/widgets/shared_pages_list.dart'; +import 'package:appflowy/features/shared_section/presentation/widgets/shared_page_list.dart'; import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_error.dart'; import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_header.dart'; import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_loading.dart'; @@ -30,7 +30,8 @@ class SharedSection extends StatelessWidget { @override Widget build(BuildContext context) { - final repository = RustSharePagesRepositoryImpl(); + // final repository = RustSharePagesRepositoryImpl(); + final repository = LocalSharedPagesRepositoryImpl(); return BlocProvider( create: (_) => SharedSectionBloc( @@ -68,7 +69,7 @@ class SharedSection extends StatelessWidget { // Shared pages list if (state.isExpanded) - SharedPagesList( + SharedPageList( sharedPages: state.sharedPages, onSetEditing: (context, value) { context.read().add(ViewEvent.setIsEditing(value)); diff --git a/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/m_shared_page_list.dart b/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/m_shared_page_list.dart new file mode 100644 index 0000000000..2076805314 --- /dev/null +++ b/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/m_shared_page_list.dart @@ -0,0 +1,38 @@ +import 'package:appflowy/features/shared_section/models/shared_page.dart'; +import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:flutter/material.dart'; + +/// Shared pages on mobile +class MSharedPageList extends StatelessWidget { + const MSharedPageList({ + super.key, + required this.sharedPages, + required this.onSelected, + }); + + final SharedPages sharedPages; + final ViewItemOnSelected onSelected; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: sharedPages.map((sharedPage) { + final view = sharedPage.view; + return MobileViewItem( + key: ValueKey(view.id), + spaceType: FolderSpaceType.public, + isFirstChild: view.id == sharedPages.first.view.id, + view: view, + level: 0, + isDraggable: false, // disable draggable for shared pages + leftPadding: HomeSpaceViewSizes.leftPadding, + isFeedback: false, + onSelected: onSelected, + ); + }).toList(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/m_shared_section_header.dart b/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/m_shared_section_header.dart new file mode 100644 index 0000000000..07c4c279bf --- /dev/null +++ b/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/m_shared_section_header.dart @@ -0,0 +1,38 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; + +class MSharedSectionHeader extends StatelessWidget { + const MSharedSectionHeader({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return SizedBox( + height: 48, + child: Row( + children: [ + const HSpace(HomeSpaceViewSizes.mHorizontalPadding), + FlowySvg( + FlowySvgs.shared_with_me_m, + color: theme.badgeColorScheme.color13Thick2, + ), + const HSpace(10.0), + FlowyText.medium( + LocaleKeys.shareSection_shared.tr(), + lineHeight: 1.15, + fontSize: 16.0, + ), + const HSpace(HomeSpaceViewSizes.mHorizontalPadding), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_pages_list.dart b/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_page_list.dart similarity index 96% rename from frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_pages_list.dart rename to frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_page_list.dart index 0143e8a856..2fd1bde910 100644 --- a/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_pages_list.dart +++ b/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_page_list.dart @@ -9,8 +9,9 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -class SharedPagesList extends StatelessWidget { - const SharedPagesList({ +/// Shared pages on desktop +class SharedPageList extends StatelessWidget { + const SharedPageList({ super.key, required this.sharedPages, required this.onAction, diff --git a/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_section_empty.dart b/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_section_empty.dart new file mode 100644 index 0000000000..dfc3906b2b --- /dev/null +++ b/frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_section_empty.dart @@ -0,0 +1,41 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; + +class SharedSectionEmpty extends StatelessWidget { + const SharedSectionEmpty({super.key}); + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlowySvg( + FlowySvgs.empty_shared_section_m, + color: theme.iconColorScheme.tertiary, + ), + const VSpace(12), + Text( + 'Nothing shared with you', + style: theme.textStyle.heading3.enhanced( + color: theme.textColorScheme.secondary, + ), + textAlign: TextAlign.center, + ), + const VSpace(4), + Text( + 'Pages shared with you will show here', + style: theme.textStyle.heading4.standard( + color: theme.textColorScheme.tertiary, + ), + textAlign: TextAlign.center, + ), + const VSpace(kBottomNavigationBarHeight + 60.0), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index e8f4585daa..edd276a2ca 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -239,7 +239,7 @@ class _HomePageState extends State<_HomePage> { ), ), ], - child: MobileSpaceTab( + child: MobileHomePageTab( userProfile: widget.userProfile, ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart index 56ad1b86df..0f12d1ea7f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/features/shared_section/presentation/m_shared_section.dart'; import 'package:appflowy/features/workspace/logic/workspace_bloc.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/home/favorite_folder/favorite_space.dart'; @@ -23,8 +24,8 @@ import 'ai_bubble_button.dart'; final ValueNotifier mobileCreateNewAIChatNotifier = ValueNotifier(0); -class MobileSpaceTab extends StatefulWidget { - const MobileSpaceTab({ +class MobileHomePageTab extends StatefulWidget { + const MobileHomePageTab({ super.key, required this.userProfile, }); @@ -32,10 +33,10 @@ class MobileSpaceTab extends StatefulWidget { final UserProfilePB userProfile; @override - State createState() => _MobileSpaceTabState(); + State createState() => _MobileHomePageTabState(); } -class _MobileSpaceTabState extends State +class _MobileHomePageTabState extends State with SingleTickerProviderStateMixin { TabController? tabController; @@ -178,6 +179,18 @@ class _MobileSpaceTabState extends State ); case MobileSpaceTabType.favorites: return MobileFavoriteSpace(userProfile: widget.userProfile); + case MobileSpaceTabType.shared: + final workspaceId = context + .read() + .state + .currentWorkspace + ?.workspaceId; + if (workspaceId == null) { + return const SizedBox.shrink(); + } + return MSharedSection( + workspaceId: workspaceId, + ); } }).toList(); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart index e3c1439dd4..e37c4bee11 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart @@ -15,7 +15,8 @@ enum MobileSpaceTabType { // DO NOT CHANGE THE ORDER spaces, recent, - favorites; + favorites, + shared; String get tr { switch (this) { @@ -25,6 +26,8 @@ enum MobileSpaceTabType { return LocaleKeys.sideBar_Spaces.tr(); case MobileSpaceTabType.favorites: return LocaleKeys.sideBar_favoriteSpace.tr(); + case MobileSpaceTabType.shared: + return 'Shared'; } } } @@ -89,6 +92,9 @@ class SpaceOrderBloc extends Bloc { if (order.isEmpty) { return MobileSpaceTabType.values; } + if (!order.contains(MobileSpaceTabType.shared.index)) { + order.add(MobileSpaceTabType.shared.index); + } return order .map((e) => MobileSpaceTabType.values[e]) .cast() diff --git a/frontend/appflowy_flutter/test/widget_test/lib/features/share_section/shared_pages_list_test.dart b/frontend/appflowy_flutter/test/widget_test/lib/features/share_section/shared_pages_list_test.dart index 7868bf80e1..573b75a960 100644 --- a/frontend/appflowy_flutter/test/widget_test/lib/features/share_section/shared_pages_list_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/lib/features/share_section/shared_pages_list_test.dart @@ -1,7 +1,7 @@ import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/features/share_tab/data/models/share_access_level.dart'; import 'package:appflowy/features/shared_section/models/shared_page.dart'; -import 'package:appflowy/features/shared_section/presentation/widgets/shared_pages_list.dart'; +import 'package:appflowy/features/shared_section/presentation/widgets/shared_page_list.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter/material.dart'; @@ -47,7 +47,7 @@ void main() { await tester.pumpWidget( WidgetTestWrapper( child: SingleChildScrollView( - child: SharedPagesList( + child: SharedPageList( sharedPages: sharedPages, onAction: (action, view, data) {}, onSelected: (context, view) {}, @@ -59,7 +59,7 @@ void main() { ); expect(find.text('Page 1'), findsOneWidget); expect(find.text('Page 2'), findsOneWidget); - expect(find.byType(SharedPagesList), findsOneWidget); + expect(find.byType(SharedPageList), findsOneWidget); }); }); } diff --git a/frontend/resources/flowy_icons/20x/empty_shared_section.svg b/frontend/resources/flowy_icons/20x/empty_shared_section.svg new file mode 100644 index 0000000000..89e5d83cf7 --- /dev/null +++ b/frontend/resources/flowy_icons/20x/empty_shared_section.svg @@ -0,0 +1,3 @@ + + +