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,6 +39,12 @@ class MobileFavoritePageFolder extends StatelessWidget {
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
), ),
], ],
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
context.read<FavoriteBloc>().add(
const FavoriteEvent.initial(),
);
},
child: MultiBlocListener( child: MultiBlocListener(
listeners: [ listeners: [
BlocListener<SidebarSectionsBloc, SidebarSectionsState>( BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
@ -80,6 +86,7 @@ class MobileFavoritePageFolder extends StatelessWidget {
}, },
), ),
), ),
),
); );
} }
} }

View File

@ -57,9 +57,17 @@ class MobileFavoriteScreen extends StatelessWidget {
..add( ..add(
const UserWorkspaceEvent.initial(), const UserWorkspaceEvent.initial(),
), ),
child: MobileFavoritePage( child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
buildWhen: (previous, current) =>
previous.currentWorkspace?.workspaceId !=
current.currentWorkspace?.workspaceId,
builder: (context, state) {
return MobileFavoritePage(
userProfile: userProfile, userProfile: userProfile,
workspaceSetting: workspaceSetting, 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,12 +30,78 @@ 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,
currentWorkspace: currentWorkspace,
onWorkspaceSelected: onWorkspaceSelected,
),
);
}
return Column(
children: children,
);
}
}
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( leftIcon: WorkspaceIcon(
enableEdit: false, enableEdit: false,
iconSize: 22, iconSize: 26,
workspace: workspace, workspace: workspace,
), ),
trailing: workspace.workspaceId == currentWorkspace.workspaceId trailing: workspace.workspaceId == currentWorkspace.workspaceId
@ -40,11 +111,9 @@ class MobileWorkspaceMenu extends StatelessWidget {
) )
: null, : null,
onTap: () => onWorkspaceSelected(workspace), onTap: () => onWorkspaceSelected(workspace),
);
},
), ),
); );
} }
return Column(
children: children,
);
}
} }

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,6 +189,8 @@ class FlowyOptionTile extends StatelessWidget {
color: backgroundColor, color: backgroundColor,
showTopBorder: showTopBorder, showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder, showBottomBorder: showBottomBorder,
child: SizedBox(
height: height,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row( child: Row(
@ -194,6 +203,7 @@ class FlowyOptionTile extends StatelessWidget {
], ],
), ),
), ),
),
); );
if (type == FlowyOptionTileType.checkbox || if (type == FlowyOptionTileType.checkbox ||

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,
);
},
(error) => _favoriteUpdated!(
FlowyResult.failure(error),
isFavorite,
), ),
); );
case FolderNotification.DidUnfavoriteView:
result.map(
(success) => _favoriteUpdated?.call(
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,