mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: show block in notification item (#3736)
* feat: show block in notification item * feat: jump to block action * fix: conflict after merge main * fix: missing import after merge main
This commit is contained in:
parent
aa27c4e6d4
commit
74d9d427bd
@ -130,7 +130,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
final result = await UserBackendService.getCurrentUserProfile().then(
|
final result = await UserBackendService.getCurrentUserProfile().then(
|
||||||
(value) async => value.andThen(
|
(value) async => value.andThen(
|
||||||
// open the document
|
// open the document
|
||||||
await _documentService.openDocument(view: view),
|
await _documentService.openDocument(viewId: view.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return state.copyWith(
|
return state.copyWith(
|
||||||
|
@ -9,7 +9,7 @@ class DocumentService {
|
|||||||
Future<Either<FlowyError, Unit>> createDocument({
|
Future<Either<FlowyError, Unit>> createDocument({
|
||||||
required ViewPB view,
|
required ViewPB view,
|
||||||
}) async {
|
}) async {
|
||||||
final canOpen = await openDocument(view: view);
|
final canOpen = await openDocument(viewId: view.id);
|
||||||
if (canOpen.isRight()) {
|
if (canOpen.isRight()) {
|
||||||
return const Right(unit);
|
return const Right(unit);
|
||||||
}
|
}
|
||||||
@ -19,13 +19,30 @@ class DocumentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<FlowyError, DocumentDataPB>> openDocument({
|
Future<Either<FlowyError, DocumentDataPB>> openDocument({
|
||||||
required ViewPB view,
|
required String viewId,
|
||||||
}) async {
|
}) async {
|
||||||
final payload = OpenDocumentPayloadPB()..documentId = view.id;
|
final payload = OpenDocumentPayloadPB()..documentId = viewId;
|
||||||
final result = await DocumentEventOpenDocument(payload).send();
|
final result = await DocumentEventOpenDocument(payload).send();
|
||||||
return result.swap();
|
return result.swap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Either<FlowyError, BlockPB>> getBlockFromDocument({
|
||||||
|
required DocumentDataPB document,
|
||||||
|
required String blockId,
|
||||||
|
}) async {
|
||||||
|
final block = document.blocks[blockId];
|
||||||
|
|
||||||
|
if (block != null) {
|
||||||
|
return right(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
return left(
|
||||||
|
FlowyError(
|
||||||
|
msg: 'Block($blockId) not found in Document(${document.pageId})',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Either<FlowyError, Unit>> closeDocument({
|
Future<Either<FlowyError, Unit>> closeDocument({
|
||||||
required ViewPB view,
|
required ViewPB view,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -10,6 +10,8 @@ import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
|||||||
import 'package:appflowy/plugins/document/presentation/export_page_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/export_page_widget.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/util/base64_string.dart';
|
import 'package:appflowy/util/base64_string.dart';
|
||||||
|
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
|
||||||
|
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'
|
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'
|
||||||
@ -60,39 +62,45 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider.value(
|
return MultiBlocProvider(
|
||||||
value: documentBloc,
|
providers: [
|
||||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
BlocProvider.value(value: getIt<NotificationActionBloc>()),
|
||||||
builder: (context, state) {
|
BlocProvider.value(value: documentBloc),
|
||||||
return state.loadingState.when(
|
],
|
||||||
loading: () =>
|
child: BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||||
const Center(child: CircularProgressIndicator.adaptive()),
|
listener: _onNotificationAction,
|
||||||
finish: (result) => result.fold(
|
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||||
(error) {
|
builder: (context, state) {
|
||||||
Log.error(error);
|
return state.loadingState.when(
|
||||||
return FlowyErrorPage.message(
|
loading: () =>
|
||||||
error.toString(),
|
const Center(child: CircularProgressIndicator.adaptive()),
|
||||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
finish: (result) => result.fold(
|
||||||
);
|
(error) {
|
||||||
},
|
Log.error(error);
|
||||||
(data) {
|
return FlowyErrorPage.message(
|
||||||
if (state.forceClose) {
|
error.toString(),
|
||||||
widget.onDeleted();
|
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||||
return const SizedBox.shrink();
|
|
||||||
} else if (documentBloc.editorState == null) {
|
|
||||||
return Center(
|
|
||||||
child: ExportPageWidget(
|
|
||||||
onTap: () async => await _exportPage(data),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else {
|
},
|
||||||
editorState = documentBloc.editorState!;
|
(data) {
|
||||||
return _buildEditorPage(context, state);
|
if (state.forceClose) {
|
||||||
}
|
widget.onDeleted();
|
||||||
},
|
return const SizedBox.shrink();
|
||||||
),
|
} else if (documentBloc.editorState == null) {
|
||||||
);
|
return Center(
|
||||||
},
|
child: ExportPageWidget(
|
||||||
|
onTap: () async => await _exportPage(data),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
editorState = documentBloc.editorState!;
|
||||||
|
return _buildEditorPage(context, state);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -112,9 +120,7 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
if (state.isDeleted) _buildBanner(context),
|
if (state.isDeleted) _buildBanner(context),
|
||||||
Expanded(
|
Expanded(child: appflowyEditorPage),
|
||||||
child: appflowyEditorPage,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -151,4 +157,22 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
showSnackBarMessage(context, 'Export success to $path');
|
showSnackBarMessage(context, 'Export success to $path');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onNotificationAction(
|
||||||
|
BuildContext context,
|
||||||
|
NotificationActionState state,
|
||||||
|
) async {
|
||||||
|
if (state.action != null && state.action!.type == ActionType.jumpToBlock) {
|
||||||
|
final path = state.action?.arguments?[ActionArgumentKeys.nodePath.name];
|
||||||
|
|
||||||
|
if (editorState != null && widget.view.id == state.action?.objectId) {
|
||||||
|
editorState!.updateSelectionWithReason(
|
||||||
|
Selection.collapsed(
|
||||||
|
Position(path: [path]),
|
||||||
|
),
|
||||||
|
reason: SelectionUpdateReason.transaction,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,231 @@
|
|||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
||||||
|
required BuildContext context,
|
||||||
|
required EditorState editorState,
|
||||||
|
required EditorStyleCustomizer styleCustomizer,
|
||||||
|
List<SelectionMenuItem>? slashMenuItems,
|
||||||
|
bool editable = true,
|
||||||
|
}) {
|
||||||
|
final standardActions = [
|
||||||
|
OptionAction.delete,
|
||||||
|
OptionAction.duplicate,
|
||||||
|
// OptionAction.divider,
|
||||||
|
// OptionAction.moveUp,
|
||||||
|
// OptionAction.moveDown,
|
||||||
|
];
|
||||||
|
|
||||||
|
final calloutBGColor = AFThemeExtension.of(context).calloutBGColor;
|
||||||
|
|
||||||
|
final configuration = BlockComponentConfiguration(
|
||||||
|
padding: (_) => const EdgeInsets.symmetric(vertical: 5.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
final customBlockComponentBuilderMap = {
|
||||||
|
PageBlockKeys.type: PageBlockComponentBuilder(),
|
||||||
|
ParagraphBlockKeys.type: ParagraphBlockComponentBuilder(
|
||||||
|
configuration: configuration,
|
||||||
|
),
|
||||||
|
TodoListBlockKeys.type: TodoListBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
placeholderText: (_) => 'To-do',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
placeholderText: (_) => 'List',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
placeholderText: (_) => 'List',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
QuoteBlockKeys.type: QuoteBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
placeholderText: (_) => 'Quote',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
HeadingBlockKeys.type: HeadingBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
|
||||||
|
placeholderText: (node) =>
|
||||||
|
'Heading ${node.attributes[HeadingBlockKeys.level]}',
|
||||||
|
),
|
||||||
|
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
|
||||||
|
),
|
||||||
|
ImageBlockKeys.type: CustomImageBlockComponentBuilder(
|
||||||
|
configuration: configuration,
|
||||||
|
showMenu: true,
|
||||||
|
menuBuilder: (Node node, CustomImageBlockComponentState state) =>
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 10,
|
||||||
|
child: ImageMenu(
|
||||||
|
node: node,
|
||||||
|
state: state,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TableBlockKeys.type: TableBlockComponentBuilder(
|
||||||
|
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
||||||
|
TableMenu(
|
||||||
|
node: node,
|
||||||
|
editorState: editorState,
|
||||||
|
position: position,
|
||||||
|
dir: dir,
|
||||||
|
onBuild: onBuild,
|
||||||
|
onClose: onClose,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TableCellBlockKeys.type: TableCellBlockComponentBuilder(
|
||||||
|
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
||||||
|
TableMenu(
|
||||||
|
node: node,
|
||||||
|
editorState: editorState,
|
||||||
|
position: position,
|
||||||
|
dir: dir,
|
||||||
|
onBuild: onBuild,
|
||||||
|
onClose: onClose,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DatabaseBlockKeys.boardType: DatabaseViewBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DatabaseBlockKeys.calendarType: DatabaseViewBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
|
||||||
|
configuration: configuration,
|
||||||
|
defaultColor: calloutBGColor,
|
||||||
|
),
|
||||||
|
DividerBlockKeys.type: DividerBlockComponentBuilder(
|
||||||
|
configuration: configuration,
|
||||||
|
height: 28.0,
|
||||||
|
),
|
||||||
|
MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
padding: (_) => const EdgeInsets.symmetric(vertical: 20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CodeBlockKeys.type: CodeBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||||
|
placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 30,
|
||||||
|
right: 30,
|
||||||
|
bottom: 36,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
|
||||||
|
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
|
||||||
|
ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(
|
||||||
|
configuration: configuration,
|
||||||
|
),
|
||||||
|
OutlineBlockKeys.type: OutlineBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
placeholderTextStyle: (_) =>
|
||||||
|
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
final builders = {
|
||||||
|
...standardBlockComponentBuilderMap,
|
||||||
|
...customBlockComponentBuilderMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editable) {
|
||||||
|
// customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience.
|
||||||
|
for (final entry in builders.entries) {
|
||||||
|
if (entry.key == PageBlockKeys.type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final builder = entry.value;
|
||||||
|
|
||||||
|
// customize the action builder.
|
||||||
|
final supportColorBuilderTypes = [
|
||||||
|
ParagraphBlockKeys.type,
|
||||||
|
HeadingBlockKeys.type,
|
||||||
|
BulletedListBlockKeys.type,
|
||||||
|
NumberedListBlockKeys.type,
|
||||||
|
QuoteBlockKeys.type,
|
||||||
|
TodoListBlockKeys.type,
|
||||||
|
CalloutBlockKeys.type,
|
||||||
|
OutlineBlockKeys.type,
|
||||||
|
ToggleListBlockKeys.type,
|
||||||
|
];
|
||||||
|
|
||||||
|
final supportAlignBuilderType = [
|
||||||
|
ImageBlockKeys.type,
|
||||||
|
];
|
||||||
|
|
||||||
|
final colorAction = [
|
||||||
|
OptionAction.divider,
|
||||||
|
OptionAction.color,
|
||||||
|
];
|
||||||
|
|
||||||
|
final alignAction = [
|
||||||
|
OptionAction.divider,
|
||||||
|
OptionAction.align,
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<OptionAction> actions = [
|
||||||
|
...standardActions,
|
||||||
|
if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
|
||||||
|
if (supportAlignBuilderType.contains(entry.key)) ...alignAction,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (PlatformExtension.isDesktop) {
|
||||||
|
builder.showActions =
|
||||||
|
(node) => node.parent?.type != TableCellBlockKeys.type;
|
||||||
|
|
||||||
|
builder.actionBuilder = (context, state) {
|
||||||
|
final top = builder.configuration.padding(context.node).top;
|
||||||
|
final padding = context.node.type == HeadingBlockKeys.type
|
||||||
|
? EdgeInsets.only(top: top + 8.0)
|
||||||
|
: EdgeInsets.only(top: top + 2.0);
|
||||||
|
return Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: BlockActionList(
|
||||||
|
blockComponentContext: context,
|
||||||
|
blockComponentState: state,
|
||||||
|
editorState: editorState,
|
||||||
|
actions: actions,
|
||||||
|
showSlashMenu: slashMenuItems != null
|
||||||
|
? () => customSlashCommand(
|
||||||
|
slashMenuItems,
|
||||||
|
shouldInsertSlash: false,
|
||||||
|
style: styleCustomizer.selectionMenuStyleBuilder(),
|
||||||
|
).handler.call(editorState)
|
||||||
|
: () {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builders;
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||||
@ -10,6 +10,9 @@ import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.d
|
|||||||
import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart';
|
import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart';
|
||||||
import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart';
|
import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart';
|
||||||
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
|
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
|
||||||
|
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||||
@ -104,7 +107,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
late final List<SelectionMenuItem> slashMenuItems;
|
late final List<SelectionMenuItem> slashMenuItems;
|
||||||
|
|
||||||
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
|
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
|
||||||
_customAppFlowyBlockComponentBuilders();
|
getEditorBuilderMap(
|
||||||
|
context: context,
|
||||||
|
editorState: widget.editorState,
|
||||||
|
styleCustomizer: widget.styleCustomizer,
|
||||||
|
);
|
||||||
|
|
||||||
List<CharacterShortcutEvent> get characterShortcutEvents => [
|
List<CharacterShortcutEvent> get characterShortcutEvents => [
|
||||||
// code block
|
// code block
|
||||||
@ -227,6 +234,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final editorState = widget.editorState;
|
final editorState = widget.editorState;
|
||||||
|
_setInitialSelection(editorScrollController);
|
||||||
|
|
||||||
if (PlatformExtension.isMobile) {
|
if (PlatformExtension.isMobile) {
|
||||||
return Column(
|
return Column(
|
||||||
@ -280,216 +288,19 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, BlockComponentBuilder> _customAppFlowyBlockComponentBuilders() {
|
void _setInitialSelection(EditorScrollController scrollController) {
|
||||||
final standardActions = [
|
final action = getIt<NotificationActionBloc>().state.action;
|
||||||
OptionAction.delete,
|
final viewId = action?.objectId;
|
||||||
OptionAction.duplicate,
|
final nodePath =
|
||||||
// OptionAction.divider,
|
action?.arguments?[ActionArgumentKeys.nodePath.name] as int?;
|
||||||
// OptionAction.moveUp,
|
|
||||||
// OptionAction.moveDown,
|
|
||||||
];
|
|
||||||
|
|
||||||
const calloutBGColor = Colors.black;
|
if (viewId != null && viewId == documentBloc.view.id && nodePath != null) {
|
||||||
// AFThemeExtension.of(context).calloutBGColor;
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
scrollController.itemScrollController.jumpTo(index: nodePath);
|
||||||
final configuration = BlockComponentConfiguration(
|
widget.editorState.selection =
|
||||||
padding: (_) => const EdgeInsets.symmetric(vertical: 5.0),
|
Selection.collapsed(Position(path: [nodePath]));
|
||||||
);
|
});
|
||||||
|
|
||||||
final customBlockComponentBuilderMap = {
|
|
||||||
PageBlockKeys.type: PageBlockComponentBuilder(),
|
|
||||||
ParagraphBlockKeys.type: ParagraphBlockComponentBuilder(
|
|
||||||
configuration: configuration,
|
|
||||||
),
|
|
||||||
TodoListBlockKeys.type: TodoListBlockComponentBuilder(
|
|
||||||
configuration: configuration.copyWith(
|
|
||||||
placeholderText: (_) => 'To-do',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
|
|
||||||
configuration: configuration.copyWith(
|
|
||||||
placeholderText: (_) => 'List',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
|
|
||||||
configuration: configuration.copyWith(
|
|
||||||
placeholderText: (_) => 'List',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
QuoteBlockKeys.type: QuoteBlockComponentBuilder(
|
|
||||||
configuration: configuration.copyWith(
|
|
||||||
placeholderText: (_) => 'Quote',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
HeadingBlockKeys.type: HeadingBlockComponentBuilder(
|
|
||||||
configuration: configuration.copyWith(
|
|
||||||
padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
|
|
||||||
placeholderText: (node) =>
|
|
||||||
'Heading ${node.attributes[HeadingBlockKeys.level]}',
|
|
||||||
),
|
|
||||||
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
|
|
||||||
),
|
|
||||||
ImageBlockKeys.type: CustomImageBlockComponentBuilder(
|
|
||||||
configuration: configuration,
|
|
||||||
showMenu: true,
|
|
||||||
menuBuilder: (Node node, CustomImageBlockComponentState state) =>
|
|
||||||
Positioned(
|
|
||||||
top: 0,
|
|
||||||
right: 10,
|
|
||||||
child: ImageMenu(
|
|
||||||
node: node,
|
|
||||||
state: state,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TableBlockKeys.type: TableBlockComponentBuilder(
|
|
||||||
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
|
||||||
TableMenu(
|
|
||||||
node: node,
|
|
||||||
editorState: editorState,
|
|
||||||
position: position,
|
|
||||||
dir: dir,
|
|
||||||
onBuild: onBuild,
|
|
||||||
onClose: onClose,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TableCellBlockKeys.type: TableCellBlockComponentBuilder(
|
|
||||||
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
|
||||||
TableMenu(
|
|
||||||
node: node,
|
|
||||||
editorState: editorState,
|
|
||||||
position: position,
|
|
||||||
dir: dir,
|
|
||||||
onBuild: onBuild,
|
|
||||||
onClose: onClose,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
|
|
||||||
configuration: configuration.copyWith(
|
|
||||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DatabaseBlockKeys.boardType: DatabaseViewBlockComponentBuilder(
|
|
||||||
configuration: configuration.copyWith(
|
|
||||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DatabaseBlockKeys.calendarType: DatabaseViewBlockComponentBuilder(
|
|
||||||
configuration: configuration.copyWith(
|
|
||||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
|
|
||||||
configuration: configuration,
|
|
||||||
defaultColor: calloutBGColor,
|
|
||||||
),
|
|
||||||
DividerBlockKeys.type: DividerBlockComponentBuilder(
|
|
||||||
configuration: configuration,
|
|
||||||
height: 28.0,
|
|
||||||
),
|
|
||||||
MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
|
|
||||||
configuration: configuration.copyWith(
|
|
||||||
padding: (_) => const EdgeInsets.symmetric(vertical: 20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CodeBlockKeys.type: CodeBlockComponentBuilder(
|
|
||||||
configuration: configuration.copyWith(
|
|
||||||
textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
|
||||||
placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 30,
|
|
||||||
right: 30,
|
|
||||||
bottom: 36,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
|
|
||||||
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
|
|
||||||
ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(
|
|
||||||
configuration: configuration,
|
|
||||||
),
|
|
||||||
OutlineBlockKeys.type: OutlineBlockComponentBuilder(
|
|
||||||
configuration: configuration.copyWith(
|
|
||||||
placeholderTextStyle: (_) =>
|
|
||||||
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
|
|
||||||
configuration: configuration.copyWith(
|
|
||||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
final builders = {
|
|
||||||
...standardBlockComponentBuilderMap,
|
|
||||||
...customBlockComponentBuilderMap,
|
|
||||||
};
|
|
||||||
|
|
||||||
// customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience.
|
|
||||||
for (final entry in builders.entries) {
|
|
||||||
if (entry.key == PageBlockKeys.type) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final builder = entry.value;
|
|
||||||
|
|
||||||
// customize the action builder.
|
|
||||||
final supportColorBuilderTypes = [
|
|
||||||
ParagraphBlockKeys.type,
|
|
||||||
HeadingBlockKeys.type,
|
|
||||||
BulletedListBlockKeys.type,
|
|
||||||
NumberedListBlockKeys.type,
|
|
||||||
QuoteBlockKeys.type,
|
|
||||||
TodoListBlockKeys.type,
|
|
||||||
CalloutBlockKeys.type,
|
|
||||||
OutlineBlockKeys.type,
|
|
||||||
ToggleListBlockKeys.type,
|
|
||||||
];
|
|
||||||
|
|
||||||
final supportAlignBuilderType = [
|
|
||||||
ImageBlockKeys.type,
|
|
||||||
];
|
|
||||||
|
|
||||||
final colorAction = [
|
|
||||||
OptionAction.divider,
|
|
||||||
OptionAction.color,
|
|
||||||
];
|
|
||||||
|
|
||||||
final alignAction = [
|
|
||||||
OptionAction.divider,
|
|
||||||
OptionAction.align,
|
|
||||||
];
|
|
||||||
|
|
||||||
final List<OptionAction> actions = [
|
|
||||||
...standardActions,
|
|
||||||
if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
|
|
||||||
if (supportAlignBuilderType.contains(entry.key)) ...alignAction,
|
|
||||||
];
|
|
||||||
|
|
||||||
// only show the ... and + button on the desktop platform.
|
|
||||||
if (PlatformExtension.isDesktop) {
|
|
||||||
builder.showActions =
|
|
||||||
(node) => node.parent?.type != TableCellBlockKeys.type;
|
|
||||||
builder.actionBuilder = (context, state) {
|
|
||||||
final top = builder.configuration.padding(context.node).top;
|
|
||||||
final padding = context.node.type == HeadingBlockKeys.type
|
|
||||||
? EdgeInsets.only(top: top + 8.0)
|
|
||||||
: EdgeInsets.only(top: top + 2.0);
|
|
||||||
return Padding(
|
|
||||||
padding: padding,
|
|
||||||
child: BlockActionList(
|
|
||||||
blockComponentContext: context,
|
|
||||||
blockComponentState: state,
|
|
||||||
editorState: widget.editorState,
|
|
||||||
actions: actions,
|
|
||||||
showSlashMenu: () => showSlashMenu(widget.editorState),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return builders;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SelectionMenuItem> _customSlashMenuItems() {
|
List<SelectionMenuItem> _customSlashMenuItems() {
|
||||||
|
@ -39,6 +39,8 @@ class MentionDateBlock extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final editorState = context.read<EditorState>();
|
||||||
|
|
||||||
DateTime? parsedDate = DateTime.tryParse(date);
|
DateTime? parsedDate = DateTime.tryParse(date);
|
||||||
if (parsedDate == null) {
|
if (parsedDate == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
@ -108,10 +110,12 @@ class MentionDateBlock extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTapDown: (details) => DatePickerMenu(
|
onTapDown: editorState.editable
|
||||||
context: context,
|
? (details) => DatePickerMenu(
|
||||||
editorState: context.read<EditorState>(),
|
context: context,
|
||||||
).show(details.globalPosition, options: options),
|
editorState: context.read<EditorState>(),
|
||||||
|
).show(details.globalPosition, options: options)
|
||||||
|
: null,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
|
@ -137,9 +137,7 @@ class ReminderReferenceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final viewId = context.read<DocumentBloc>().view.id;
|
final viewId = context.read<DocumentBloc>().view.id;
|
||||||
final reminder = _reminderFromDate(date, viewId);
|
final reminder = _reminderFromDate(date, viewId, node);
|
||||||
|
|
||||||
context.read<ReminderBloc>().add(ReminderEvent.add(reminder: reminder));
|
|
||||||
|
|
||||||
final transaction = editorState.transaction
|
final transaction = editorState.transaction
|
||||||
..replaceText(
|
..replaceText(
|
||||||
@ -157,6 +155,10 @@ class ReminderReferenceService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await editorState.apply(transaction);
|
await editorState.apply(transaction);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
context.read<ReminderBloc>().add(ReminderEvent.add(reminder: reminder));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setOptions() {
|
void _setOptions() {
|
||||||
@ -204,7 +206,7 @@ class ReminderReferenceService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ReminderPB _reminderFromDate(DateTime date, String viewId) {
|
ReminderPB _reminderFromDate(DateTime date, String viewId, Node node) {
|
||||||
return ReminderPB(
|
return ReminderPB(
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
objectId: viewId,
|
objectId: viewId,
|
||||||
@ -212,6 +214,7 @@ class ReminderReferenceService {
|
|||||||
message: LocaleKeys.reminderNotification_message.tr(),
|
message: LocaleKeys.reminderNotification_message.tr(),
|
||||||
meta: {
|
meta: {
|
||||||
ReminderMetaKeys.includeTime.name: false.toString(),
|
ReminderMetaKeys.includeTime.name: false.toString(),
|
||||||
|
ReminderMetaKeys.blockId.name: node.id,
|
||||||
},
|
},
|
||||||
scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),
|
scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),
|
||||||
isAck: date.isBefore(DateTime.now()),
|
isAck: date.isBefore(DateTime.now()),
|
||||||
|
@ -111,7 +111,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
pressReminder: (reminderId) {
|
pressReminder: (reminderId, path) {
|
||||||
final reminder =
|
final reminder =
|
||||||
state.reminders.firstWhereOrNull((r) => r.id == reminderId);
|
state.reminders.firstWhereOrNull((r) => r.id == reminderId);
|
||||||
|
|
||||||
@ -120,12 +120,22 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
add(
|
add(
|
||||||
ReminderEvent.update(ReminderUpdate(id: reminderId, isRead: true)),
|
ReminderEvent.update(
|
||||||
|
ReminderUpdate(
|
||||||
|
id: reminderId,
|
||||||
|
isRead: state.pastReminders.contains(reminder),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
actionBloc.add(
|
actionBloc.add(
|
||||||
NotificationActionEvent.performAction(
|
NotificationActionEvent.performAction(
|
||||||
action: NotificationAction(objectId: reminder.objectId),
|
action: NotificationAction(
|
||||||
|
objectId: reminder.objectId,
|
||||||
|
arguments: {
|
||||||
|
ActionArgumentKeys.nodePath.name: path,
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -191,8 +201,10 @@ class ReminderEvent with _$ReminderEvent {
|
|||||||
// Mark all unread reminders as read
|
// Mark all unread reminders as read
|
||||||
const factory ReminderEvent.markAllRead() = _MarkAllRead;
|
const factory ReminderEvent.markAllRead() = _MarkAllRead;
|
||||||
|
|
||||||
const factory ReminderEvent.pressReminder({required String reminderId}) =
|
const factory ReminderEvent.pressReminder({
|
||||||
_PressReminder;
|
required String reminderId,
|
||||||
|
@Default(null) int? path,
|
||||||
|
}) = _PressReminder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Object used to merge updates with
|
/// Object used to merge updates with
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
|
|
||||||
enum ReminderMetaKeys {
|
enum ReminderMetaKeys {
|
||||||
includeTime("include_time");
|
includeTime("include_time"),
|
||||||
|
blockId("block_id");
|
||||||
|
|
||||||
const ReminderMetaKeys(this.name);
|
const ReminderMetaKeys(this.name);
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class DocumentExporter {
|
|||||||
|
|
||||||
Future<Either<FlowyError, String>> export(DocumentExportType type) async {
|
Future<Either<FlowyError, String>> export(DocumentExportType type) async {
|
||||||
final documentService = DocumentService();
|
final documentService = DocumentService();
|
||||||
final result = await documentService.openDocument(view: view);
|
final result = await documentService.openDocument(viewId: view.id);
|
||||||
return result.fold((error) => left(error), (r) {
|
return result.fold((error) => left(error), (r) {
|
||||||
final document = r.toDocument();
|
final document = r.toDocument();
|
||||||
if (document == null) {
|
if (document == null) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
enum ActionType {
|
enum ActionType {
|
||||||
openView,
|
openView,
|
||||||
|
jumpToBlock,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [NotificationAction] is used to communicate with the
|
/// A [NotificationAction] is used to communicate with the
|
||||||
@ -10,10 +11,31 @@ enum ActionType {
|
|||||||
class NotificationAction {
|
class NotificationAction {
|
||||||
const NotificationAction({
|
const NotificationAction({
|
||||||
this.type = ActionType.openView,
|
this.type = ActionType.openView,
|
||||||
|
this.arguments,
|
||||||
required this.objectId,
|
required this.objectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ActionType type;
|
final ActionType type;
|
||||||
|
|
||||||
final String objectId;
|
final String objectId;
|
||||||
|
final Map<String, dynamic>? arguments;
|
||||||
|
|
||||||
|
NotificationAction copyWith({
|
||||||
|
ActionType? type,
|
||||||
|
String? objectId,
|
||||||
|
Map<String, dynamic>? arguments,
|
||||||
|
}) =>
|
||||||
|
NotificationAction(
|
||||||
|
type: type ?? this.type,
|
||||||
|
objectId: objectId ?? this.objectId,
|
||||||
|
arguments: arguments ?? this.arguments,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActionArgumentKeys {
|
||||||
|
nodePath('node_path');
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
const ActionArgumentKeys(this.name);
|
||||||
}
|
}
|
||||||
|
@ -135,3 +135,22 @@ extension ViewLayoutExtension on ViewLayoutPB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ViewFinder on List<ViewPB> {
|
||||||
|
ViewPB? findView(String id) {
|
||||||
|
for (final view in this) {
|
||||||
|
if (view.id == id) {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view.childViews.isNotEmpty) {
|
||||||
|
final v = view.childViews.findView(id);
|
||||||
|
if (v != null) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,7 +14,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
||||||
show UserProfilePB;
|
show UserProfilePB;
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
import 'package:flowy_infra_ui/widget/spacing.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';
|
||||||
@ -64,23 +63,7 @@ class HomeSideBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocListener<NotificationActionBloc, NotificationActionState>(
|
BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||||
listener: (context, state) {
|
listener: _onNotificationAction,
|
||||||
final action = state.action;
|
|
||||||
if (action != null) {
|
|
||||||
switch (action.type) {
|
|
||||||
case ActionType.openView:
|
|
||||||
final view = context
|
|
||||||
.read<MenuBloc>()
|
|
||||||
.state
|
|
||||||
.views
|
|
||||||
.firstWhereOrNull((view) => action.objectId == view.id);
|
|
||||||
|
|
||||||
if (view != null) {
|
|
||||||
context.read<TabsBloc>().openPlugin(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: Builder(
|
child: Builder(
|
||||||
@ -145,4 +128,31 @@ class HomeSideBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onNotificationAction(
|
||||||
|
BuildContext context,
|
||||||
|
NotificationActionState state,
|
||||||
|
) {
|
||||||
|
final action = state.action;
|
||||||
|
if (action != null) {
|
||||||
|
if (action.type == ActionType.openView) {
|
||||||
|
final view =
|
||||||
|
context.read<MenuBloc>().state.views.findView(action.objectId);
|
||||||
|
|
||||||
|
if (view != null) {
|
||||||
|
context.read<TabsBloc>().openPlugin(view);
|
||||||
|
|
||||||
|
final nodePath =
|
||||||
|
action.arguments?[ActionArgumentKeys.nodePath.name] as int?;
|
||||||
|
if (nodePath != null) {
|
||||||
|
context.read<NotificationActionBloc>().add(
|
||||||
|
NotificationActionEvent.performAction(
|
||||||
|
action: action.copyWith(type: ActionType.jumpToBlock),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,17 +121,9 @@ class _NotificationDialogState extends State<NotificationDialog>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAction(ReminderPB reminder) {
|
void _onAction(ReminderPB reminder, int? path) {
|
||||||
final view = widget.views.firstWhereOrNull(
|
|
||||||
(view) => view.id == reminder.objectId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (view == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_reminderBloc.add(
|
_reminderBloc.add(
|
||||||
ReminderEvent.pressReminder(reminderId: reminder.id),
|
ReminderEvent.pressReminder(reminderId: reminder.id, path: path),
|
||||||
);
|
);
|
||||||
|
|
||||||
widget.mutex.close();
|
widget.mutex.close();
|
||||||
|
@ -31,7 +31,7 @@ class NotificationButton extends StatelessWidget {
|
|||||||
child: AppFlowyPopover(
|
child: AppFlowyPopover(
|
||||||
mutex: mutex,
|
mutex: mutex,
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
constraints: const BoxConstraints(maxHeight: 250, maxWidth: 400),
|
constraints: const BoxConstraints(maxHeight: 250, maxWidth: 425),
|
||||||
windowPadding: EdgeInsets.zero,
|
windowPadding: EdgeInsets.zero,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
popupBuilder: (_) =>
|
popupBuilder: (_) =>
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
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/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.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';
|
||||||
@ -18,6 +22,8 @@ class NotificationItem extends StatefulWidget {
|
|||||||
required this.scheduled,
|
required this.scheduled,
|
||||||
required this.body,
|
required this.body,
|
||||||
required this.isRead,
|
required this.isRead,
|
||||||
|
this.path,
|
||||||
|
this.block,
|
||||||
this.includeTime = false,
|
this.includeTime = false,
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
this.onAction,
|
this.onAction,
|
||||||
@ -29,11 +35,20 @@ class NotificationItem extends StatefulWidget {
|
|||||||
final String title;
|
final String title;
|
||||||
final Int64 scheduled;
|
final Int64 scheduled;
|
||||||
final String body;
|
final String body;
|
||||||
|
final bool isRead;
|
||||||
|
final Future<int?>? path;
|
||||||
|
|
||||||
|
/// If [block] is provided, then [body] will be shown only if
|
||||||
|
/// [block] fails to fetch.
|
||||||
|
///
|
||||||
|
/// [block] is rendered as a result of a [FutureBuilder].
|
||||||
|
///
|
||||||
|
final Future<Node?>? block;
|
||||||
|
|
||||||
final bool includeTime;
|
final bool includeTime;
|
||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
final bool isRead;
|
|
||||||
|
|
||||||
final VoidCallback? onAction;
|
final void Function(int? path)? onAction;
|
||||||
final VoidCallback? onDelete;
|
final VoidCallback? onDelete;
|
||||||
final void Function(bool isRead)? onReadChanged;
|
final void Function(bool isRead)? onReadChanged;
|
||||||
|
|
||||||
@ -44,6 +59,13 @@ class NotificationItem extends StatefulWidget {
|
|||||||
class _NotificationItemState extends State<NotificationItem> {
|
class _NotificationItemState extends State<NotificationItem> {
|
||||||
final PopoverMutex mutex = PopoverMutex();
|
final PopoverMutex mutex = PopoverMutex();
|
||||||
bool _isHovering = false;
|
bool _isHovering = false;
|
||||||
|
int? path;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.path?.then((p) => path = p);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -56,61 +78,110 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: widget.onAction,
|
onTap: () => widget.onAction?.call(path),
|
||||||
child: Opacity(
|
child: AbsorbPointer(
|
||||||
opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,
|
child: Opacity(
|
||||||
child: DecoratedBox(
|
opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,
|
||||||
decoration: BoxDecoration(
|
child: DecoratedBox(
|
||||||
color: _isHovering && widget.onAction != null
|
decoration: BoxDecoration(
|
||||||
? AFThemeExtension.of(context).lightGreyHover
|
color: _isHovering && widget.onAction != null
|
||||||
: Colors.transparent,
|
? AFThemeExtension.of(context).lightGreyHover
|
||||||
border: widget.isRead || widget.readOnly
|
: Colors.transparent,
|
||||||
? null
|
border: widget.isRead || widget.readOnly
|
||||||
: Border(
|
? null
|
||||||
left: BorderSide(
|
: Border(
|
||||||
width: 2,
|
left: BorderSide(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
width: 2,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
FlowySvg(
|
||||||
|
FlowySvgs.time_s,
|
||||||
|
size: const Size.square(20),
|
||||||
|
color: Theme.of(context).colorScheme.tertiary,
|
||||||
|
),
|
||||||
|
const HSpace(16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
FlowyText.semibold(
|
||||||
|
widget.title,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).colorScheme.tertiary,
|
||||||
|
),
|
||||||
|
// TODO(Xazin): Relative time + View Name
|
||||||
|
FlowyText.regular(
|
||||||
|
_scheduledString(
|
||||||
|
widget.scheduled,
|
||||||
|
widget.includeTime,
|
||||||
|
),
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
const VSpace(5),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: Corners.s8Border,
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
child: FutureBuilder<Node?>(
|
||||||
|
future: widget.block,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasError ||
|
||||||
|
!snapshot.hasData ||
|
||||||
|
snapshot.data == null) {
|
||||||
|
return FlowyText.regular(
|
||||||
|
widget.body,
|
||||||
|
maxLines: 4,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final EditorState editorState = EditorState(
|
||||||
|
document: Document(root: snapshot.data!),
|
||||||
|
);
|
||||||
|
|
||||||
|
final EditorStyleCustomizer
|
||||||
|
styleCustomizer = EditorStyleCustomizer(
|
||||||
|
context: context,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Transform.scale(
|
||||||
|
scale: .9,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: AppFlowyEditor(
|
||||||
|
editorState: editorState,
|
||||||
|
editorStyle: styleCustomizer.style(),
|
||||||
|
editable: false,
|
||||||
|
shrinkWrap: true,
|
||||||
|
blockComponentBuilders:
|
||||||
|
getEditorBuilderMap(
|
||||||
|
context: context,
|
||||||
|
editorState: editorState,
|
||||||
|
styleCustomizer: styleCustomizer,
|
||||||
|
editable: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
child: Padding(
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 10,
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
FlowySvg(
|
|
||||||
FlowySvgs.time_s,
|
|
||||||
size: const Size.square(20),
|
|
||||||
color: Theme.of(context).colorScheme.tertiary,
|
|
||||||
),
|
|
||||||
const HSpace(16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
FlowyText.semibold(
|
|
||||||
widget.title,
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).colorScheme.tertiary,
|
|
||||||
),
|
|
||||||
// TODO(Xazin): Relative time + View Name
|
|
||||||
FlowyText.regular(
|
|
||||||
_scheduledString(
|
|
||||||
widget.scheduled,
|
|
||||||
widget.includeTime,
|
|
||||||
),
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
const VSpace(5),
|
|
||||||
FlowyText.regular(widget.body, maxLines: 4),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
|
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||||
|
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notifications_hub_empty.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notifications_hub_empty.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class NotificationsView extends StatelessWidget {
|
class NotificationsView extends StatelessWidget {
|
||||||
@ -23,7 +29,7 @@ class NotificationsView extends StatelessWidget {
|
|||||||
final ReminderBloc reminderBloc;
|
final ReminderBloc reminderBloc;
|
||||||
final List<ViewPB> views;
|
final List<ViewPB> views;
|
||||||
final bool isUpcoming;
|
final bool isUpcoming;
|
||||||
final Function(ReminderPB reminder)? onAction;
|
final Function(ReminderPB reminder, int? path)? onAction;
|
||||||
final Function(ReminderPB reminder)? onDelete;
|
final Function(ReminderPB reminder)? onDelete;
|
||||||
final Function(ReminderPB reminder, bool isRead)? onReadChanged;
|
final Function(ReminderPB reminder, bool isRead)? onReadChanged;
|
||||||
final Widget? actionBar;
|
final Widget? actionBar;
|
||||||
@ -47,19 +53,35 @@ class NotificationsView extends StatelessWidget {
|
|||||||
if (actionBar != null) actionBar!,
|
if (actionBar != null) actionBar!,
|
||||||
...shownReminders.map(
|
...shownReminders.map(
|
||||||
(ReminderPB reminder) {
|
(ReminderPB reminder) {
|
||||||
|
final blockId = reminder.meta[ReminderMetaKeys.blockId.name];
|
||||||
|
|
||||||
|
final documentService = DocumentService();
|
||||||
|
final documentFuture = documentService.openDocument(
|
||||||
|
viewId: reminder.objectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<Node?>? nodeBuilder;
|
||||||
|
Future<int?>? pathFinder;
|
||||||
|
if (blockId != null) {
|
||||||
|
nodeBuilder = _getNodeFromDocument(documentFuture, blockId);
|
||||||
|
pathFinder = _getPathFromDocument(documentFuture, blockId);
|
||||||
|
}
|
||||||
|
|
||||||
return NotificationItem(
|
return NotificationItem(
|
||||||
reminderId: reminder.id,
|
reminderId: reminder.id,
|
||||||
key: ValueKey(reminder.id),
|
key: ValueKey(reminder.id),
|
||||||
title: reminder.title,
|
title: reminder.title,
|
||||||
scheduled: reminder.scheduledAt,
|
scheduled: reminder.scheduledAt,
|
||||||
body: reminder.message,
|
body: reminder.message,
|
||||||
|
path: pathFinder,
|
||||||
|
block: nodeBuilder,
|
||||||
isRead: reminder.isRead,
|
isRead: reminder.isRead,
|
||||||
includeTime: reminder.includeTime ?? false,
|
includeTime: reminder.includeTime ?? false,
|
||||||
readOnly: isUpcoming,
|
readOnly: isUpcoming,
|
||||||
onReadChanged: (isRead) =>
|
onReadChanged: (isRead) =>
|
||||||
onReadChanged?.call(reminder, isRead),
|
onReadChanged?.call(reminder, isRead),
|
||||||
onDelete: () => onDelete?.call(reminder),
|
onDelete: () => onDelete?.call(reminder),
|
||||||
onAction: () => onAction?.call(reminder),
|
onAction: (path) => onAction?.call(reminder, path),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -67,4 +89,69 @@ class NotificationsView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Node?> _getNodeFromDocument(
|
||||||
|
Future<Either<FlowyError, DocumentDataPB>> documentFuture,
|
||||||
|
String blockId,
|
||||||
|
) async {
|
||||||
|
final document = (await documentFuture).fold(
|
||||||
|
(l) => null,
|
||||||
|
(document) => document,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (document == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final blockOrFailure = await DocumentService().getBlockFromDocument(
|
||||||
|
document: document,
|
||||||
|
blockId: blockId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return blockOrFailure.fold(
|
||||||
|
(_) => null,
|
||||||
|
(block) => block.toNode(meta: MetaPB()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int?> _getPathFromDocument(
|
||||||
|
Future<Either<FlowyError, DocumentDataPB>> documentFuture,
|
||||||
|
String blockId,
|
||||||
|
) async {
|
||||||
|
final document = (await documentFuture).fold(
|
||||||
|
(l) => null,
|
||||||
|
(document) => document,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (document == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final rootNode = document.toDocument()?.root;
|
||||||
|
if (rootNode == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _searchById(rootNode, blockId)?.path.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively iterates a [Node] and compares by its [id]
|
||||||
|
///
|
||||||
|
Node? _searchById(Node current, String id) {
|
||||||
|
if (current.id == id) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.children.isNotEmpty) {
|
||||||
|
for (final child in current.children) {
|
||||||
|
final node = _searchById(child, id);
|
||||||
|
|
||||||
|
if (node != null) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user