feat: show member count on mobile (#4991)

* feat: show member count on mobile

* fix: favorite section not sync after switching workspace

* fix: favorite page will throw an error

* fix: flutter analyze
This commit is contained in:
Lucas.Xu 2024-03-26 13:52:48 +07:00 committed by GitHub
parent a1b183f330
commit 6e5b346f25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 227 additions and 132 deletions

View File

@ -4,7 +4,7 @@ import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_fa
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -16,11 +16,11 @@ class MobileFavoritePageFolder extends StatelessWidget {
const MobileFavoritePageFolder({ const MobileFavoritePageFolder({
super.key, super.key,
required this.userProfile, required this.userProfile,
required this.workspaceSetting, required this.workspaceId,
}); });
final UserProfilePB userProfile; final UserProfilePB userProfile;
final WorkspaceSettingPB workspaceSetting; final String workspaceId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -31,7 +31,7 @@ class MobileFavoritePageFolder extends StatelessWidget {
..add( ..add(
SidebarSectionsEvent.initial( SidebarSectionsEvent.initial(
userProfile, userProfile,
workspaceSetting.workspaceId, workspaceId,
), ),
), ),
), ),
@ -39,45 +39,52 @@ class MobileFavoritePageFolder extends StatelessWidget {
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
), ),
], ],
child: MultiBlocListener( child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listeners: [ listener: (context, state) {
BlocListener<SidebarSectionsBloc, SidebarSectionsState>( context.read<FavoriteBloc>().add(
listenWhen: (p, c) => const FavoriteEvent.initial(),
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) =>
context.pushView(state.lastCreatedRootView!),
),
],
child: Builder(
builder: (context) {
final favoriteState = context.watch<FavoriteBloc>().state;
if (favoriteState.views.isEmpty) {
return FlowyMobileStateContainer.info(
emoji: '😁',
title: LocaleKeys.favorite_noFavorite.tr(),
description: LocaleKeys.favorite_noFavoriteHintText.tr(),
); );
} },
return Scrollbar( child: MultiBlocListener(
child: SingleChildScrollView( listeners: [
child: Padding( BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
padding: const EdgeInsets.all(8.0), listenWhen: (p, c) =>
child: SlidableAutoCloseBehavior( p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
child: Column( listener: (context, state) =>
children: [ context.pushView(state.lastCreatedRootView!),
MobileFavoriteFolder( ),
showHeader: false, ],
forceExpanded: true, child: Builder(
views: favoriteState.views, builder: (context) {
), final favoriteState = context.watch<FavoriteBloc>().state;
const VSpace(100.0), if (favoriteState.views.isEmpty) {
], return FlowyMobileStateContainer.info(
emoji: '😁',
title: LocaleKeys.favorite_noFavorite.tr(),
description: LocaleKeys.favorite_noFavoriteHintText.tr(),
);
}
return Scrollbar(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SlidableAutoCloseBehavior(
child: Column(
children: [
MobileFavoriteFolder(
showHeader: false,
forceExpanded: true,
views: favoriteState.views,
),
const VSpace(100.0),
],
),
), ),
), ),
), ),
), );
); },
}, ),
), ),
), ),
); );

View File

@ -57,9 +57,17 @@ class MobileFavoriteScreen extends StatelessWidget {
..add( ..add(
const UserWorkspaceEvent.initial(), const UserWorkspaceEvent.initial(),
), ),
child: MobileFavoritePage( child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
userProfile: userProfile, buildWhen: (previous, current) =>
workspaceSetting: workspaceSetting, previous.currentWorkspace?.workspaceId !=
current.currentWorkspace?.workspaceId,
builder: (context, state) {
return MobileFavoritePage(
userProfile: userProfile,
workspaceId: state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
);
},
), ),
), ),
), ),
@ -73,11 +81,11 @@ class MobileFavoritePage extends StatelessWidget {
const MobileFavoritePage({ const MobileFavoritePage({
super.key, super.key,
required this.userProfile, required this.userProfile,
required this.workspaceSetting, required this.workspaceId,
}); });
final UserProfilePB userProfile; final UserProfilePB userProfile;
final WorkspaceSettingPB workspaceSetting; final String workspaceId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -100,7 +108,7 @@ class MobileFavoritePage extends StatelessWidget {
Expanded( Expanded(
child: MobileFavoritePageFolder( child: MobileFavoritePageFolder(
userProfile: userProfile, userProfile: userProfile,
workspaceSetting: workspaceSetting, workspaceId: workspaceId,
), ),
), ),
], ],

View File

@ -1,10 +1,13 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart'; import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -38,9 +41,19 @@ class MobileSectionFolder extends StatelessWidget {
onPressed: () => context onPressed: () => context
.read<FolderBloc>() .read<FolderBloc>()
.add(const FolderEvent.expandOrUnExpand()), .add(const FolderEvent.expandOrUnExpand()),
onAdded: () => context.read<FolderBloc>().add( onAdded: () {
const FolderEvent.expandOrUnExpand(isExpanded: true), context.read<SidebarSectionsBloc>().add(
), SidebarSectionsEvent.createRootViewInSection(
name:
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
viewSection: categoryType.toViewSectionPB,
),
);
context.read<FolderBloc>().add(
const FolderEvent.expandOrUnExpand(isExpanded: true),
);
},
), ),
const VSpace(8.0), const VSpace(8.0),
const Divider( const Divider(

View File

@ -1,11 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@visibleForTesting @visibleForTesting
const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey'); const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey');
@ -72,15 +67,7 @@ class _MobileSectionFolderHeaderState extends State<MobileSectionFolderHeader> {
FlowySvgs.add_s, FlowySvgs.add_s,
size: Size.square(iconSize), size: Size.square(iconSize),
), ),
onPressed: () { onPressed: widget.onAdded,
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
viewSection: ViewSectionPB.Public,
),
);
},
), ),
], ],
); );

View File

@ -1,8 +1,13 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Only works on mobile. // Only works on mobile.
class MobileWorkspaceMenu extends StatelessWidget { class MobileWorkspaceMenu extends StatelessWidget {
@ -25,21 +30,12 @@ class MobileWorkspaceMenu extends StatelessWidget {
for (var i = 0; i < workspaces.length; i++) { for (var i = 0; i < workspaces.length; i++) {
final workspace = workspaces[i]; final workspace = workspaces[i];
children.add( children.add(
FlowyOptionTile.text( _WorkspaceMenuItem(
text: workspace.name, userProfile: userProfile,
workspace: workspace,
showTopBorder: i == 0, showTopBorder: i == 0,
leftIcon: WorkspaceIcon( currentWorkspace: currentWorkspace,
enableEdit: false, onWorkspaceSelected: onWorkspaceSelected,
iconSize: 22,
workspace: workspace,
),
trailing: workspace.workspaceId == currentWorkspace.workspaceId
? const FlowySvg(
FlowySvgs.m_blue_check_s,
blendMode: null,
)
: null,
onTap: () => onWorkspaceSelected(workspace),
), ),
); );
} }
@ -48,3 +44,76 @@ class MobileWorkspaceMenu extends StatelessWidget {
); );
} }
} }
class _WorkspaceMenuItem extends StatelessWidget {
const _WorkspaceMenuItem({
required this.userProfile,
required this.workspace,
required this.showTopBorder,
required this.currentWorkspace,
required this.onWorkspaceSelected,
});
final UserProfilePB userProfile;
final UserWorkspacePB workspace;
final bool showTopBorder;
final UserWorkspacePB currentWorkspace;
final void Function(UserWorkspacePB workspace) onWorkspaceSelected;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => WorkspaceMemberBloc(
userProfile: userProfile,
workspace: workspace,
)..add(const WorkspaceMemberEvent.initial()),
child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(
builder: (context, state) {
final members = state.members;
return FlowyOptionTile.text(
content: Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FlowyText(
workspace.name,
fontSize: 14,
fontWeight: FontWeight.w500,
),
FlowyText(
state.isLoading
? ''
: LocaleKeys.settings_appearance_members_membersCount
.plural(
members.length,
),
fontSize: 10.0,
color: Theme.of(context).hintColor,
),
],
),
),
),
height: 60,
showTopBorder: showTopBorder,
leftIcon: WorkspaceIcon(
enableEdit: false,
iconSize: 26,
workspace: workspace,
),
trailing: workspace.workspaceId == currentWorkspace.workspaceId
? const FlowySvg(
FlowySvgs.m_blue_check_s,
blendMode: null,
)
: null,
onTap: () => onWorkspaceSelected(workspace),
);
},
),
);
}
}

View File

@ -406,7 +406,10 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
ViewEvent.createView( ViewEvent.createView(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(), LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout, layout,
section: widget.categoryType.toViewSectionPB, section:
widget.categoryType != FolderCategoryType.favorite
? widget.categoryType.toViewSectionPB
: null,
), ),
); );
}, },

View File

@ -36,26 +36,31 @@ class FlowyOptionTile extends StatelessWidget {
this.content, this.content,
this.backgroundColor, this.backgroundColor,
this.fontFamily, this.fontFamily,
this.height,
}); });
factory FlowyOptionTile.text({ factory FlowyOptionTile.text({
required String text, String? text,
Widget? content,
Color? textColor, Color? textColor,
bool showTopBorder = true, bool showTopBorder = true,
bool showBottomBorder = true, bool showBottomBorder = true,
Widget? leftIcon, Widget? leftIcon,
Widget? trailing, Widget? trailing,
VoidCallback? onTap, VoidCallback? onTap,
double? height,
}) { }) {
return FlowyOptionTile._( return FlowyOptionTile._(
type: FlowyOptionTileType.text, type: FlowyOptionTileType.text,
text: text, text: text,
content: content,
textColor: textColor, textColor: textColor,
onTap: onTap, onTap: onTap,
showTopBorder: showTopBorder, showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder, showBottomBorder: showBottomBorder,
leading: leftIcon, leading: leftIcon,
trailing: trailing, trailing: trailing,
height: height,
); );
} }
@ -174,6 +179,8 @@ class FlowyOptionTile extends StatelessWidget {
final Color? backgroundColor; final Color? backgroundColor;
final String? fontFamily; final String? fontFamily;
final double? height;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final leadingWidget = _buildLeading(); final leadingWidget = _buildLeading();
@ -182,16 +189,19 @@ class FlowyOptionTile extends StatelessWidget {
color: backgroundColor, color: backgroundColor,
showTopBorder: showTopBorder, showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder, showBottomBorder: showBottomBorder,
child: Padding( child: SizedBox(
padding: const EdgeInsets.symmetric(horizontal: 16.0), height: height,
child: Row( child: Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 16.0),
if (leadingWidget != null) leadingWidget, child: Row(
if (content != null) content!, children: [
if (content == null) _buildText(), if (leadingWidget != null) leadingWidget,
if (content == null) _buildTextField(), if (content != null) content!,
if (trailing != null) trailing!, if (content == null) _buildText(),
], if (content == null) _buildTextField(),
if (trailing != null) trailing!,
],
),
), ),
), ),
); );

View File

@ -27,8 +27,8 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
void _dispatch() { void _dispatch() {
on<FavoriteEvent>( on<FavoriteEvent>(
(event, emit) async { (event, emit) async {
await event.map( await event.when(
initial: (e) async { initial: () async {
_listener.start( _listener.start(
favoritesUpdated: _onFavoritesUpdated, favoritesUpdated: _onFavoritesUpdated,
); );
@ -44,23 +44,23 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
), ),
); );
}, },
didFavorite: (e) { fetchFavorites: () async {
final result = await _service.readFavorites();
emit( emit(
state.copyWith(views: [...state.views, ...e.favorite.items]), result.fold(
(view) => state.copyWith(
views: view.items,
),
(error) => state.copyWith(
views: [],
),
),
); );
}, },
didUnfavorite: (e) { toggle: (view) async {
final views = [...state.views]..removeWhere(
(view) => e.favorite.items.any((item) => item.id == view.id),
);
emit(
state.copyWith(views: views),
);
},
toggle: (e) async {
await _service.toggleFavorite( await _service.toggleFavorite(
e.view.id, view.id,
!e.view.isFavorite, !view.isFavorite,
); );
}, },
); );
@ -73,9 +73,7 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
bool didFavorite, bool didFavorite,
) { ) {
favoriteOrFailed.fold( favoriteOrFailed.fold(
(favorite) => didFavorite (favorite) => add(const FetchFavorites()),
? add(FavoriteEvent.didFavorite(favorite))
: add(FavoriteEvent.didUnfavorite(favorite)),
(error) => Log.error(error), (error) => Log.error(error),
); );
} }
@ -84,11 +82,8 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
@freezed @freezed
class FavoriteEvent with _$FavoriteEvent { class FavoriteEvent with _$FavoriteEvent {
const factory FavoriteEvent.initial() = Initial; const factory FavoriteEvent.initial() = Initial;
const factory FavoriteEvent.didFavorite(RepeatedViewPB favorite) =
DidFavorite;
const factory FavoriteEvent.didUnfavorite(RepeatedViewPB favorite) =
DidUnfavorite;
const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite; const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite;
const factory FavoriteEvent.fetchFavorites() = FetchFavorites;
} }
@freezed @freezed

View File

@ -37,24 +37,25 @@ class FavoriteListener {
FolderNotification ty, FolderNotification ty,
FlowyResult<Uint8List, FlowyError> result, FlowyResult<Uint8List, FlowyError> result,
) { ) {
if (_favoriteUpdated == null) { switch (ty) {
return; case FolderNotification.DidFavoriteView:
} result.onSuccess(
(success) => _favoriteUpdated?.call(
final isFavorite = ty == FolderNotification.DidFavoriteView; FlowyResult.success(RepeatedViewPB.fromBuffer(success)),
result.fold( true,
(payload) { ),
final view = RepeatedViewPB.fromBuffer(payload);
_favoriteUpdated!(
FlowyResult.success(view),
isFavorite,
); );
}, case FolderNotification.DidUnfavoriteView:
(error) => _favoriteUpdated!( result.map(
FlowyResult.failure(error), (success) => _favoriteUpdated?.call(
isFavorite, FlowyResult.success(RepeatedViewPB.fromBuffer(success)),
), false,
); ),
);
break;
default:
break;
}
} }
Future<void> stop() async { Future<void> stop() async {

View File

@ -363,7 +363,7 @@ class ViewEvent with _$ViewEvent {
ViewLayoutPB layoutType, { ViewLayoutPB layoutType, {
/// open the view after created /// open the view after created
@Default(true) bool openAfterCreated, @Default(true) bool openAfterCreated,
required ViewSectionPB section, ViewSectionPB? section,
}) = CreateView; }) = CreateView;
const factory ViewEvent.viewDidUpdate( const factory ViewEvent.viewDidUpdate(
FlowyResult<ViewPB, FlowyError> result, FlowyResult<ViewPB, FlowyError> result,

View File

@ -174,10 +174,10 @@ class _SidebarSwitchWorkspaceButtonState
children: [ children: [
const HSpace(2.0), const HSpace(2.0),
SizedBox.square( SizedBox.square(
dimension: 28.0, dimension: 30.0,
child: WorkspaceIcon( child: WorkspaceIcon(
workspace: widget.currentWorkspace, workspace: widget.currentWorkspace,
iconSize: 18, iconSize: 20,
enableEdit: false, enableEdit: false,
), ),
), ),

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:appflowy/util/color_generator/color_generator.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
@ -31,16 +33,16 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
Widget child = widget.workspace.icon.isNotEmpty Widget child = widget.workspace.icon.isNotEmpty
? Container( ? Container(
width: widget.iconSize, width: widget.iconSize,
margin: const EdgeInsets.all(2), alignment: Alignment.center,
child: FlowyText( child: FlowyText(
widget.workspace.icon, widget.workspace.icon,
textAlign: TextAlign.center,
fontSize: widget.iconSize, fontSize: widget.iconSize,
), ),
) )
: Container( : Container(
alignment: Alignment.center, alignment: Alignment.center,
width: widget.iconSize, width: widget.iconSize,
height: max(widget.iconSize, 26),
decoration: BoxDecoration( decoration: BoxDecoration(
color: ColorGenerator.generateColorFromString( color: ColorGenerator.generateColorFromString(
widget.workspace.name, widget.workspace.name,