diff --git a/frontend/appflowy_flutter/lib/mobile/application/notification/notification_reminder_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/notification/notification_reminder_bloc.dart new file mode 100644 index 0000000000..8adfa58d0f --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/application/notification/notification_reminder_bloc.dart @@ -0,0 +1,194 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; +import 'package:appflowy/plugins/document/application/document_service.dart'; +import 'package:appflowy/user/application/reminder/reminder_extension.dart'; +import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; +import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:bloc/bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:time/time.dart'; + +part 'notification_reminder_bloc.freezed.dart'; + +class NotificationReminderBloc + extends Bloc { + NotificationReminderBloc() : super(NotificationReminderState.initial()) { + on((event, emit) async { + await event.when( + initial: (reminder, dateFormat, timeFormat) async { + this.reminder = reminder; + + final createdAt = await _getCreatedAt( + reminder, + dateFormat, + timeFormat, + ); + final view = await _getView(reminder); + final node = await _getContent(reminder); + + if (view == null || node == null) { + emit( + NotificationReminderState( + createdAt: createdAt, + pageTitle: '', + reminderContent: '', + status: NotificationReminderStatus.error, + ), + ); + } else { + emit( + NotificationReminderState( + createdAt: createdAt, + pageTitle: view.name, + view: view, + reminderContent: node.delta?.toPlainText() ?? '', + nodes: [node], + status: NotificationReminderStatus.loaded, + ), + ); + } + }, + reset: () {}, + ); + }); + } + + late final ReminderPB reminder; + + Future _getCreatedAt( + ReminderPB reminder, + UserDateFormatPB dateFormat, + UserTimeFormatPB timeFormat, + ) async { + final rCreatedAt = reminder.createdAt; + final createdAt = rCreatedAt != null + ? _formatTimestamp( + rCreatedAt, + timeFormat: timeFormat, + dateFormate: dateFormat, + ) + : ''; + return createdAt; + } + + Future _getView(ReminderPB reminder) async { + return ViewBackendService.getView(reminder.objectId) + .fold((s) => s, (_) => null); + } + + Future _getContent(ReminderPB reminder) async { + final blockId = reminder.meta[ReminderMetaKeys.blockId]; + + if (blockId == null) { + return null; + } + + final document = await DocumentService() + .openDocument( + documentId: reminder.objectId, + ) + .fold((s) => s.toDocument(), (_) => null); + + if (document == null) { + return null; + } + + final node = _searchById(document.root, blockId); + + if (node == null) { + return null; + } + + return node; + } + + 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; + } + + String _formatTimestamp( + int timestamp, { + required UserDateFormatPB dateFormate, + required UserTimeFormatPB timeFormat, + }) { + final now = DateTime.now(); + final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); + final difference = now.difference(dateTime); + final String date; + + if (difference.inMinutes < 1) { + date = LocaleKeys.sideBar_justNow.tr(); + } else if (difference.inHours < 1 && dateTime.isToday) { + // Less than 1 hour + date = LocaleKeys.sideBar_minutesAgo + .tr(namedArgs: {'count': difference.inMinutes.toString()}); + } else if (difference.inHours >= 1 && dateTime.isToday) { + // in same day + date = timeFormat.formatTime(dateTime); + } else { + date = dateFormate.formatDate(dateTime, false); + } + + return date; + } +} + +@freezed +class NotificationReminderEvent with _$NotificationReminderEvent { + const factory NotificationReminderEvent.initial( + ReminderPB reminder, + UserDateFormatPB dateFormat, + UserTimeFormatPB timeFormat, + ) = _Initial; + + const factory NotificationReminderEvent.reset() = _Reset; +} + +enum NotificationReminderStatus { + initial, + loading, + loaded, + error, +} + +@freezed +class NotificationReminderState with _$NotificationReminderState { + const NotificationReminderState._(); + + const factory NotificationReminderState({ + required String createdAt, + required String pageTitle, + required String reminderContent, + @Default(NotificationReminderStatus.initial) + NotificationReminderStatus status, + @Default([]) List nodes, + ViewPB? view, + }) = _NotificationReminderState; + + factory NotificationReminderState.initial() => + const NotificationReminderState( + createdAt: '', + pageTitle: '', + reminderContent: '', + ); +} diff --git a/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart index 20ab2326ca..4dde5cc102 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart @@ -149,7 +149,7 @@ class DocumentPageStyleBloc ) { double padding = switch (fontLayout) { PageStyleFontLayout.small => 1.0, - PageStyleFontLayout.normal => 2.0, + PageStyleFontLayout.normal => 1.0, PageStyleFontLayout.large => 4.0, }; switch (lineHeightLayout) { @@ -165,6 +165,16 @@ class DocumentPageStyleBloc return max(0, padding); } + double calculateIconScale( + PageStyleFontLayout fontLayout, + ) { + return switch (fontLayout) { + PageStyleFontLayout.small => 0.8, + PageStyleFontLayout.normal => 1.0, + PageStyleFontLayout.large => 1.2, + }; + } + PageStyleFontLayout _getSelectedFontLayout(Map layoutObject) { final fontLayout = layoutObject[ViewExtKeys.fontLayoutKey] ?? PageStyleFontLayout.normal.toString(); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart index fe7ea1e7e9..f8c9a0d3b1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart @@ -21,14 +21,14 @@ class MobileSpaceTabBar extends StatelessWidget { Widget build(BuildContext context) { final baseStyle = Theme.of(context).textTheme.bodyMedium; final labelStyle = baseStyle?.copyWith( - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w500, fontSize: 16.0, - height: 20 / 16, + height: 22.0 / 16.0, ); final unselectedLabelStyle = baseStyle?.copyWith( fontWeight: FontWeight.w400, fontSize: 15.0, - height: 20 / 15, + height: 22.0 / 15.0, ); return Container( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart index f2c22e00dc..a998adddc2 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart @@ -1,10 +1,13 @@ import 'dart:ui'; import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; final PropertyValueNotifier createNewPageNotifier = @@ -25,9 +28,9 @@ final _items = [ ), const BottomNavigationBarItem( label: _notificationLabel, - icon: FlowySvg(FlowySvgs.m_home_notification_m), - activeIcon: FlowySvg( - FlowySvgs.m_home_notification_m, + icon: _NotificationNavigationBarItemIcon(), + activeIcon: _NotificationNavigationBarItemIcon( + isActive: true, ), ), ]; @@ -107,3 +110,62 @@ class MobileBottomNavigationBar extends StatelessWidget { ); } } + +class _NotificationNavigationBarItemIcon extends StatelessWidget { + const _NotificationNavigationBarItemIcon({ + this.isActive = false, + }); + + final bool isActive; + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: getIt(), + child: BlocBuilder( + builder: (context, state) { + final hasUnreads = state.reminders.any( + (reminder) => !reminder.isRead, + ); + return Stack( + children: [ + isActive + ? const FlowySvg( + FlowySvgs.m_home_active_notification_m, + blendMode: null, + ) + : const FlowySvg( + FlowySvgs.m_home_notification_m, + ), + if (hasUnreads) + const Positioned( + top: 2, + right: 4, + child: _RedDot(), + ), + ], + ); + }, + ), + ); + } +} + +class _RedDot extends StatelessWidget { + const _RedDot(); + + @override + Widget build(BuildContext context) { + return Container( + width: 6, + height: 6, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: const Color(0xFFFF2214), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_screen.dart new file mode 100644 index 0000000000..ba0fae9fa8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_screen.dart @@ -0,0 +1,119 @@ +import 'package:appflowy/mobile/application/user_profile/user_profile_bloc.dart'; +import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart'; +import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileNotificationsScreenV2 extends StatefulWidget { + const MobileNotificationsScreenV2({super.key}); + + static const routeName = '/notifications'; + + @override + State createState() => + _MobileNotificationsScreenV2State(); +} + +class _MobileNotificationsScreenV2State + extends State + with SingleTickerProviderStateMixin { + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + UserProfileBloc()..add(const UserProfileEvent.started()), + ), + BlocProvider.value(value: getIt()), + BlocProvider( + create: (_) => NotificationFilterBloc(), + ), + ], + child: BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () => + const Center(child: CircularProgressIndicator.adaptive()), + workspaceFailure: () => const WorkspaceFailedScreen(), + success: (workspaceSetting, userProfile) => + const MobileNotificationsTab(), + ); + }, + ), + ); + } +} + +class MobileNotificationsTab extends StatefulWidget { + const MobileNotificationsTab({ + super.key, + // required this.userProfile, + }); + + // final UserProfilePB userProfile; + + @override + State createState() => _MobileNotificationsTabState(); +} + +class _MobileNotificationsTabState extends State + with SingleTickerProviderStateMixin { + late TabController tabController; + + final tabs = [ + MobileNotificationTabType.inbox, + MobileNotificationTabType.unread, + MobileNotificationTabType.archive, + ]; + + @override + void initState() { + super.initState(); + + tabController = TabController( + length: 3, + vsync: this, + ); + tabController.addListener(_onTabChange); + } + + @override + void dispose() { + tabController.removeListener(_onTabChange); + tabController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MobileNotificationPageHeader(), + MobileNotificationTabBar( + tabController: tabController, + tabs: tabs, + ), + const VSpace(12.0), + Expanded( + child: TabBarView( + controller: tabController, + children: tabs.map((e) => NotificationTab(tabType: e)).toList(), + ), + ), + ], + ), + ), + ); + } + + void _onTabChange() {} +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/color.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/color.dart new file mode 100644 index 0000000000..8a59336378 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/color.dart @@ -0,0 +1,11 @@ +import 'package:appflowy/util/theme_extension.dart'; +import 'package:flutter/material.dart'; + +extension NotificationItemColors on BuildContext { + Color get notificationItemTextColor { + if (Theme.of(this).isLightMode) { + return const Color(0xFF171717); + } + return const Color(0xFFffffff).withOpacity(0.8); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/empty.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/empty.dart new file mode 100644 index 0000000000..e5598cc6e5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/empty.dart @@ -0,0 +1,58 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class EmptyNotification extends StatelessWidget { + const EmptyNotification({ + super.key, + required this.type, + }); + + final MobileNotificationTabType type; + + @override + Widget build(BuildContext context) { + final title = switch (type) { + MobileNotificationTabType.inbox => + LocaleKeys.settings_notifications_emptyInbox_title.tr(), + MobileNotificationTabType.archive => + LocaleKeys.settings_notifications_emptyArchived_title.tr(), + MobileNotificationTabType.unread => + LocaleKeys.settings_notifications_emptyUnread_title.tr(), + }; + final desc = switch (type) { + MobileNotificationTabType.inbox => + LocaleKeys.settings_notifications_emptyInbox_description.tr(), + MobileNotificationTabType.archive => + LocaleKeys.settings_notifications_emptyArchived_description.tr(), + MobileNotificationTabType.unread => + LocaleKeys.settings_notifications_emptyUnread_description.tr(), + }; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const FlowySvg(FlowySvgs.m_empty_notification_xl), + const VSpace(12.0), + FlowyText( + title, + fontSize: 16.0, + figmaLineHeight: 24.0, + fontWeight: FontWeight.w500, + ), + const VSpace(4.0), + Opacity( + opacity: 0.45, + child: FlowyText( + desc, + fontSize: 15.0, + figmaLineHeight: 22.0, + fontWeight: FontWeight.w400, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/header.dart new file mode 100644 index 0000000000..567846fb9c --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/header.dart @@ -0,0 +1,32 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/notifications/widgets/settings_popup_menu.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class MobileNotificationPageHeader extends StatelessWidget { + const MobileNotificationPageHeader({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(minHeight: 56), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const HSpace(16.0), + FlowyText( + LocaleKeys.settings_notifications_titles_notifications.tr(), + fontSize: 20, + fontWeight: FontWeight.w600, + ), + const Spacer(), + const NotificationSettingsPopupMenu(), + const HSpace(16.0), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/notification_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/notification_item.dart new file mode 100644 index 0000000000..469e7bfeb1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/notification_item.dart @@ -0,0 +1,366 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart'; +import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; +import 'package:appflowy/mobile/presentation/base/gesture.dart'; +import 'package:appflowy/mobile/presentation/notifications/widgets/color.dart'; +import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'; +import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; + +class NotificationItem extends StatelessWidget { + const NotificationItem({ + super.key, + required this.tabType, + required this.reminder, + }); + + final MobileNotificationTabType tabType; + final ReminderPB reminder; + + @override + Widget build(BuildContext context) { + final settings = context.read().state; + final dateFormate = settings.dateFormat; + final timeFormate = settings.timeFormat; + return BlocProvider( + create: (context) => NotificationReminderBloc() + ..add( + NotificationReminderEvent.initial( + reminder, + dateFormate, + timeFormate, + ), + ), + child: BlocBuilder( + builder: (context, state) { + if (state.status == NotificationReminderStatus.loading || + state.status == NotificationReminderStatus.initial) { + return const SizedBox.shrink(); + } + + if (state.status == NotificationReminderStatus.error) { + // error handle. + return const SizedBox.shrink(); + } + + final child = Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: _SlidableNotificationItem( + tabType: tabType, + reminder: reminder, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const HSpace(8.0), + !reminder.isRead ? const _UnreadRedDot() : const HSpace(6.0), + const HSpace(4.0), + _NotificationIcon(reminder: reminder), + const HSpace(12.0), + Expanded( + child: _NotificationContent(reminder: reminder), + ), + ], + ), + ), + ); + + if (reminder.isRead) { + return child; + } + + return AnimatedGestureDetector( + scaleFactor: 0.99, + onTapUp: () => _onMarkAsRead(context), + child: child, + ); + }, + ), + ); + } + + void _onMarkAsRead(BuildContext context) { + if (reminder.isRead) { + return; + } + + showToastNotification( + context, + message: LocaleKeys.settings_notifications_markAsReadNotifications_success + .tr(), + ); + + context.read().add( + ReminderEvent.update( + ReminderUpdate( + id: context.read().reminder.id, + isRead: true, + ), + ), + ); + } +} + +class _SlidableNotificationItem extends StatelessWidget { + const _SlidableNotificationItem({ + required this.tabType, + required this.reminder, + required this.child, + }); + + final MobileNotificationTabType tabType; + final ReminderPB reminder; + final Widget child; + + @override + Widget build(BuildContext context) { + // only show the actions in the inbox tab + + final List actions = switch (tabType) { + MobileNotificationTabType.inbox => [ + NotificationPaneActionType.more, + if (!reminder.isRead) NotificationPaneActionType.markAsRead, + ], + MobileNotificationTabType.unread => [ + NotificationPaneActionType.more, + NotificationPaneActionType.markAsRead, + ], + MobileNotificationTabType.archive => [ + if (kDebugMode) NotificationPaneActionType.unArchive, + ], + }; + + if (actions.isEmpty) { + return child; + } + + final children = actions + .map( + (action) => action.actionButton( + context, + tabType: tabType, + ), + ) + .toList(); + + final extentRatio = actions.length == 1 ? 1 / 5 : 1 / 3; + + return Slidable( + endActionPane: ActionPane( + motion: const ScrollMotion(), + extentRatio: extentRatio, + children: children, + ), + child: child, + ); + } +} + +const _kNotificationIconHeight = 36.0; + +class _NotificationIcon extends StatelessWidget { + const _NotificationIcon({ + required this.reminder, + }); + + final ReminderPB reminder; + + @override + Widget build(BuildContext context) { + return const FlowySvg( + FlowySvgs.m_notification_reminder_s, + size: Size.square(_kNotificationIconHeight), + blendMode: null, + ); + } +} + +class _UnreadRedDot extends StatelessWidget { + const _UnreadRedDot(); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: _kNotificationIconHeight, + child: Center( + child: SizedBox.square( + dimension: 6.0, + child: DecoratedBox( + decoration: ShapeDecoration( + color: Color(0xFFFF6331), + shape: OvalBorder(), + ), + ), + ), + ), + ); + } +} + +class _NotificationContent extends StatelessWidget { + const _NotificationContent({ + required this.reminder, + }); + + final ReminderPB reminder; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // title + _buildHeader(), + + // time & page name + _buildTimeAndPageName( + context, + state.createdAt, + state.pageTitle, + ), + + // content + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: IntrinsicHeight( + child: BlocProvider( + create: (context) => DocumentPageStyleBloc(view: state.view!), + child: _NotificationDocumentContent(nodes: state.nodes), + ), + ), + ), + ], + ); + }, + ); + } + + Widget _buildHeader() { + return FlowyText.semibold( + LocaleKeys.settings_notifications_titles_reminder.tr(), + fontSize: 14, + figmaLineHeight: 20, + ); + } + + Widget _buildTimeAndPageName( + BuildContext context, + String createdAt, + String pageTitle, + ) { + return Opacity( + opacity: 0.5, + child: Row( + children: [ + // the legacy reminder doesn't contain the timestamp, so we don't show it + if (createdAt.isNotEmpty) ...[ + FlowyText.regular( + createdAt, + fontSize: 12, + figmaLineHeight: 18, + color: context.notificationItemTextColor, + ), + const _Ellipse(), + ], + FlowyText.regular( + pageTitle, + fontSize: 12, + figmaLineHeight: 18, + color: context.notificationItemTextColor, + ), + ], + ), + ); + } +} + +class _Ellipse extends StatelessWidget { + const _Ellipse(); + + @override + Widget build(BuildContext context) { + return Container( + width: 2.50, + height: 2.50, + margin: const EdgeInsets.symmetric(horizontal: 5.0), + decoration: ShapeDecoration( + color: context.notificationItemTextColor, + shape: const OvalBorder(), + ), + ); + } +} + +class _NotificationDocumentContent extends StatelessWidget { + const _NotificationDocumentContent({ + required this.nodes, + }); + + final List nodes; + + @override + Widget build(BuildContext context) { + final editorState = EditorState( + document: Document( + root: pageNode(children: nodes), + ), + ); + + final styleCustomizer = EditorStyleCustomizer( + context: context, + padding: EdgeInsets.zero, + ); + + final editorStyle = styleCustomizer.style().copyWith( + // hide the cursor + cursorColor: Colors.transparent, + cursorWidth: 0, + textStyleConfiguration: TextStyleConfiguration( + lineHeight: 22 / 14, + applyHeightToFirstAscent: true, + applyHeightToLastDescent: true, + text: TextStyle( + fontSize: 14, + color: context.notificationItemTextColor, + height: 22 / 14, + fontWeight: FontWeight.w400, + leadingDistribution: TextLeadingDistribution.even, + ), + ), + ); + + final blockBuilders = getEditorBuilderMap( + context: context, + editorState: editorState, + styleCustomizer: styleCustomizer, + // the editor is not editable in the chat + editable: false, + customHeadingPadding: EdgeInsets.zero, + ); + + return AppFlowyEditor( + editorState: editorState, + editorStyle: editorStyle, + disableSelectionService: true, + disableKeyboardService: true, + disableScrollService: true, + editable: false, + shrinkWrap: true, + blockComponentBuilders: blockBuilders, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart new file mode 100644 index 0000000000..5ba72f6e7a --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart @@ -0,0 +1,139 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/presentation.dart'; +import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +enum _NotificationSettingsPopupMenuItem { + settings, + markAllAsRead, + archiveAll, +} + +class NotificationSettingsPopupMenu extends StatelessWidget { + const NotificationSettingsPopupMenu({super.key}); + + @override + Widget build(BuildContext context) { + return PopupMenuButton<_NotificationSettingsPopupMenuItem>( + offset: const Offset(0, 36), + padding: EdgeInsets.zero, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(12.0), + ), + ), + // todo: replace it with shadows + shadowColor: const Color(0x68000000), + elevation: 10, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: FlowySvg( + FlowySvgs.m_settings_more_s, + ), + ), + itemBuilder: (BuildContext context) => + >[ + _buildItem( + value: _NotificationSettingsPopupMenuItem.settings, + svg: FlowySvgs.m_notification_settings_s, + text: LocaleKeys.settings_notifications_settings_settings.tr(), + ), + const PopupMenuDivider(height: 0.5), + _buildItem( + value: _NotificationSettingsPopupMenuItem.markAllAsRead, + svg: FlowySvgs.m_notification_mark_as_read_s, + text: LocaleKeys.settings_notifications_settings_markAllAsRead.tr(), + ), + const PopupMenuDivider(height: 0.5), + _buildItem( + value: _NotificationSettingsPopupMenuItem.archiveAll, + svg: FlowySvgs.m_notification_archived_s, + text: LocaleKeys.settings_notifications_settings_archiveAll.tr(), + ), + ], + onSelected: (_NotificationSettingsPopupMenuItem value) { + switch (value) { + case _NotificationSettingsPopupMenuItem.markAllAsRead: + _onMarkAllAsRead(context); + break; + case _NotificationSettingsPopupMenuItem.archiveAll: + _onArchiveAll(context); + break; + case _NotificationSettingsPopupMenuItem.settings: + context.push(MobileHomeSettingPage.routeName); + break; + } + }, + ); + } + + PopupMenuItem _buildItem({ + required T value, + required FlowySvgData svg, + required String text, + }) { + return PopupMenuItem( + value: value, + padding: EdgeInsets.zero, + child: _PopupButton( + svg: svg, + text: text, + ), + ); + } + + void _onMarkAllAsRead(BuildContext context) { + showToastNotification( + context, + message: LocaleKeys + .settings_notifications_markAsReadNotifications_allSuccess + .tr(), + ); + + context.read().add(const ReminderEvent.markAllRead()); + } + + void _onArchiveAll(BuildContext context) { + showToastNotification( + context, + message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess + .tr(), + ); + + context.read().add(const ReminderEvent.archiveAll()); + } +} + +class _PopupButton extends StatelessWidget { + const _PopupButton({ + required this.svg, + required this.text, + }); + + final FlowySvgData svg; + final String text; + + @override + Widget build(BuildContext context) { + return Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + FlowySvg(svg), + const HSpace(12), + FlowyText.regular( + text, + fontSize: 16, + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart new file mode 100644 index 0000000000..85eb55ae74 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart @@ -0,0 +1,197 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'; +import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; +import 'package:appflowy/user/application/reminder/reminder_extension.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +enum NotificationPaneActionType { + more, + markAsRead, + // only used in the debug mode. + unArchive; + + MobileSlideActionButton actionButton( + BuildContext context, { + required MobileNotificationTabType tabType, + }) { + switch (this) { + case NotificationPaneActionType.markAsRead: + return MobileSlideActionButton( + backgroundColor: const Color(0xFF00C8FF), + svg: FlowySvgs.m_notification_action_mark_as_read_s, + size: 24.0, + onPressed: (context) { + showToastNotification( + context, + message: LocaleKeys + .settings_notifications_markAsReadNotifications_success + .tr(), + ); + + context.read().add( + ReminderEvent.update( + ReminderUpdate( + id: context.read().reminder.id, + isRead: true, + ), + ), + ); + }, + ); + // this action is only used in the debug mode. + case NotificationPaneActionType.unArchive: + return MobileSlideActionButton( + backgroundColor: const Color(0xFF00C8FF), + svg: FlowySvgs.m_notification_action_mark_as_read_s, + size: 24.0, + onPressed: (context) { + showToastNotification( + context, + message: 'Unarchive notification success', + ); + + context.read().add( + ReminderEvent.update( + ReminderUpdate( + id: context.read().reminder.id, + isArchived: false, + ), + ), + ); + }, + ); + case NotificationPaneActionType.more: + return MobileSlideActionButton( + backgroundColor: const Color(0xE5515563), + svg: FlowySvgs.three_dots_s, + size: 24.0, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10), + ), + onPressed: (context) { + final reminderBloc = context.read(); + final notificationReminderBloc = + context.read(); + + showMobileBottomSheet( + context, + showDragHandle: true, + showDivider: false, + useRootNavigator: true, + backgroundColor: Theme.of(context).colorScheme.surface, + builder: (context) { + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: reminderBloc), + BlocProvider.value(value: notificationReminderBloc), + ], + child: const _NotificationMoreActions(), + ); + }, + ); + }, + ); + } + } +} + +class _NotificationMoreActions extends StatelessWidget { + const _NotificationMoreActions(); + + @override + Widget build(BuildContext context) { + final reminder = context.read().reminder; + return Column( + children: [ + if (!reminder.isRead) + FlowyOptionTile.text( + height: 52.0, + text: LocaleKeys.settings_notifications_action_markAsRead.tr(), + leftIcon: const FlowySvg( + FlowySvgs.m_notification_action_mark_as_read_s, + size: Size.square(20), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => _onMarkAsRead(context), + ), + FlowyOptionTile.text( + height: 52.0, + text: LocaleKeys.settings_notifications_action_multipleChoice.tr(), + leftIcon: const FlowySvg( + FlowySvgs.m_notification_action_multiple_choice_s, + size: Size.square(20), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => _onMultipleChoice(context), + ), + if (!reminder.isArchived) + FlowyOptionTile.text( + height: 52.0, + text: LocaleKeys.settings_notifications_action_archive.tr(), + leftIcon: const FlowySvg( + FlowySvgs.m_notification_action_archive_s, + size: Size.square(20), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => _onArchive(context), + ), + ], + ); + } + + void _onMarkAsRead(BuildContext context) { + Navigator.of(context).pop(); + + showToastNotification( + context, + message: LocaleKeys.settings_notifications_markAsReadNotifications_success + .tr(), + ); + + context.read().add( + ReminderEvent.update( + ReminderUpdate( + id: context.read().reminder.id, + isRead: true, + ), + ), + ); + } + + void _onMultipleChoice(BuildContext context) { + Navigator.of(context).pop(); + } + + void _onArchive(BuildContext context) { + showToastNotification( + context, + message: LocaleKeys.settings_notifications_archiveNotifications_success + .tr() + .tr(), + ); + + context.read().add( + ReminderEvent.update( + ReminderUpdate( + id: context.read().reminder.id, + isRead: true, + isArchived: true, + ), + ), + ); + + Navigator.of(context).pop(); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart new file mode 100644 index 0000000000..6c24a035db --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart @@ -0,0 +1,96 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/notifications/widgets/notification_item.dart'; +import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'; +import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; +import 'package:appflowy/user/application/reminder/reminder_extension.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/appflowy_backend.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'; + +class NotificationTab extends StatefulWidget { + const NotificationTab({ + super.key, + required this.tabType, + }); + + final MobileNotificationTabType tabType; + + @override + State createState() => _NotificationTabState(); +} + +class _NotificationTabState extends State + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + + return BlocBuilder( + builder: (context, state) { + final reminders = _filterReminders(state.reminders); + + if (reminders.isEmpty) { + return EmptyNotification( + type: widget.tabType, + ); + } + + return RefreshIndicator.adaptive( + onRefresh: () async => _onRefresh(context), + child: ListView.separated( + itemCount: reminders.length, + separatorBuilder: (context, index) => const VSpace(8.0), + itemBuilder: (context, index) { + final reminder = reminders[index]; + return NotificationItem( + key: ValueKey('${widget.tabType}_${reminder.id}'), + tabType: widget.tabType, + reminder: reminder, + ); + }, + ), + ); + }, + ); + } + + Future _onRefresh(BuildContext context) async { + context.read().add(const ReminderEvent.refresh()); + + // at least 0.5 seconds to dismiss the refresh indicator. + // otherwise, it will be dismissed immediately. + await context.read().stream.firstOrNull; + await Future.delayed(const Duration(milliseconds: 500)); + + if (context.mounted) { + showToastNotification( + context, + message: LocaleKeys.settings_notifications_refreshSuccess.tr(), + ); + } + } + + List _filterReminders(List reminders) { + switch (widget.tabType) { + case MobileNotificationTabType.inbox: + return reminders.reversed + .where((reminder) => !reminder.isArchived) + .toList(); + case MobileNotificationTabType.archive: + return reminders.reversed + .where((reminder) => reminder.isArchived) + .toList(); + case MobileNotificationTabType.unread: + return reminders.reversed + .where((reminder) => !reminder.isRead) + .toList(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab_bar.dart new file mode 100644 index 0000000000..37768e3e7b --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab_bar.dart @@ -0,0 +1,88 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart'; +import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:reorderable_tabbar/reorderable_tabbar.dart'; + +enum MobileNotificationTabType { + inbox, + unread, + archive; + + String get tr { + switch (this) { + case MobileNotificationTabType.inbox: + return LocaleKeys.settings_notifications_tabs_inbox.tr(); + case MobileNotificationTabType.unread: + return LocaleKeys.settings_notifications_tabs_unread.tr(); + case MobileNotificationTabType.archive: + return LocaleKeys.settings_notifications_tabs_archived.tr(); + } + } + + List get actions { + switch (this) { + case MobileNotificationTabType.inbox: + return [ + NotificationPaneActionType.more, + NotificationPaneActionType.markAsRead, + ]; + case MobileNotificationTabType.unread: + case MobileNotificationTabType.archive: + return []; + } + } +} + +class MobileNotificationTabBar extends StatelessWidget { + const MobileNotificationTabBar({ + super.key, + this.height = 38.0, + required this.tabController, + required this.tabs, + }); + + final double height; + final List tabs; + final TabController tabController; + + @override + Widget build(BuildContext context) { + final baseStyle = Theme.of(context).textTheme.bodyMedium; + final labelStyle = baseStyle?.copyWith( + fontWeight: FontWeight.w500, + fontSize: 16.0, + height: 22.0 / 16.0, + ); + final unselectedLabelStyle = baseStyle?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 15.0, + height: 22.0 / 15.0, + ); + + return Container( + height: height, + padding: const EdgeInsets.only(left: 8.0), + child: ReorderableTabBar( + controller: tabController, + tabs: tabs.map((e) => Tab(text: e.tr)).toList(), + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Theme.of(context).primaryColor, + isScrollable: true, + labelStyle: labelStyle, + labelColor: baseStyle?.color, + labelPadding: const EdgeInsets.symmetric(horizontal: 12.0), + unselectedLabelStyle: unselectedLabelStyle, + overlayColor: WidgetStateProperty.all(Colors.transparent), + indicator: RoundUnderlineTabIndicator( + width: 28.0, + borderSide: BorderSide( + color: Theme.of(context).primaryColor, + width: 3, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/widgets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/widgets.dart new file mode 100644 index 0000000000..578e55892c --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/widgets.dart @@ -0,0 +1,6 @@ +export 'empty.dart'; +export 'header.dart'; +export 'settings_popup_menu.dart'; +export 'slide_actions.dart'; +export 'tab.dart'; +export 'tab_bar.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index ba416c9064..765a1354f6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; @@ -15,6 +12,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; Map getEditorBuilderMap({ @@ -25,6 +24,7 @@ Map getEditorBuilderMap({ bool editable = true, ShowPlaceholder? showParagraphPlaceholder, String Function(Node)? placeholderText, + EdgeInsets? customHeadingPadding, }) { final standardActions = [OptionAction.delete, OptionAction.duplicate]; @@ -85,6 +85,10 @@ Map getEditorBuilderMap({ HeadingBlockKeys.type: HeadingBlockComponentBuilder( configuration: configuration.copyWith( padding: (node) { + if (customHeadingPadding != null) { + return customHeadingPadding; + } + if (PlatformExtension.isMobile) { final pageStyle = context.read().state; final factor = pageStyle.fontLayout.factor; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 78f8e20fee..52d01aa6ac 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -1,8 +1,5 @@ import 'dart:ui' as ui; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; @@ -31,6 +28,8 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; final codeBlockLocalization = CodeBlockLocalizations( @@ -481,6 +480,9 @@ class _AppFlowyEditorPageState extends State { } void _customizeBlockComponentBackgroundColorDecorator() { + if (!context.mounted) { + return; + } blockComponentBackgroundColorDecorator = (Node node, String colorString) => buildEditorCustomizedColor(context, node, colorString); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart index 2a1101794a..db6231a70a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart @@ -19,6 +19,7 @@ class EmojiPickerButton extends StatelessWidget { this.direction, this.title, this.showBorder = true, + this.enable = true, }); final String emoji; @@ -31,6 +32,7 @@ class EmojiPickerButton extends StatelessWidget { final PopoverDirection? direction; final String? title; final bool showBorder; + final bool enable; @override Widget build(BuildContext context) { @@ -71,30 +73,33 @@ class EmojiPickerButton extends StatelessWidget { text: emoji.isEmpty && defaultIcon != null ? defaultIcon! : FlowyText.emoji(emoji, fontSize: emojiSize), - onTap: popoverController.show, + onTap: enable ? popoverController.show : null, ), ), ); } + return FlowyTextButton( emoji, overflow: TextOverflow.visible, fontSize: emojiSize, padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 35.0), + constraints: const BoxConstraints.tightFor(width: 36.0), fillColor: Colors.transparent, mainAxisAlignment: MainAxisAlignment.center, - onPressed: () async { - final result = await context.push( - Uri( - path: MobileEmojiPickerScreen.routeName, - queryParameters: {MobileEmojiPickerScreen.pageTitle: title}, - ).toString(), - ); - if (result != null) { - onSubmitted(result.emoji, null); - } - }, + onPressed: enable + ? () async { + final result = await context.push( + Uri( + path: MobileEmojiPickerScreen.routeName, + queryParameters: {MobileEmojiPickerScreen.pageTitle: title}, + ).toString(), + ); + if (result != null) { + onSubmitted(result.emoji, null); + } + } + : null, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart index abb166ec12..43b84ddc1c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart @@ -1,5 +1,4 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -30,22 +29,25 @@ class BulletedListIcon extends StatelessWidget { return level; } - FlowySvg get icon { - final index = level % bulletedListIcons.length; - return FlowySvg(bulletedListIcons[index]); - } - @override Widget build(BuildContext context) { - final iconPadding = PlatformExtension.isMobile - ? context.read().state.iconPadding - : 0.0; + final textStyle = + context.read().editorStyle.textStyleConfiguration; + final fontSize = textStyle.text.fontSize ?? 16.0; + final height = textStyle.text.height ?? textStyle.lineHeight; + final size = fontSize * height; + final index = level % bulletedListIcons.length; + final icon = FlowySvg( + bulletedListIcons[index], + size: Size.square(size * 0.8), + ); return Container( - constraints: const BoxConstraints( - minWidth: 22, - minHeight: 22, + constraints: BoxConstraints( + minWidth: size, + minHeight: size, ), - margin: EdgeInsets.only(top: iconPadding, right: 8.0), + margin: const EdgeInsets.only(right: 8.0), + alignment: Alignment.center, child: icon, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index a7bfe2da41..0ecc162c5c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -194,6 +194,7 @@ class _CalloutBlockComponentWidgetState key: ValueKey( emoji.toString(), ), // force to refresh the popover state + enable: editorState.editable, title: '', emoji: emoji, emojiSize: 16.0, @@ -205,7 +206,7 @@ class _CalloutBlockComponentWidgetState ), Flexible( child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6.0), + padding: const EdgeInsets.symmetric(vertical: 4.0), child: buildCalloutBlockComponent(context, textDirection), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart index 29007ada98..62d16a1714 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart @@ -1,10 +1,9 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; enum MentionType { @@ -94,6 +93,7 @@ class MentionBlock extends StatelessWidget { editorState: editorState, date: date, node: node, + textStyle: textStyle, index: index, reminderId: mention[MentionBlockKeys.reminderId], reminderOption: reminderOption, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart index 75261263d3..b40e5045fa 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart @@ -1,14 +1,12 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/reminder/reminder_extension.dart'; +import 'package:appflowy/util/theme_extension.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/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart'; @@ -27,6 +25,7 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nanoid/non_secure.dart'; @@ -37,6 +36,7 @@ class MentionDateBlock extends StatefulWidget { required this.date, required this.index, required this.node, + this.textStyle, this.reminderId, this.reminderOption, this.includeTime = false, @@ -55,6 +55,8 @@ class MentionDateBlock extends StatefulWidget { final bool includeTime; + final TextStyle? textStyle; + @override State createState() => _MentionDateBlockState(); } @@ -77,8 +79,6 @@ class _MentionDateBlockState extends State { return const SizedBox.shrink(); } - final fontSize = context.read().state.fontSize; - return MultiBlocProvider( providers: [ BlocProvider.value(value: context.read()), @@ -163,6 +163,20 @@ class _MentionDateBlockState extends State { _updateReminder(reminderOption, reminder), ); + final color = reminder?.isAck == true + ? Theme.of(context).isLightMode + ? const Color(0xFFFE0299) + : Theme.of(context).colorScheme.error + : null; + final textStyle = widget.textStyle?.copyWith( + color: color, + leadingDistribution: TextLeadingDistribution.even, + ); + + // when font size equals 14, the icon size is 16.0. + // scale the icon size based on the font size. + final iconSize = (widget.textStyle?.fontSize ?? 14.0) / 14.0 * 16.0; + return GestureDetector( onTapDown: (details) { if (widget.editorState.editable) { @@ -228,32 +242,32 @@ class _MentionDateBlockState extends State { } } }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - widget.reminderId != null - ? FlowySvgs.clock_alarm_s - : FlowySvgs.date_s, - size: const Size.square(18.0), - color: reminder?.isAck == true - ? Theme.of(context).colorScheme.error - : null, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.reminderId != null + ? '@$formattedDate' + : formattedDate, + style: widget.textStyle?.copyWith( + color: color, + leadingDistribution: TextLeadingDistribution.even, ), - const HSpace(2), - FlowyText( - formattedDate, - fontSize: fontSize, - color: reminder?.isAck == true - ? Theme.of(context).colorScheme.error - : null, - ), - ], - ), + strutStyle: widget.textStyle != null + ? StrutStyle.fromTextStyle(widget.textStyle!) + : null, + ), + const HSpace(4), + FlowySvg( + widget.reminderId != null + ? FlowySvgs.reminder_clock_s + : FlowySvgs.date_s, + size: Size.square(iconSize), + color: textStyle?.color, + ), + ], ), ), ); @@ -375,6 +389,8 @@ class _MentionDateBlockState extends State { meta: { ReminderMetaKeys.includeTime: false.toString(), ReminderMetaKeys.blockId: widget.node.id, + ReminderMetaKeys.createdAt: + DateTime.now().millisecondsSinceEpoch.toString(), }, scheduledAt: Int64(parsedDate!.millisecondsSinceEpoch ~/ 1000), isAck: parsedDate!.isBefore(DateTime.now()), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart index 8e63873641..211d6740b1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart @@ -16,18 +16,22 @@ class NumberedListIcon extends StatelessWidget { @override Widget build(BuildContext context) { final textStyle = - context.read().editorStyle.textStyleConfiguration.text; + context.read().editorStyle.textStyleConfiguration; + final fontSize = textStyle.text.fontSize ?? 16.0; + final height = textStyle.text.height ?? textStyle.lineHeight; + final size = fontSize * height; return Container( - constraints: const BoxConstraints( - minWidth: 22, - minHeight: 22, + constraints: BoxConstraints( + minWidth: size, + minHeight: size, ), margin: const EdgeInsets.only(right: 8.0), alignment: Alignment.center, child: Center( child: Text( node.levelString, - style: textStyle, + style: textStyle.text, + strutStyle: StrutStyle.fromTextStyle(textStyle.text), textDirection: textDirection, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart index 85972a3c2c..95841051d7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart @@ -1,5 +1,4 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -17,9 +16,13 @@ class TodoListIcon extends StatelessWidget { @override Widget build(BuildContext context) { - final iconPadding = PlatformExtension.isMobile - ? context.read().state.iconPadding - : 0.0; + // the icon height should be equal to the text height * text font size + final textStyle = + context.read().editorStyle.textStyleConfiguration; + final fontSize = textStyle.text.fontSize ?? 16.0; + final height = textStyle.text.height ?? textStyle.lineHeight; + final iconSize = fontSize * height; + final checked = node.attributes[TodoListBlockKeys.checked] ?? false; return GestureDetector( behavior: HitTestBehavior.opaque, @@ -28,16 +31,18 @@ class TodoListIcon extends StatelessWidget { onCheck(); }, child: Container( - constraints: const BoxConstraints( - minWidth: 22, - minHeight: 22, + constraints: BoxConstraints( + minWidth: iconSize, + minHeight: iconSize, ), - margin: EdgeInsets.only(top: iconPadding, right: 8.0), + margin: const EdgeInsets.only(right: 8.0), + alignment: Alignment.center, child: FlowySvg( checked ? FlowySvgs.m_todo_list_checked_s : FlowySvgs.m_todo_list_unchecked_s, blendMode: checked ? null : BlendMode.srcIn, + size: Size.square(iconSize * 0.9), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart index 319e6091c8..c46c3a453d 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart @@ -219,6 +219,8 @@ class ReminderReferenceService extends InlineActionsDelegate { meta: { ReminderMetaKeys.includeTime: false.toString(), ReminderMetaKeys.blockId: node.id, + ReminderMetaKeys.createdAt: + DateTime.now().millisecondsSinceEpoch.toString(), }, scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000), isAck: date.isBefore(DateTime.now()), diff --git a/frontend/appflowy_flutter/lib/shared/time_format.dart b/frontend/appflowy_flutter/lib/shared/time_format.dart new file mode 100644 index 0000000000..450f9954af --- /dev/null +++ b/frontend/appflowy_flutter/lib/shared/time_format.dart @@ -0,0 +1,42 @@ +import 'package:appflowy/generated/locale_keys.g.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/time_format_ext.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; +import 'package:time/time.dart'; + +String formatTimestampWithContext( + BuildContext context, { + required int timestamp, + String? prefix, +}) { + final now = DateTime.now(); + final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); + final difference = now.difference(dateTime); + final String date; + + final dateFormate = context.read().state.dateFormat; + final timeFormate = context.read().state.timeFormat; + + if (difference.inMinutes < 1) { + date = LocaleKeys.sideBar_justNow.tr(); + } else if (difference.inHours < 1 && dateTime.isToday) { + // Less than 1 hour + date = LocaleKeys.sideBar_minutesAgo + .tr(namedArgs: {'count': difference.inMinutes.toString()}); + } else if (difference.inHours >= 1 && dateTime.isToday) { + // in same day + date = timeFormate.formatTime(dateTime); + } else { + date = dateFormate.formatDate(dateTime, false); + } + + if (difference.inHours >= 1 && prefix != null) { + return '$prefix $date'; + } + + return date; +} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 65096984bd..55e95393f6 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -10,7 +10,7 @@ import 'package:appflowy/mobile/presentation/database/mobile_calendar_events_scr import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_page.dart'; -import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_page.dart'; +import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/mobile/presentation/setting/cloud/appflowy_cloud_page.dart'; import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; @@ -159,33 +159,11 @@ StatefulShellRoute _mobileHomeScreenWithNavigationBarRoute() { ), ], ), - // Enable search feature after we have a search page. - // StatefulShellBranch( - // routes: [ - // GoRoute( - // path: '/d', - // builder: (BuildContext context, GoRouterState state) => - // const RootPlaceholderScreen( - // label: 'Search', - // detailsPath: '/d/details', - // ), - // routes: [ - // GoRoute( - // path: 'details', - // builder: (BuildContext context, GoRouterState state) => - // const DetailsPlaceholderScreen( - // label: 'Search Page details', - // ), - // ), - // ], - // ), - // ], - // ), StatefulShellBranch( routes: [ GoRoute( - path: MobileNotificationsScreen.routeName, - builder: (_, __) => const MobileNotificationsScreen(), + path: MobileNotificationsScreenV2.routeName, + builder: (_, __) => const MobileNotificationsScreenV2(), ), ], ), diff --git a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart index d50c6fc795..8ac5ea9c11 100644 --- a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/list_extension.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_extension.dart'; import 'package:appflowy/user/application/reminder/reminder_service.dart'; @@ -17,6 +18,7 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:protobuf/protobuf.dart'; part 'reminder_bloc.freezed.dart'; @@ -37,24 +39,6 @@ class ReminderBloc extends Bloc { on( (event, emit) async { await event.when( - markAllRead: () async { - final unreadReminders = - state.pastReminders.where((reminder) => !reminder.isRead); - - final reminders = [...state.reminders]; - final updatedReminders = []; - for (final reminder in unreadReminders) { - reminders.remove(reminder); - - reminder.isRead = true; - await _reminderService.updateReminder(reminder: reminder); - - updatedReminders.add(reminder); - } - - reminders.addAll(updatedReminders); - emit(state.copyWith(reminders: reminders)); - }, started: () async { final remindersOrFailure = await _reminderService.fetchReminders(); @@ -171,6 +155,68 @@ class ReminderBloc extends Bloc { ); } }, + markAllRead: () async { + final unreadReminders = state.reminders.where( + (reminder) => !reminder.isRead, + ); + + for (final reminder in unreadReminders) { + reminder.isRead = true; + await _reminderService.updateReminder(reminder: reminder); + } + + final reminder = [...state.reminders].map((e) { + if (e.isRead) { + return e; + } + e.freeze(); + return e.rebuild((update) { + update.isRead = true; + }); + }).toList(); + + emit( + state.copyWith( + reminders: reminder, + ), + ); + }, + archiveAll: () async { + final unArchivedReminders = state.reminders.where( + (reminder) => !reminder.isArchived, + ); + + for (final reminder in unArchivedReminders) { + reminder.isRead = true; + reminder.meta[ReminderMetaKeys.isArchived] = true.toString(); + await _reminderService.updateReminder(reminder: reminder); + } + + final reminder = [...state.reminders].map((e) { + if (e.isRead && e.isArchived) { + return e; + } + e.freeze(); + return e.rebuild((update) { + update.isRead = true; + update.meta[ReminderMetaKeys.isArchived] = true.toString(); + }); + }).toList(); + + emit( + state.copyWith( + reminders: reminder, + ), + ); + }, + refresh: () async { + final remindersOrFailure = await _reminderService.fetchReminders(); + + remindersOrFailure.fold( + (reminders) => emit(state.copyWith(reminders: reminders)), + (error) => emit(state), + ); + }, ); }, ); @@ -242,11 +288,15 @@ class ReminderEvent with _$ReminderEvent { // Mark all unread reminders as read const factory ReminderEvent.markAllRead() = _MarkAllRead; + const factory ReminderEvent.archiveAll() = _ArchiveAll; + const factory ReminderEvent.pressReminder({ required String reminderId, @Default(null) int? path, @Default(null) ViewPB? view, }) = _PressReminder; + + const factory ReminderEvent.refresh() = _Refresh; } /// Object used to merge updates with @@ -259,6 +309,7 @@ class ReminderUpdate { this.isRead, this.scheduledAt, this.includeTime, + this.isArchived, }); final String id; @@ -266,6 +317,7 @@ class ReminderUpdate { final bool? isRead; final DateTime? scheduledAt; final bool? includeTime; + final bool? isArchived; ReminderPB merge({required ReminderPB a}) { final isAcknowledged = isAck == null && scheduledAt != null @@ -277,6 +329,10 @@ class ReminderUpdate { meta[ReminderMetaKeys.includeTime] = includeTime.toString(); } + if (isArchived != a.isArchived) { + meta[ReminderMetaKeys.isArchived] = isArchived.toString(); + } + return ReminderPB( id: a.id, objectId: a.objectId, @@ -327,7 +383,7 @@ class ReminderState { } late final List _reminders; - List get reminders => _reminders; + List get reminders => _reminders.unique((e) => e.id); late final List pastReminders; late final List upcomingReminders; diff --git a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart index 94bf638de5..3d23580320 100644 --- a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart +++ b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart @@ -4,6 +4,8 @@ class ReminderMetaKeys { static String includeTime = "include_time"; static String blockId = "block_id"; static String rowId = "row_id"; + static String createdAt = "created_at"; + static String isArchived = "is_archived"; } extension ReminderExtension on ReminderPB { @@ -12,4 +14,18 @@ extension ReminderExtension on ReminderPB { return includeTimeStr != null ? includeTimeStr == true.toString() : null; } + + String? get blockId => meta[ReminderMetaKeys.blockId]; + + String? get rowId => meta[ReminderMetaKeys.rowId]; + + int? get createdAt { + final t = meta[ReminderMetaKeys.createdAt]; + return t != null ? int.tryParse(t) : null; + } + + bool get isArchived { + final t = meta[ReminderMetaKeys.isArchived]; + return t != null ? t == true.toString() : false; + } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart index 1651c9055f..0551b03a3f 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -16,10 +16,14 @@ class FlowyText extends StatelessWidget { final bool selectable; final String? fontFamily; final List? fallbackFontFamily; - final double? lineHeight; final bool withTooltip; final StrutStyle? strutStyle; final bool isEmoji; + + /// this is used to control the line height in Flutter. + final double? lineHeight; + + /// this is used to control the line height from Figma. final double? figmaLineHeight; const FlowyText( @@ -36,10 +40,10 @@ class FlowyText extends StatelessWidget { this.fontFamily, this.fallbackFontFamily, this.lineHeight, + this.figmaLineHeight, this.withTooltip = false, this.isEmoji = false, this.strutStyle, - this.figmaLineHeight, }); FlowyText.small( @@ -195,7 +199,7 @@ class FlowyText extends StatelessWidget { textStyle, forceStrutHeight: true, leadingDistribution: TextLeadingDistribution.even, - height: lineHeight ?? 1.1, + height: lineHeight, ) : null, ); diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index a0c4bef0da..9e81ff2293 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,8 +53,8 @@ packages: dependency: "direct main" description: path: "." - ref: aac7729 - resolved-ref: aac77292a1a175fd7450eef30167032d3cec7fea + ref: c8e0ca9 + resolved-ref: c8e0ca946b99b59286fabb811c39de5347f8bebd url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "3.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 0f9ba4080f..e9468a0c5d 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -196,7 +196,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "aac7729" + ref: "c8e0ca9" appflowy_editor_plugins: git: diff --git a/frontend/resources/flowy_icons/16x/m_notification_action_archive.svg b/frontend/resources/flowy_icons/16x/m_notification_action_archive.svg new file mode 100644 index 0000000000..ada869aebf --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_notification_action_archive.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/m_notification_action_mark_as_read.svg b/frontend/resources/flowy_icons/16x/m_notification_action_mark_as_read.svg new file mode 100644 index 0000000000..68d249cf74 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_notification_action_mark_as_read.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/m_notification_action_multiple_choice.svg b/frontend/resources/flowy_icons/16x/m_notification_action_multiple_choice.svg new file mode 100644 index 0000000000..e8926358f0 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_notification_action_multiple_choice.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/m_notification_archived.svg b/frontend/resources/flowy_icons/16x/m_notification_archived.svg new file mode 100644 index 0000000000..37c769880e --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_notification_archived.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/m_notification_mark_as_read.svg b/frontend/resources/flowy_icons/16x/m_notification_mark_as_read.svg new file mode 100644 index 0000000000..5292de6310 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_notification_mark_as_read.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/m_notification_reminder.svg b/frontend/resources/flowy_icons/16x/m_notification_reminder.svg new file mode 100644 index 0000000000..8369a8c9a8 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_notification_reminder.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/m_notification_settings.svg b/frontend/resources/flowy_icons/16x/m_notification_settings.svg new file mode 100644 index 0000000000..987f547415 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_notification_settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/m_settings_more.svg b/frontend/resources/flowy_icons/16x/m_settings_more.svg new file mode 100644 index 0000000000..bfd339eb61 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_settings_more.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/16x/reminder_clock.svg b/frontend/resources/flowy_icons/16x/reminder_clock.svg new file mode 100644 index 0000000000..c383974855 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/reminder_clock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_home_active_notification.svg b/frontend/resources/flowy_icons/24x/m_home_active_notification.svg new file mode 100644 index 0000000000..17cd501184 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_home_active_notification.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/40x/m_empty_notification.svg b/frontend/resources/flowy_icons/40x/m_empty_notification.svg new file mode 100644 index 0000000000..f1ec21a65c --- /dev/null +++ b/frontend/resources/flowy_icons/40x/m_empty_notification.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 83e61cba09..fe193ec950 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -953,6 +953,46 @@ "showNotificationsIcon": { "label": "Show notifications icon", "hint": "Toggle off to hide the notification icon in the sidebar." + }, + "archiveNotifications": { + "allSuccess": "Archived all notifications successfully", + "success": "Archived notification successfully" + }, + "markAsReadNotifications": { + "allSuccess": "Marked all as read successfully", + "success": "Marked as read successfully" + }, + "action": { + "markAsRead": "Mark as read", + "multipleChoice": "Multiple choice", + "archive": "Archive" + }, + "settings": { + "settings": "Settings", + "markAllAsRead": "Mark all as read", + "archiveAll": "Archive all" + }, + "emptyInbox": { + "title": "No notifications yet", + "description": "You'll be notified here for @mentions" + }, + "emptyUnread": { + "title": "No unread notifications", + "description": "You're all caught up!" + }, + "emptyArchived": { + "title": "No archived notifications", + "description": "You haven't archived any notifications yet" + }, + "tabs": { + "inbox": "Inbox", + "unread": "Unread", + "archived": "Archived" + }, + "refreshSuccess": "Notifications refreshed successfully", + "titles": { + "notifications": "Notifications", + "reminder": "Reminder" } }, "appearance": {