feat: support notification on mobile (#5831)

* feat: add inbox/unread/archived tabs

* feat: dump notification info

* chore: add reminder bloc

* feat: support unread / archive notification tab

* feat: support archive all & mark all as read

* feat: add empty page

* chore: optimize gesture

* feat: add red dot above notification icon

* chore: optimize code logic

* feat: optimize tabbar animation

* fix: notification align issue

* fix: todo list icon align issue

* feat: disable emoji button inside callout in read-only mode

* feat: optimize icon size in editor

* chore: improve text color in dark mode
This commit is contained in:
Lucas.Xu 2024-07-31 15:15:15 +08:00 committed by GitHub
parent 7c3dd5375d
commit d1c1449cf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 1748 additions and 135 deletions

View File

@ -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<NotificationReminderEvent, NotificationReminderState> {
NotificationReminderBloc() : super(NotificationReminderState.initial()) {
on<NotificationReminderEvent>((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<String> _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<ViewPB?> _getView(ReminderPB reminder) async {
return ViewBackendService.getView(reminder.objectId)
.fold((s) => s, (_) => null);
}
Future<Node?> _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<Node> nodes,
ViewPB? view,
}) = _NotificationReminderState;
factory NotificationReminderState.initial() =>
const NotificationReminderState(
createdAt: '',
pageTitle: '',
reminderContent: '',
);
}

View File

@ -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();

View File

@ -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(

View File

@ -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<ViewLayoutPB?> createNewPageNotifier =
@ -25,9 +28,9 @@ final _items = <BottomNavigationBarItem>[
),
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<ReminderBloc>(),
child: BlocBuilder<ReminderBloc, ReminderState>(
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),
),
),
);
}
}

View File

@ -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<MobileNotificationsScreenV2> createState() =>
_MobileNotificationsScreenV2State();
}
class _MobileNotificationsScreenV2State
extends State<MobileNotificationsScreenV2>
with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<UserProfileBloc>(
create: (context) =>
UserProfileBloc()..add(const UserProfileEvent.started()),
),
BlocProvider<ReminderBloc>.value(value: getIt<ReminderBloc>()),
BlocProvider<NotificationFilterBloc>(
create: (_) => NotificationFilterBloc(),
),
],
child: BlocBuilder<UserProfileBloc, UserProfileState>(
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<MobileNotificationsTab> createState() => _MobileNotificationsTabState();
}
class _MobileNotificationsTabState extends State<MobileNotificationsTab>
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() {}
}

View File

@ -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);
}
}

View File

@ -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,
),
),
],
);
}
}

View File

@ -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),
],
),
);
}
}

View File

@ -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<AppearanceSettingsCubit>().state;
final dateFormate = settings.dateFormat;
final timeFormate = settings.timeFormat;
return BlocProvider<NotificationReminderBloc>(
create: (context) => NotificationReminderBloc()
..add(
NotificationReminderEvent.initial(
reminder,
dateFormate,
timeFormate,
),
),
child: BlocBuilder<NotificationReminderBloc, NotificationReminderState>(
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<ReminderBloc>().add(
ReminderEvent.update(
ReminderUpdate(
id: context.read<NotificationReminderBloc>().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<NotificationPaneActionType> 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<NotificationReminderBloc, NotificationReminderState>(
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<Node> 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,
);
}
}

View File

@ -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) =>
<PopupMenuEntry<_NotificationSettingsPopupMenuItem>>[
_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<T> _buildItem<T>({
required T value,
required FlowySvgData svg,
required String text,
}) {
return PopupMenuItem<T>(
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<ReminderBloc>().add(const ReminderEvent.markAllRead());
}
void _onArchiveAll(BuildContext context) {
showToastNotification(
context,
message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
.tr(),
);
context.read<ReminderBloc>().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,
),
],
),
);
}
}

View File

@ -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<ReminderBloc>().add(
ReminderEvent.update(
ReminderUpdate(
id: context.read<NotificationReminderBloc>().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<ReminderBloc>().add(
ReminderEvent.update(
ReminderUpdate(
id: context.read<NotificationReminderBloc>().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<ReminderBloc>();
final notificationReminderBloc =
context.read<NotificationReminderBloc>();
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<NotificationReminderBloc>().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<ReminderBloc>().add(
ReminderEvent.update(
ReminderUpdate(
id: context.read<NotificationReminderBloc>().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<ReminderBloc>().add(
ReminderEvent.update(
ReminderUpdate(
id: context.read<NotificationReminderBloc>().reminder.id,
isRead: true,
isArchived: true,
),
),
);
Navigator.of(context).pop();
}
}

View File

@ -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<NotificationTab> createState() => _NotificationTabState();
}
class _NotificationTabState extends State<NotificationTab>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return BlocBuilder<ReminderBloc, ReminderState>(
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<void> _onRefresh(BuildContext context) async {
context.read<ReminderBloc>().add(const ReminderEvent.refresh());
// at least 0.5 seconds to dismiss the refresh indicator.
// otherwise, it will be dismissed immediately.
await context.read<ReminderBloc>().stream.firstOrNull;
await Future.delayed(const Duration(milliseconds: 500));
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.settings_notifications_refreshSuccess.tr(),
);
}
}
List<ReminderPB> _filterReminders(List<ReminderPB> 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();
}
}
}

View File

@ -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<NotificationPaneActionType> 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<MobileNotificationTabType> 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,
),
),
),
);
}
}

View File

@ -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';

View File

@ -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<String, BlockComponentBuilder> getEditorBuilderMap({
@ -25,6 +24,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
bool editable = true,
ShowPlaceholder? showParagraphPlaceholder,
String Function(Node)? placeholderText,
EdgeInsets? customHeadingPadding,
}) {
final standardActions = [OptionAction.delete, OptionAction.duplicate];
@ -85,6 +85,10 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
HeadingBlockKeys.type: HeadingBlockComponentBuilder(
configuration: configuration.copyWith(
padding: (node) {
if (customHeadingPadding != null) {
return customHeadingPadding;
}
if (PlatformExtension.isMobile) {
final pageStyle = context.read<DocumentPageStyleBloc>().state;
final factor = pageStyle.fontLayout.factor;

View File

@ -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<AppFlowyEditorPage> {
}
void _customizeBlockComponentBackgroundColorDecorator() {
if (!context.mounted) {
return;
}
blockComponentBackgroundColorDecorator = (Node node, String colorString) =>
buildEditorCustomizedColor(context, node, colorString);
}

View File

@ -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,20 +73,22 @@ 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 {
onPressed: enable
? () async {
final result = await context.push<EmojiPickerResult>(
Uri(
path: MobileEmojiPickerScreen.routeName,
@ -94,7 +98,8 @@ class EmojiPickerButton extends StatelessWidget {
if (result != null) {
onSubmitted(result.emoji, null);
}
},
}
: null,
);
}
}

View File

@ -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<DocumentPageStyleBloc>().state.iconPadding
: 0.0;
final textStyle =
context.read<EditorState>().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,
);
}

View File

@ -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),
),
),

View File

@ -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,

View File

@ -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<MentionDateBlock> createState() => _MentionDateBlockState();
}
@ -77,8 +79,6 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
return const SizedBox.shrink();
}
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
return MultiBlocProvider(
providers: [
BlocProvider<ReminderBloc>.value(value: context.read<ReminderBloc>()),
@ -163,6 +163,20 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
_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,34 +242,34 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
}
}
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
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,
),
strutStyle: widget.textStyle != null
? StrutStyle.fromTextStyle(widget.textStyle!)
: null,
),
const HSpace(4),
FlowySvg(
widget.reminderId != null
? FlowySvgs.clock_alarm_s
? FlowySvgs.reminder_clock_s
: FlowySvgs.date_s,
size: const Size.square(18.0),
color: reminder?.isAck == true
? Theme.of(context).colorScheme.error
: null,
),
const HSpace(2),
FlowyText(
formattedDate,
fontSize: fontSize,
color: reminder?.isAck == true
? Theme.of(context).colorScheme.error
: null,
size: Size.square(iconSize),
color: textStyle?.color,
),
],
),
),
),
);
},
),
@ -375,6 +389,8 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
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()),

View File

@ -16,18 +16,22 @@ class NumberedListIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
final textStyle =
context.read<EditorState>().editorStyle.textStyleConfiguration.text;
context.read<EditorState>().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,
),
),

View File

@ -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<DocumentPageStyleBloc>().state.iconPadding
: 0.0;
// the icon height should be equal to the text height * text font size
final textStyle =
context.read<EditorState>().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),
),
),
);

View File

@ -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()),

View File

@ -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<AppearanceSettingsCubit>().state.dateFormat;
final timeFormate = context.read<AppearanceSettingsCubit>().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;
}

View File

@ -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: <RouteBase>[
// GoRoute(
// path: '/d',
// builder: (BuildContext context, GoRouterState state) =>
// const RootPlaceholderScreen(
// label: 'Search',
// detailsPath: '/d/details',
// ),
// routes: <RouteBase>[
// GoRoute(
// path: 'details',
// builder: (BuildContext context, GoRouterState state) =>
// const DetailsPlaceholderScreen(
// label: 'Search Page details',
// ),
// ),
// ],
// ),
// ],
// ),
StatefulShellBranch(
routes: <RouteBase>[
GoRoute(
path: MobileNotificationsScreen.routeName,
builder: (_, __) => const MobileNotificationsScreen(),
path: MobileNotificationsScreenV2.routeName,
builder: (_, __) => const MobileNotificationsScreenV2(),
),
],
),

View File

@ -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<ReminderEvent, ReminderState> {
on<ReminderEvent>(
(event, emit) async {
await event.when(
markAllRead: () async {
final unreadReminders =
state.pastReminders.where((reminder) => !reminder.isRead);
final reminders = [...state.reminders];
final updatedReminders = <ReminderPB>[];
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<ReminderEvent, ReminderState> {
);
}
},
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<ReminderPB> _reminders;
List<ReminderPB> get reminders => _reminders;
List<ReminderPB> get reminders => _reminders.unique((e) => e.id);
late final List<ReminderPB> pastReminders;
late final List<ReminderPB> upcomingReminders;

View File

@ -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;
}
}

View File

@ -16,10 +16,14 @@ class FlowyText extends StatelessWidget {
final bool selectable;
final String? fontFamily;
final List<String>? 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,
);

View File

@ -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"

View File

@ -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:

View File

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.25 8.5166V15.8333C16.25 17.4999 15.8333 18.3333 13.75 18.3333H6.25C4.16667 18.3333 3.75 17.4999 3.75 15.8333V8.5166" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.16675 1.6665H15.8334C17.5001 1.6665 18.3334 2.49984 18.3334 4.1665V5.83317C18.3334 7.49984 17.5001 8.33317 15.8334 8.33317H4.16675C2.50008 8.33317 1.66675 7.49984 1.66675 5.83317V4.1665C1.66675 2.49984 2.50008 1.6665 4.16675 1.6665Z" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.48315 11.6665H11.5165" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 772 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.225 21.251H14.775C19.4 21.251 21.25 19.401 21.25 14.776V9.22598C21.25 4.60098 19.4 2.75098 14.775 2.75098H9.225C4.6 2.75098 2.75 4.60098 2.75 9.22598V14.776C2.75 19.401 4.6 21.251 9.225 21.251Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.06909 12.2014L10.6868 14.8192L16.1998 9.40137" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 535 B

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.1416 10.2085V13.9585C14.1416 17.0835 12.8916 18.3335 9.7666 18.3335H6.0166C2.8916 18.3335 1.6416 17.0835 1.6416 13.9585V10.2085C1.6416 7.0835 2.8916 5.8335 6.0166 5.8335H9.7666C12.8916 5.8335 14.1416 7.0835 14.1416 10.2085Z" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.3084 6.24984C18.3084 8.6415 16.4834 10.5998 14.1417 10.8082V10.2082C14.1417 7.08317 12.8917 5.83317 9.76675 5.83317H9.16675C9.37508 3.4915 11.3334 1.6665 13.7251 1.6665C16.0501 1.6665 17.9667 3.3915 18.2584 5.6415C18.2917 5.83317 18.3084 6.0415 18.3084 6.24984Z" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 789 B

View File

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.25 8.5166V15.8333C16.25 17.4999 15.8333 18.3333 13.75 18.3333H6.25C4.16667 18.3333 3.75 17.4999 3.75 15.8333V8.5166" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.1665 1.66675H15.8332C17.4998 1.66675 18.3332 2.50008 18.3332 4.16675V5.83341C18.3332 7.50008 17.4998 8.33342 15.8332 8.33342H4.1665C2.49984 8.33342 1.6665 7.50008 1.6665 5.83341V4.16675C1.6665 2.50008 2.49984 1.66675 4.1665 1.66675Z" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.48291 11.6667H11.5162" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 772 B

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.68734 17.7084H12.3123C16.1665 17.7084 17.7082 16.1667 17.7082 12.3126V7.68758C17.7082 3.83341 16.1665 2.29175 12.3123 2.29175H7.68734C3.83317 2.29175 2.2915 3.83341 2.2915 7.68758V12.3126C2.2915 16.1667 3.83317 17.7084 7.68734 17.7084Z" stroke="#1E2022" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.72412 10.1669L8.90558 12.3484L13.4997 7.8335" stroke="#1E2022" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 582 B

View File

@ -0,0 +1,11 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" rx="18" fill="#F1E2FF"/>
<g clip-path="url(#clip0_756_7938)">
<path d="M10.889 18.9758C10.889 16.467 12.2534 14.148 14.4676 12.8929C15.5594 12.276 16.7922 11.9519 18.0463 11.9519C19.3004 11.9519 20.5332 12.276 21.6251 12.8929C23.8397 14.148 25.2036 16.4666 25.2036 18.9758C25.204 22.8556 21.9997 26 18.0465 26C14.0934 26 10.889 22.8552 10.889 18.9758ZM14.5662 10.2628C14.6346 10.3376 14.6871 10.4256 14.7205 10.5213C14.7539 10.617 14.7674 10.7185 14.7604 10.8196C14.7533 10.9208 14.7258 11.0194 14.6794 11.1096C14.633 11.1997 14.5688 11.2795 14.4907 11.3441L11.3166 14.0258C11.1563 14.1599 10.9506 14.2272 10.7421 14.2137C10.5336 14.2002 10.3383 14.107 10.1966 13.9533C10.1282 13.8785 10.0757 13.7906 10.0423 13.6949C10.0089 13.5993 9.99528 13.4978 10.0023 13.3967C10.0092 13.2956 10.0367 13.1969 10.0829 13.1067C10.1292 13.0166 10.1933 12.9367 10.2713 12.872L13.4458 10.1904C13.6062 10.0558 13.8125 9.98844 14.0214 10.0024C14.2316 10.0157 14.4276 10.1099 14.5662 10.2628ZM17.9363 14.9813C17.4981 14.9813 17.143 15.3244 17.143 15.7475V18.8122C17.143 19.0687 17.2755 19.3078 17.4959 19.45L19.8767 20.9824C20.2411 21.217 20.7344 21.1224 20.9775 20.7699C21.2206 20.418 21.122 19.9424 20.7576 19.7078L18.7301 18.4025V15.7475C18.7301 15.3244 18.3745 14.9813 17.9363 14.9813ZM21.4237 10.2677C21.5641 10.111 21.7602 10.0155 21.9702 10.0016C22.1801 9.98776 22.3871 10.0566 22.5468 10.1935L25.7276 12.9254C26.0582 13.2089 26.092 13.7022 25.8031 14.0267C25.5138 14.3511 25.0112 14.3844 24.6805 14.1004L21.4993 11.3694C21.4208 11.3029 21.3565 11.2213 21.3101 11.1294C21.2636 11.0376 21.2361 10.9374 21.2291 10.8347C21.222 10.7321 21.2356 10.629 21.2691 10.5317C21.3025 10.4344 21.3551 10.3448 21.4237 10.2681V10.2677Z" fill="#B369FE"/>
</g>
<defs>
<clipPath id="clip0_756_7938">
<rect width="16" height="16" fill="white" transform="translate(10 10)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12.5C11.3807 12.5 12.5 11.3807 12.5 10C12.5 8.61929 11.3807 7.5 10 7.5C8.61929 7.5 7.5 8.61929 7.5 10C7.5 11.3807 8.61929 12.5 10 12.5Z" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.6665 10.7334V9.26669C1.6665 8.40003 2.37484 7.68336 3.24984 7.68336C4.75817 7.68336 5.37484 6.6167 4.6165 5.30836C4.18317 4.55836 4.4415 3.58336 5.19984 3.15003L6.6415 2.32503C7.29984 1.93336 8.14984 2.1667 8.5415 2.82503L8.63317 2.98336C9.38317 4.2917 10.6165 4.2917 11.3748 2.98336L11.4665 2.82503C11.8582 2.1667 12.7082 1.93336 13.3665 2.32503L14.8082 3.15003C15.5665 3.58336 15.8248 4.55836 15.3915 5.30836C14.6332 6.6167 15.2498 7.68336 16.7582 7.68336C17.6248 7.68336 18.3415 8.39169 18.3415 9.26669V10.7334C18.3415 11.6 17.6332 12.3167 16.7582 12.3167C15.2498 12.3167 14.6332 13.3834 15.3915 14.6917C15.8248 15.45 15.5665 16.4167 14.8082 16.85L13.3665 17.675C12.7082 18.0667 11.8582 17.8334 11.4665 17.175L11.3748 17.0167C10.6248 15.7084 9.3915 15.7084 8.63317 17.0167L8.5415 17.175C8.14984 17.8334 7.29984 18.0667 6.6415 17.675L5.19984 16.85C4.4415 16.4167 4.18317 15.4417 4.6165 14.6917C5.37484 13.3834 4.75817 12.3167 3.24984 12.3167C2.37484 12.3167 1.6665 11.6 1.6665 10.7334Z" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,6 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.964 20.1331C16.0057 20.1331 20.1307 16.0081 20.1307 10.9665C20.1307 5.9248 16.0057 1.7998 10.964 1.7998C5.92236 1.7998 1.79736 5.9248 1.79736 10.9665C1.79736 16.0081 5.92236 20.1331 10.964 20.1331Z" stroke="#171717" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.7606 11.4167H6.77707" stroke="#171717" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.9906 11.4167H11.007" stroke="#171717" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.2201 11.4167H15.2365" stroke="#171717" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 760 B

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.8332 8.83333C13.8332 12.0533 11.2198 14.6667 7.99984 14.6667C4.77984 14.6667 2.1665 12.0533 2.1665 8.83333C2.1665 5.61333 4.77984 3 7.99984 3C11.2198 3 13.8332 5.61333 13.8332 8.83333Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 5.33325V8.66659" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 1.33325H10" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 576 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.3399 14.49L18.3399 12.83C18.1299 12.46 17.9399 11.76 17.9399 11.35V8.82C17.9399 6.47 16.5599 4.44 14.5699 3.49C14.0499 2.57 13.0899 2 11.9899 2C10.8999 2 9.91994 2.59 9.39994 3.52C7.44994 4.49 6.09994 6.5 6.09994 8.82V11.35C6.09994 11.76 5.90994 12.46 5.69994 12.82L4.68994 14.49C4.28994 15.16 4.19994 15.9 4.44994 16.58C4.68994 17.25 5.25994 17.77 5.99994 18.02C7.93994 18.68 9.97994 19 12.0199 19C14.0599 19 16.0999 18.68 18.0399 18.03C18.7399 17.8 19.2799 17.27 19.5399 16.58C19.7999 15.89 19.7299 15.13 19.3399 14.49Z" fill="#00BDF1"/>
<path d="M14.8301 20.01C14.4101 21.17 13.3001 22 12.0001 22C11.2101 22 10.4301 21.68 9.88005 21.11C9.56005 20.81 9.32005 20.41 9.18005 20C9.31005 20.02 9.44005 20.03 9.58005 20.05C9.81005 20.08 10.0501 20.11 10.2901 20.13C10.8601 20.18 11.4401 20.21 12.0201 20.21C12.5901 20.21 13.1601 20.18 13.7201 20.13C13.9301 20.11 14.1401 20.1 14.3401 20.07C14.5001 20.05 14.6601 20.03 14.8301 20.01Z" fill="#00BDF1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,6 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.4">
<path d="M16.5001 40.3334H27.5001C36.6667 40.3334 40.3334 36.6667 40.3334 27.5001V16.5001C40.3334 7.33341 36.6667 3.66675 27.5001 3.66675H16.5001C7.33341 3.66675 3.66675 7.33341 3.66675 16.5001V27.5001C3.66675 36.6667 7.33341 40.3334 16.5001 40.3334Z" stroke="#171717" stroke-width="1.83333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.66675 23.8338H10.5601C11.9534 23.8338 13.2184 24.6221 13.8417 25.8688L15.4734 29.1504C16.5001 31.1671 18.3334 31.1671 18.7734 31.1671H25.2451C26.6384 31.1671 27.9034 30.3788 28.5267 29.1321L30.1584 25.8504C30.7817 24.6038 32.0467 23.8154 33.4401 23.8154H40.2967" stroke="#171717" stroke-width="1.83333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@ -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": {