feat: support private section (#4882)

This commit is contained in:
Lucas.Xu
2024-03-21 11:02:03 +07:00
committed by GitHub
parent 9201cd6347
commit ef9891abfe
75 changed files with 1758 additions and 776 deletions

View File

@ -33,18 +33,19 @@ class SidebarRootViewsBloc
await event.when(
initial: (userProfile, workspaceId) async {
_initial(userProfile, workspaceId);
await _fetchApps(emit);
await _fetchRootViews(emit);
},
reset: (userProfile, workspaceId) async {
await _listener?.stop();
_initial(userProfile, workspaceId);
await _fetchApps(emit);
await _fetchRootViews(emit);
},
createRootView: (name, desc, index) async {
final result = await _workspaceService.createApp(
createRootView: (name, desc, index, section) async {
final result = await _workspaceService.createView(
name: name,
desc: desc,
index: index,
viewSection: section,
);
result.fold(
(view) => emit(state.copyWith(lastCreatedRootView: view)),
@ -59,48 +60,59 @@ class SidebarRootViewsBloc
);
},
didReceiveViews: (viewsOrFailure) async {
emit(
viewsOrFailure.fold(
(views) => state.copyWith(
views: views,
successOrFailure: FlowyResult.success(null),
),
(err) =>
state.copyWith(successOrFailure: FlowyResult.failure(err)),
),
);
// emit(
// viewsOrFailure.fold(
// (views) => state.copyWith(
// views: views,
// successOrFailure: FlowyResult.success(null),
// ),
// (err) =>
// state.copyWith(successOrFailure: FlowyResult.failure(err)),
// ),
// );
},
moveRootView: (int fromIndex, int toIndex) {
if (state.views.length > fromIndex) {
final view = state.views[fromIndex];
// if (state.views.length > fromIndex) {
// final view = state.views[fromIndex];
_workspaceService.moveApp(
appId: view.id,
fromIndex: fromIndex,
toIndex: toIndex,
);
// _workspaceService.moveApp(
// appId: view.id,
// fromIndex: fromIndex,
// toIndex: toIndex,
// );
final views = List<ViewPB>.from(state.views);
views.insert(toIndex, views.removeAt(fromIndex));
emit(state.copyWith(views: views));
}
// final views = List<ViewPB>.from(state.views);
// views.insert(toIndex, views.removeAt(fromIndex));
// emit(state.copyWith(views: views));
// }
},
);
},
);
}
Future<void> _fetchApps(Emitter<SidebarRootViewState> emit) async {
final viewsOrError = await _workspaceService.getViews();
emit(
viewsOrError.fold(
(views) => state.copyWith(views: views),
(error) {
Log.error(error);
return state.copyWith(successOrFailure: FlowyResult.failure(error));
},
),
);
Future<void> _fetchRootViews(
Emitter<SidebarRootViewState> emit,
) async {
try {
final publicViews = await _workspaceService.getPublicViews().getOrThrow();
final privateViews =
await _workspaceService.getPrivateViews().getOrThrow();
emit(
state.copyWith(
publicViews: publicViews,
privateViews: privateViews,
),
);
} catch (e) {
Log.error(e);
// TODO: handle error
// emit(
// state.copyWith(
// successOrFailure: FlowyResult.failure(e),
// ),
// );
}
}
void _handleAppsOrFail(FlowyResult<List<ViewPB>, FlowyError> viewsOrFail) {
@ -137,9 +149,12 @@ class SidebarRootViewsEvent with _$SidebarRootViewsEvent {
String name, {
String? desc,
int? index,
required ViewSectionPB viewSection,
}) = _createRootView;
const factory SidebarRootViewsEvent.moveRootView(int fromIndex, int toIndex) =
_MoveRootView;
const factory SidebarRootViewsEvent.moveRootView(
int fromIndex,
int toIndex,
) = _MoveRootView;
const factory SidebarRootViewsEvent.didReceiveViews(
FlowyResult<List<ViewPB>, FlowyError> appsOrFail,
) = _ReceiveApps;
@ -148,13 +163,13 @@ class SidebarRootViewsEvent with _$SidebarRootViewsEvent {
@freezed
class SidebarRootViewState with _$SidebarRootViewState {
const factory SidebarRootViewState({
required List<ViewPB> views,
@Default([]) List<ViewPB> privateViews,
@Default([]) List<ViewPB> publicViews,
required FlowyResult<void, FlowyError> successOrFailure,
@Default(null) ViewPB? lastCreatedRootView,
}) = _SidebarRootViewState;
factory SidebarRootViewState.initial() => SidebarRootViewState(
views: [],
successOrFailure: FlowyResult.success(null),
);
}

View File

@ -0,0 +1,261 @@
import 'dart:async';
import 'package:appflowy/workspace/application/workspace/workspace_sections_listener.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'sidebar_sections_bloc.freezed.dart';
class SidebarSection {
const SidebarSection({
required this.publicViews,
required this.privateViews,
});
const SidebarSection.empty()
: publicViews = const [],
privateViews = const [];
final List<ViewPB> publicViews;
final List<ViewPB> privateViews;
List<ViewPB> get views => publicViews + privateViews;
SidebarSection copyWith({
List<ViewPB>? publicViews,
List<ViewPB>? privateViews,
}) {
return SidebarSection(
publicViews: publicViews ?? this.publicViews,
privateViews: privateViews ?? this.privateViews,
);
}
}
/// The [SidebarSectionsBloc] is responsible for
/// managing the root views in different sections of the workspace.
class SidebarSectionsBloc
extends Bloc<SidebarSectionsEvent, SidebarSectionsState> {
SidebarSectionsBloc() : super(SidebarSectionsState.initial()) {
on<SidebarSectionsEvent>(
(event, emit) async {
await event.when(
initial: (userProfile, workspaceId) async {
_initial(userProfile, workspaceId);
final sectionViews = await _getSectionViews();
if (sectionViews != null) {
emit(
state.copyWith(
section: sectionViews,
),
);
}
},
reset: (userProfile, workspaceId) async {
_reset(userProfile, workspaceId);
final sectionViews = await _getSectionViews();
if (sectionViews != null) {
emit(
state.copyWith(
section: sectionViews,
),
);
}
},
createRootViewInSection: (name, section, desc, index) async {
final result = await _workspaceService.createView(
name: name,
viewSection: section,
desc: desc,
index: index,
);
result.fold(
(view) => emit(
state.copyWith(
lastCreatedRootView: view,
createRootViewResult: FlowyResult.success(null),
),
),
(error) {
Log.error('Failed to create root view: $error');
emit(
state.copyWith(
createRootViewResult: FlowyResult.failure(error),
),
);
},
);
},
receiveSectionViewsUpdate: (sectionViews) async {
final section = sectionViews.section;
switch (section) {
case ViewSectionPB.Public:
emit(
state.copyWith(
section: state.section.copyWith(
publicViews: sectionViews.views,
),
),
);
case ViewSectionPB.Private:
emit(
state.copyWith(
section: state.section.copyWith(
privateViews: sectionViews.views,
),
),
);
break;
default:
break;
}
},
moveRootView: (fromIndex, toIndex, fromSection, toSection) async {
final views = fromSection == ViewSectionPB.Public
? List<ViewPB>.from(state.section.publicViews)
: List<ViewPB>.from(state.section.privateViews);
if (fromIndex < 0 || fromIndex >= views.length) {
Log.error(
'Invalid fromIndex: $fromIndex, maxIndex: ${views.length - 1}',
);
return;
}
final view = views[fromIndex];
final result = await _workspaceService.moveView(
viewId: view.id,
fromIndex: fromIndex,
toIndex: toIndex,
);
result.fold(
(value) {
views.insert(toIndex, views.removeAt(fromIndex));
var newState = state;
if (fromSection == ViewSectionPB.Public) {
newState = newState.copyWith(
section: newState.section.copyWith(publicViews: views),
);
} else if (fromSection == ViewSectionPB.Private) {
newState = newState.copyWith(
section: newState.section.copyWith(privateViews: views),
);
}
emit(newState);
},
(error) {
Log.error('Failed to move root view: $error');
},
);
},
);
},
);
}
late WorkspaceService _workspaceService;
WorkspaceSectionsListener? _listener;
@override
Future<void> close() async {
await _listener?.stop();
_listener = null;
return super.close();
}
ViewSectionPB? getViewSection(ViewPB view) {
final publicViews = state.section.publicViews.map((e) => e.id);
final privateViews = state.section.privateViews.map((e) => e.id);
if (publicViews.contains(view.id)) {
return ViewSectionPB.Public;
} else if (privateViews.contains(view.id)) {
return ViewSectionPB.Private;
} else {
return null;
}
}
Future<SidebarSection?> _getSectionViews() async {
try {
final publicViews = await _workspaceService.getPublicViews().getOrThrow();
final privateViews =
await _workspaceService.getPrivateViews().getOrThrow();
return SidebarSection(
publicViews: publicViews,
privateViews: privateViews,
);
} catch (e) {
Log.error('Failed to get section views: $e');
return null;
}
}
void _initial(UserProfilePB userProfile, String workspaceId) {
_workspaceService = WorkspaceService(workspaceId: workspaceId);
_listener = WorkspaceSectionsListener(
user: userProfile,
workspaceId: workspaceId,
)..start(
sectionChanged: (result) {
if (!isClosed) {
result.fold(
(s) => add(SidebarSectionsEvent.receiveSectionViewsUpdate(s)),
(f) => Log.error('Failed to receive section views: $f'),
);
}
},
);
}
void _reset(UserProfilePB userProfile, String workspaceId) {
_listener?.stop();
_listener = null;
_initial(userProfile, workspaceId);
}
}
@freezed
class SidebarSectionsEvent with _$SidebarSectionsEvent {
const factory SidebarSectionsEvent.initial(
UserProfilePB userProfile,
String workspaceId,
) = _Initial;
const factory SidebarSectionsEvent.reset(
UserProfilePB userProfile,
String workspaceId,
) = _Reset;
const factory SidebarSectionsEvent.createRootViewInSection({
required String name,
required ViewSectionPB viewSection,
String? desc,
int? index,
}) = _CreateRootViewInSection;
const factory SidebarSectionsEvent.moveRootView({
required int fromIndex,
required int toIndex,
required ViewSectionPB fromSection,
required ViewSectionPB toSection,
}) = _MoveRootView;
const factory SidebarSectionsEvent.receiveSectionViewsUpdate(
SectionViewsPB sectionViews,
) = _ReceiveSectionViewsUpdate;
}
@freezed
class SidebarSectionsState with _$SidebarSectionsState {
const factory SidebarSectionsState({
required SidebarSection section,
@Default(null) ViewPB? lastCreatedRootView,
FlowyResult<void, FlowyError>? createRootViewResult,
}) = _SidebarSectionsState;
factory SidebarSectionsState.initial() => const SidebarSectionsState(
section: SidebarSection.empty(),
);
}

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -10,7 +11,19 @@ part 'folder_bloc.freezed.dart';
enum FolderCategoryType {
favorite,
personal,
private,
public;
ViewSectionPB get toViewSectionPB {
switch (this) {
case FolderCategoryType.private:
return ViewSectionPB.Private;
case FolderCategoryType.public:
return ViewSectionPB.Public;
case FolderCategoryType.favorite:
throw UnimplementedError();
}
}
}
class FolderBloc extends Bloc<FolderEvent, FolderState> {

View File

@ -2,7 +2,7 @@ import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -20,14 +20,20 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
(event, emit) async {
await event.when(
initial: () async {
// do nothing
add(const FetchWorkspaces());
},
workspacesReceived: (workspaceId) async {},
fetchWorkspaces: () async {
final result = await _fetchWorkspaces();
if (result != null) {
final members = await _userService
.getWorkspaceMembers(
result.$1.workspaceId,
)
.fold((s) => s.items.length, (f) => -1);
emit(
state.copyWith(
isCollaborativeWorkspace: members > 1,
currentWorkspace: result.$1,
workspaces: result.$2,
),
@ -258,7 +264,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
workspaces.firstWhere((e) => e.workspaceId == currentWorkspace.id);
return (currentWorkspaceInList, workspaces);
} catch (e) {
Log.error(e);
Log.error('fetch workspace error: $e');
return null;
}
}
@ -292,6 +298,7 @@ class UserWorkspaceState with _$UserWorkspaceState {
const factory UserWorkspaceState({
required UserWorkspacePB? currentWorkspace,
required List<UserWorkspacePB> workspaces,
@Default(false) bool isCollaborativeWorkspace,
@Default(null) FlowyResult<void, FlowyError>? createWorkspaceResult,
@Default(null) FlowyResult<void, FlowyError>? deleteWorkspaceResult,
@Default(null) FlowyResult<void, FlowyError>? openWorkspaceResult,

View File

@ -165,6 +165,8 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
viewId: value.from.id,
newParentId: value.newParentId,
prevViewId: value.prevId,
fromSection: value.fromSection,
toSection: value.toSection,
);
emit(
result.fold(
@ -184,8 +186,8 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
layoutType: e.layoutType,
ext: {},
openAfterCreate: e.openAfterCreated,
section: e.section,
);
emit(
result.fold(
(view) => state.copyWith(
@ -353,12 +355,15 @@ class ViewEvent with _$ViewEvent {
ViewPB from,
String newParentId,
String? prevId,
ViewSectionPB? fromSection,
ViewSectionPB? toSection,
) = Move;
const factory ViewEvent.createView(
String name,
ViewLayoutPB layoutType, {
/// open the view after created
@Default(true) bool openAfterCreated,
required ViewSectionPB section,
}) = CreateView;
const factory ViewEvent.viewDidUpdate(
FlowyResult<ViewPB, FlowyError> result,

View File

@ -37,6 +37,7 @@ class ViewBackendService {
/// The [index] is the index of the view in the parent view.
/// If the index is null, the view will be added to the end of the list.
int? index,
ViewSectionPB? section,
}) {
final payload = CreateViewPayloadPB.create()
..parentViewId = parentViewId
@ -58,6 +59,10 @@ class ViewBackendService {
payload.index = index;
}
if (section != null) {
payload.section = section;
}
return FolderEventCreateView(payload).send();
}
@ -195,11 +200,15 @@ class ViewBackendService {
required String viewId,
required String newParentId,
required String? prevViewId,
ViewSectionPB? fromSection,
ViewSectionPB? toSection,
}) {
final payload = MoveNestedViewPayloadPB(
viewId: viewId,
newParentId: newParentId,
prevViewId: prevViewId,
fromSection: fromSection,
toSection: toSection,
);
return FolderEventMoveNestedView(payload).send();

View File

@ -11,23 +11,28 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart';
typedef AppListNotifyValue = FlowyResult<List<ViewPB>, FlowyError>;
typedef RootViewsNotifyValue = FlowyResult<List<ViewPB>, FlowyError>;
typedef WorkspaceNotifyValue = FlowyResult<WorkspacePB, FlowyError>;
/// The [WorkspaceListener] listens to the changes including the below:
///
/// - The root views of the workspace. (Not including the views are inside the root views)
/// - The workspace itself.
class WorkspaceListener {
WorkspaceListener({required this.user, required this.workspaceId});
final UserProfilePB user;
final String workspaceId;
PublishNotifier<AppListNotifyValue>? _appsChangedNotifier = PublishNotifier();
PublishNotifier<RootViewsNotifyValue>? _appsChangedNotifier =
PublishNotifier();
PublishNotifier<WorkspaceNotifyValue>? _workspaceUpdatedNotifier =
PublishNotifier();
FolderNotificationListener? _listener;
void start({
void Function(AppListNotifyValue)? appsChanged,
void Function(RootViewsNotifyValue)? appsChanged,
void Function(WorkspaceNotifyValue)? onWorkspaceUpdated,
}) {
if (appsChanged != null) {

View File

@ -0,0 +1,68 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy/core/notification/folder_notification.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB;
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart';
typedef SectionNotifyValue = FlowyResult<SectionViewsPB, FlowyError>;
/// The [WorkspaceSectionsListener] listens to the changes including the below:
///
/// - The root views inside different section of the workspace. (Not including the views are inside the root views)
/// depends on the section type(s).
class WorkspaceSectionsListener {
WorkspaceSectionsListener({
required this.user,
required this.workspaceId,
});
final UserProfilePB user;
final String workspaceId;
final _sectionNotifier = PublishNotifier<SectionNotifyValue>();
late final FolderNotificationListener _listener;
void start({
void Function(SectionNotifyValue)? sectionChanged,
}) {
if (sectionChanged != null) {
_sectionNotifier.addPublishListener(sectionChanged);
}
_listener = FolderNotificationListener(
objectId: workspaceId,
handler: _handleObservableType,
);
}
void _handleObservableType(
FolderNotification ty,
FlowyResult<Uint8List, FlowyError> result,
) {
switch (ty) {
case FolderNotification.DidUpdateSectionViews:
final FlowyResult<SectionViewsPB, FlowyError> value = result.fold(
(s) => FlowyResult.success(
SectionViewsPB.fromBuffer(s),
),
(f) => FlowyResult.failure(f),
);
_sectionNotifier.value = value;
break;
default:
break;
}
}
Future<void> stop() async {
_sectionNotifier.dispose();
await _listener.stop();
}
}

View File

@ -2,9 +2,7 @@ import 'dart:async';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'
show CreateViewPayloadPB, MoveViewPayloadPB, ViewLayoutPB, ViewPB;
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
class WorkspaceService {
@ -12,15 +10,18 @@ class WorkspaceService {
final String workspaceId;
Future<FlowyResult<ViewPB, FlowyError>> createApp({
Future<FlowyResult<ViewPB, FlowyError>> createView({
required String name,
required ViewSectionPB viewSection,
String? desc,
int? index,
}) {
final payload = CreateViewPayloadPB.create()
..parentViewId = workspaceId
..name = name
..layout = ViewLayoutPB.Document;
// only allow document layout for the top-level views
..layout = ViewLayoutPB.Document
..section = viewSection;
if (desc != null) {
payload.desc = desc;
@ -37,8 +38,8 @@ class WorkspaceService {
return FolderEventReadCurrentWorkspace().send();
}
Future<FlowyResult<List<ViewPB>, FlowyError>> getViews() {
final payload = WorkspaceIdPB.create()..value = workspaceId;
Future<FlowyResult<List<ViewPB>, FlowyError>> getPublicViews() {
final payload = GetWorkspaceViewPB.create()..value = workspaceId;
return FolderEventReadWorkspaceViews(payload).send().then((result) {
return result.fold(
(views) => FlowyResult.success(views.items),
@ -47,13 +48,23 @@ class WorkspaceService {
});
}
Future<FlowyResult<void, FlowyError>> moveApp({
required String appId,
Future<FlowyResult<List<ViewPB>, FlowyError>> getPrivateViews() {
final payload = GetWorkspaceViewPB.create()..value = workspaceId;
return FolderEventReadPrivateViews(payload).send().then((result) {
return result.fold(
(views) => FlowyResult.success(views.items),
(error) => FlowyResult.failure(error),
);
});
}
Future<FlowyResult<void, FlowyError>> moveView({
required String viewId,
required int fromIndex,
required int toIndex,
}) {
final payload = MoveViewPayloadPB.create()
..viewId = appId
..viewId = viewId
..from = fromIndex
..to = toIndex;