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/workspace/application/favorite/favorite_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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -16,11 +16,11 @@ class MobileFavoritePageFolder extends StatelessWidget {
const MobileFavoritePageFolder({
super.key,
required this.userProfile,
required this.workspaceSetting,
required this.workspaceId,
});
final UserProfilePB userProfile;
final WorkspaceSettingPB workspaceSetting;
final String workspaceId;
@override
Widget build(BuildContext context) {
@ -31,7 +31,7 @@ class MobileFavoritePageFolder extends StatelessWidget {
..add(
SidebarSectionsEvent.initial(
userProfile,
workspaceSetting.workspaceId,
workspaceId,
),
),
),
@ -39,45 +39,52 @@ class MobileFavoritePageFolder extends StatelessWidget {
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) =>
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(),
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
context.read<FavoriteBloc>().add(
const FavoriteEvent.initial(),
);
}
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),
],
},
child: MultiBlocListener(
listeners: [
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) =>
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: 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(
const UserWorkspaceEvent.initial(),
),
child: MobileFavoritePage(
userProfile: userProfile,
workspaceSetting: workspaceSetting,
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
buildWhen: (previous, current) =>
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({
super.key,
required this.userProfile,
required this.workspaceSetting,
required this.workspaceId,
});
final UserProfilePB userProfile;
final WorkspaceSettingPB workspaceSetting;
final String workspaceId;
@override
Widget build(BuildContext context) {
@ -100,7 +108,7 @@ class MobileFavoritePage extends StatelessWidget {
Expanded(
child: MobileFavoritePageFolder(
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/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/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/view/view_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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -38,9 +41,19 @@ class MobileSectionFolder extends StatelessWidget {
onPressed: () => context
.read<FolderBloc>()
.add(const FolderEvent.expandOrUnExpand()),
onAdded: () => context.read<FolderBloc>().add(
const FolderEvent.expandOrUnExpand(isExpanded: true),
),
onAdded: () {
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 Divider(

View File

@ -1,11 +1,6 @@
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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@visibleForTesting
const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey');
@ -72,15 +67,7 @@ class _MobileSectionFolderHeaderState extends State<MobileSectionFolderHeader> {
FlowySvgs.add_s,
size: Size.square(iconSize),
),
onPressed: () {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
viewSection: ViewSectionPB.Public,
),
);
},
onPressed: widget.onAdded,
),
],
);

View File

@ -1,8 +1,13 @@
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/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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Only works on mobile.
class MobileWorkspaceMenu extends StatelessWidget {
@ -25,21 +30,12 @@ class MobileWorkspaceMenu extends StatelessWidget {
for (var i = 0; i < workspaces.length; i++) {
final workspace = workspaces[i];
children.add(
FlowyOptionTile.text(
text: workspace.name,
_WorkspaceMenuItem(
userProfile: userProfile,
workspace: workspace,
showTopBorder: i == 0,
leftIcon: WorkspaceIcon(
enableEdit: false,
iconSize: 22,
workspace: workspace,
),
trailing: workspace.workspaceId == currentWorkspace.workspaceId
? const FlowySvg(
FlowySvgs.m_blue_check_s,
blendMode: null,
)
: null,
onTap: () => onWorkspaceSelected(workspace),
currentWorkspace: currentWorkspace,
onWorkspaceSelected: onWorkspaceSelected,
),
);
}
@ -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(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
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.backgroundColor,
this.fontFamily,
this.height,
});
factory FlowyOptionTile.text({
required String text,
String? text,
Widget? content,
Color? textColor,
bool showTopBorder = true,
bool showBottomBorder = true,
Widget? leftIcon,
Widget? trailing,
VoidCallback? onTap,
double? height,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.text,
text: text,
content: content,
textColor: textColor,
onTap: onTap,
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
leading: leftIcon,
trailing: trailing,
height: height,
);
}
@ -174,6 +179,8 @@ class FlowyOptionTile extends StatelessWidget {
final Color? backgroundColor;
final String? fontFamily;
final double? height;
@override
Widget build(BuildContext context) {
final leadingWidget = _buildLeading();
@ -182,16 +189,19 @@ class FlowyOptionTile extends StatelessWidget {
color: backgroundColor,
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
if (leadingWidget != null) leadingWidget,
if (content != null) content!,
if (content == null) _buildText(),
if (content == null) _buildTextField(),
if (trailing != null) trailing!,
],
child: SizedBox(
height: height,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
if (leadingWidget != null) leadingWidget,
if (content != null) content!,
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() {
on<FavoriteEvent>(
(event, emit) async {
await event.map(
initial: (e) async {
await event.when(
initial: () async {
_listener.start(
favoritesUpdated: _onFavoritesUpdated,
);
@ -44,23 +44,23 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
),
);
},
didFavorite: (e) {
fetchFavorites: () async {
final result = await _service.readFavorites();
emit(
state.copyWith(views: [...state.views, ...e.favorite.items]),
result.fold(
(view) => state.copyWith(
views: view.items,
),
(error) => state.copyWith(
views: [],
),
),
);
},
didUnfavorite: (e) {
final views = [...state.views]..removeWhere(
(view) => e.favorite.items.any((item) => item.id == view.id),
);
emit(
state.copyWith(views: views),
);
},
toggle: (e) async {
toggle: (view) async {
await _service.toggleFavorite(
e.view.id,
!e.view.isFavorite,
view.id,
!view.isFavorite,
);
},
);
@ -73,9 +73,7 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
bool didFavorite,
) {
favoriteOrFailed.fold(
(favorite) => didFavorite
? add(FavoriteEvent.didFavorite(favorite))
: add(FavoriteEvent.didUnfavorite(favorite)),
(favorite) => add(const FetchFavorites()),
(error) => Log.error(error),
);
}
@ -84,11 +82,8 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
@freezed
class FavoriteEvent with _$FavoriteEvent {
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.fetchFavorites() = FetchFavorites;
}
@freezed

View File

@ -37,24 +37,25 @@ class FavoriteListener {
FolderNotification ty,
FlowyResult<Uint8List, FlowyError> result,
) {
if (_favoriteUpdated == null) {
return;
}
final isFavorite = ty == FolderNotification.DidFavoriteView;
result.fold(
(payload) {
final view = RepeatedViewPB.fromBuffer(payload);
_favoriteUpdated!(
FlowyResult.success(view),
isFavorite,
switch (ty) {
case FolderNotification.DidFavoriteView:
result.onSuccess(
(success) => _favoriteUpdated?.call(
FlowyResult.success(RepeatedViewPB.fromBuffer(success)),
true,
),
);
},
(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 {

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/util/color_generator/color_generator.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
? Container(
width: widget.iconSize,
margin: const EdgeInsets.all(2),
alignment: Alignment.center,
child: FlowyText(
widget.workspace.icon,
textAlign: TextAlign.center,
fontSize: widget.iconSize,
),
)
: Container(
alignment: Alignment.center,
width: widget.iconSize,
height: max(widget.iconSize, 26),
decoration: BoxDecoration(
color: ColorGenerator.generateColorFromString(
widget.workspace.name,