feat: notification multiple select (#5847)

* chore: update editor version

* feat: support multi select notification items

* fix: flutter analyze

* feat: add navgation bar button

* feat: add multi select item

* feat: add multi choice in notification page

* feat: support multi choice

* chore: update icon

* feat: support open page from notification page

* chore: update version
This commit is contained in:
Lucas.Xu 2024-08-01 16:30:15 +08:00 committed by GitHub
parent 7261d1e8da
commit 9fbba5fb60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1109 additions and 394 deletions

View File

@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.6.6"
APPFLOWY_VERSION = "0.6.7"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"

View File

@ -9,7 +9,7 @@ class AnimatedGestureDetector extends StatefulWidget {
this.duration = const Duration(milliseconds: 100),
this.alignment = Alignment.center,
this.behavior = HitTestBehavior.opaque,
required this.onTapUp,
this.onTapUp,
required this.child,
});
@ -19,7 +19,7 @@ class AnimatedGestureDetector extends StatefulWidget {
final Alignment alignment;
final bool feedback;
final HitTestBehavior behavior;
final VoidCallback onTapUp;
final VoidCallback? onTapUp;
@override
State<AnimatedGestureDetector> createState() =>
@ -38,7 +38,7 @@ class _AnimatedGestureDetectorState extends State<AnimatedGestureDetector> {
HapticFeedbackType.light.call();
widget.onTapUp();
widget.onTapUp?.call();
},
onTapDown: (details) {
setState(() => scale = widget.scaleFactor);

View File

@ -1,17 +1,30 @@
import 'dart:ui';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
import 'package:appflowy/mobile/presentation/widgets/navigation_bar_button.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/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
enum BottomNavigationBarActionType {
home,
notificationMultiSelect,
}
final PropertyValueNotifier<ViewLayoutPB?> createNewPageNotifier =
PropertyValueNotifier(null);
final ValueNotifier<BottomNavigationBarActionType> bottomNavigationBarType =
ValueNotifier(BottomNavigationBarActionType.home);
const _homeLabel = 'home';
const _addLabel = 'add';
@ -37,7 +50,7 @@ final _items = <BottomNavigationBarItem>[
/// Builds the "shell" for the app by building a Scaffold with a
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
class MobileBottomNavigationBar extends StatelessWidget {
class MobileBottomNavigationBar extends StatefulWidget {
/// Constructs an [MobileBottomNavigationBar].
const MobileBottomNavigationBar({
required this.navigationShell,
@ -47,68 +60,76 @@ class MobileBottomNavigationBar extends StatelessWidget {
/// The navigation shell and container for the branch Navigators.
final StatefulNavigationShell navigationShell;
@override
State<MobileBottomNavigationBar> createState() =>
_MobileBottomNavigationBarState();
}
class _MobileBottomNavigationBarState extends State<MobileBottomNavigationBar> {
Widget? _bottomNavigationBar;
@override
void initState() {
super.initState();
bottomNavigationBarType.addListener(_animate);
}
@override
void dispose() {
bottomNavigationBarType.removeListener(_animate);
super.dispose();
}
@override
Widget build(BuildContext context) {
final isLightMode = Theme.of(context).isLightMode;
final backgroundColor = isLightMode
? Colors.white.withOpacity(0.95)
: const Color(0xFF23262B).withOpacity(0.95);
final borderColor = isLightMode
? const Color(0x141F2329)
: const Color(0xFF23262B).withOpacity(0.5);
_bottomNavigationBar = switch (bottomNavigationBarType.value) {
BottomNavigationBarActionType.home =>
_buildHomePageNavigationBar(context),
BottomNavigationBarActionType.notificationMultiSelect =>
_buildNotificationNavigationBar(context),
};
return Scaffold(
body: navigationShell,
body: widget.navigationShell,
extendBody: true,
bottomNavigationBar: ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 3,
sigmaY: 3,
),
child: DecoratedBox(
decoration: BoxDecoration(
border: isLightMode
? Border(top: BorderSide(color: borderColor))
: null,
color: backgroundColor,
),
child: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
enableFeedback: false,
type: BottomNavigationBarType.fixed,
elevation: 0,
items: _items,
backgroundColor: Colors.transparent,
currentIndex: navigationShell.currentIndex,
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
),
),
),
bottomNavigationBar: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: _transitionBuilder,
child: _bottomNavigationBar,
),
);
}
/// Navigate to the current location of the branch at the provided index when
/// tapping an item in the BottomNavigationBar.
void _onTap(BuildContext context, int bottomBarIndex) {
if (_items[bottomBarIndex].label == _addLabel) {
// show an add dialog
createNewPageNotifier.value = ViewLayoutPB.Document;
return;
}
// When navigating to a new branch, it's recommended to use the goBranch
// method, as doing so makes sure the last navigation state of the
// Navigator for the branch is restored.
navigationShell.goBranch(
bottomBarIndex,
// A common pattern when using bottom navigation bars is to support
// navigating to the initial location when tapping the item that is
// already active. This example demonstrates how to support this behavior,
// using the initialLocation parameter of goBranch.
initialLocation: bottomBarIndex == navigationShell.currentIndex,
Widget _buildHomePageNavigationBar(BuildContext context) {
return _HomePageNavigationBar(
navigationShell: widget.navigationShell,
);
}
Widget _buildNotificationNavigationBar(BuildContext context) {
return const _NotificationNavigationBar();
}
// widget A going down, widget B going up
Widget _transitionBuilder(
Widget child,
Animation<double> animation,
) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(animation),
child: child,
);
}
void _animate() {
setState(() {});
}
}
class _NotificationNavigationBarItemIcon extends StatelessWidget {
@ -169,3 +190,170 @@ class _RedDot extends StatelessWidget {
);
}
}
class _HomePageNavigationBar extends StatelessWidget {
const _HomePageNavigationBar({
required this.navigationShell,
});
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 3,
sigmaY: 3,
),
child: DecoratedBox(
decoration: BoxDecoration(
border: context.border,
color: context.backgroundColor,
),
child: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
enableFeedback: false,
type: BottomNavigationBarType.fixed,
elevation: 0,
items: _items,
backgroundColor: Colors.transparent,
currentIndex: navigationShell.currentIndex,
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
),
),
),
);
}
/// Navigate to the current location of the branch at the provided index when
/// tapping an item in the BottomNavigationBar.
void _onTap(BuildContext context, int bottomBarIndex) {
if (_items[bottomBarIndex].label == _addLabel) {
// show an add dialog
createNewPageNotifier.value = ViewLayoutPB.Document;
return;
}
// When navigating to a new branch, it's recommended to use the goBranch
// method, as doing so makes sure the last navigation state of the
// Navigator for the branch is restored.
navigationShell.goBranch(
bottomBarIndex,
// A common pattern when using bottom navigation bars is to support
// navigating to the initial location when tapping the item that is
// already active. This example demonstrates how to support this behavior,
// using the initialLocation parameter of goBranch.
initialLocation: bottomBarIndex == navigationShell.currentIndex,
);
}
}
class _NotificationNavigationBar extends StatelessWidget {
const _NotificationNavigationBar();
@override
Widget build(BuildContext context) {
return Container(
// todo: use real height here.
height: 90,
decoration: BoxDecoration(
border: context.border,
color: context.backgroundColor,
),
padding: const EdgeInsets.only(bottom: 20),
child: ValueListenableBuilder(
valueListenable: mSelectedNotificationIds,
builder: (context, value, child) {
if (value.isEmpty) {
// not editable
return IgnorePointer(
child: Opacity(
opacity: 0.3,
child: child,
),
);
}
return child!;
},
child: Row(
children: [
const HSpace(20),
Expanded(
child: NavigationBarButton(
icon: FlowySvgs.m_notification_action_mark_as_read_s,
text: LocaleKeys.settings_notifications_action_markAsRead.tr(),
onTap: () => _onMarkAsRead(context),
),
),
const HSpace(16),
Expanded(
child: NavigationBarButton(
icon: FlowySvgs.m_notification_action_archive_s,
text: LocaleKeys.settings_notifications_action_archive.tr(),
onTap: () => _onArchive(context),
),
),
const HSpace(20),
],
),
),
);
}
void _onMarkAsRead(BuildContext context) {
if (mSelectedNotificationIds.value.isEmpty) {
return;
}
showToastNotification(
context,
message: LocaleKeys
.settings_notifications_markAsReadNotifications_allSuccess
.tr(),
);
getIt<ReminderBloc>()
.add(ReminderEvent.markAsRead(mSelectedNotificationIds.value));
mSelectedNotificationIds.value = [];
}
void _onArchive(BuildContext context) {
if (mSelectedNotificationIds.value.isEmpty) {
return;
}
showToastNotification(
context,
message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
.tr(),
);
getIt<ReminderBloc>()
.add(ReminderEvent.archive(mSelectedNotificationIds.value));
mSelectedNotificationIds.value = [];
}
}
extension on BuildContext {
Color get backgroundColor {
return Theme.of(this).isLightMode
? Colors.white.withOpacity(0.95)
: const Color(0xFF23262B).withOpacity(0.95);
}
Color get borderColor {
return Theme.of(this).isLightMode
? const Color(0x141F2329)
: const Color(0xFF23262B).withOpacity(0.5);
}
Border? get border {
return Theme.of(this).isLightMode
? Border(top: BorderSide(color: borderColor))
: null;
}
}

View File

@ -0,0 +1,59 @@
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileNotificationsMultiSelectScreen extends StatelessWidget {
const MobileNotificationsMultiSelectScreen({super.key});
static const routeName = '/notifications_multi_select';
@override
Widget build(BuildContext context) {
return BlocProvider<ReminderBloc>.value(
value: getIt<ReminderBloc>(),
child: const MobileNotificationMultiSelect(),
);
}
}
class MobileNotificationMultiSelect extends StatefulWidget {
const MobileNotificationMultiSelect({
super.key,
});
@override
State<MobileNotificationMultiSelect> createState() =>
_MobileNotificationMultiSelectState();
}
class _MobileNotificationMultiSelectState
extends State<MobileNotificationMultiSelect> {
@override
void dispose() {
mSelectedNotificationIds.value.clear();
super.dispose();
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MobileNotificationMultiSelectPageHeader(),
VSpace(12.0),
Expanded(
child: MultiSelectNotificationTab(),
),
],
),
),
);
}
}

View File

@ -1,13 +1,17 @@
import 'package:appflowy/mobile/application/user_profile/user_profile_bloc.dart';
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_multiple_select_page.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/presentation.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:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
final PropertyValueNotifier<List<String>> mSelectedNotificationIds =
PropertyValueNotifier([]);
class MobileNotificationsScreenV2 extends StatefulWidget {
const MobileNotificationsScreenV2({super.key});
@ -20,29 +24,33 @@ class MobileNotificationsScreenV2 extends StatefulWidget {
class _MobileNotificationsScreenV2State
extends State<MobileNotificationsScreenV2>
with SingleTickerProviderStateMixin {
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return MultiBlocProvider(
providers: [
BlocProvider<UserProfileBloc>(
create: (context) =>
UserProfileBloc()..add(const UserProfileEvent.started()),
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(),
);
child: ValueListenableBuilder(
valueListenable: bottomNavigationBarType,
builder: (_, value, __) {
switch (value) {
case BottomNavigationBarActionType.home:
return const MobileNotificationsTab();
case BottomNavigationBarActionType.notificationMultiSelect:
return const MobileNotificationMultiSelect();
}
},
),
);
@ -52,11 +60,8 @@ class _MobileNotificationsScreenV2State
class MobileNotificationsTab extends StatefulWidget {
const MobileNotificationsTab({
super.key,
// required this.userProfile,
});
// final UserProfilePB userProfile;
@override
State<MobileNotificationsTab> createState() => _MobileNotificationsTabState();
}
@ -79,12 +84,10 @@ class _MobileNotificationsTabState extends State<MobileNotificationsTab>
length: 3,
vsync: this,
);
tabController.addListener(_onTabChange);
}
@override
void dispose() {
tabController.removeListener(_onTabChange);
tabController.dispose();
super.dispose();
@ -114,6 +117,4 @@ class _MobileNotificationsTabState extends State<MobileNotificationsTab>
),
);
}
void _onTabChange() {}
}

View File

@ -1,5 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/settings_popup_menu.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -30,3 +32,65 @@ class MobileNotificationPageHeader extends StatelessWidget {
);
}
}
class MobileNotificationMultiSelectPageHeader extends StatelessWidget {
const MobileNotificationMultiSelectPageHeader({
super.key,
});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 56),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildCancelButton(
isOpaque: false,
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () => bottomNavigationBarType.value =
BottomNavigationBarActionType.home,
),
ValueListenableBuilder(
valueListenable: mSelectedNotificationIds,
builder: (_, value, __) {
return FlowyText(
// todo: i18n
'${value.length} Selected',
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w500,
);
},
),
// this button is used to align the text to the center
_buildCancelButton(
isOpaque: true,
padding: const EdgeInsets.symmetric(horizontal: 16),
),
],
),
);
}
//
Widget _buildCancelButton({
required bool isOpaque,
required EdgeInsets padding,
VoidCallback? onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Padding(
padding: padding,
child: FlowyText(
LocaleKeys.button_cancel.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w400,
color: isOpaque ? Colors.transparent : null,
),
),
);
}
}

View File

@ -0,0 +1,115 @@
import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart';
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MultiSelectNotificationItem extends StatelessWidget {
const MultiSelectNotificationItem({
super.key,
required this.reminder,
});
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 = ValueListenableBuilder(
valueListenable: mSelectedNotificationIds,
builder: (_, selectedIds, child) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12),
decoration: selectedIds.contains(reminder.id)
? ShapeDecoration(
color: const Color(0x1900BCF0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
)
: null,
child: child,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: _InnerNotificationItem(
reminder: reminder,
),
),
);
return AnimatedGestureDetector(
scaleFactor: 0.99,
onTapUp: () {
if (mSelectedNotificationIds.value.contains(reminder.id)) {
mSelectedNotificationIds.value = mSelectedNotificationIds.value
..remove(reminder.id);
} else {
mSelectedNotificationIds.value = mSelectedNotificationIds.value
..add(reminder.id);
}
},
child: child,
);
},
),
);
}
}
class _InnerNotificationItem extends StatelessWidget {
const _InnerNotificationItem({
required this.reminder,
});
final ReminderPB reminder;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const HSpace(10.0),
NotificationCheckIcon(
isSelected: mSelectedNotificationIds.value.contains(reminder.id),
),
const HSpace(3.0),
!reminder.isRead ? const UnreadRedDot() : const HSpace(6.0),
const HSpace(3.0),
NotificationIcon(reminder: reminder),
const HSpace(12.0),
Expanded(
child: NotificationContent(reminder: reminder),
),
],
);
}
}

View File

@ -1,18 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.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';
@ -60,55 +52,61 @@ class NotificationItem extends StatelessWidget {
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),
),
],
child: _InnerNotificationItem(
tabType: tabType,
reminder: reminder,
),
),
);
if (reminder.isRead) {
return child;
}
return AnimatedGestureDetector(
scaleFactor: 0.99,
onTapUp: () => _onMarkAsRead(context),
child: child,
onTapUp: () async {
final view = state.view;
if (view == null) {
return;
}
await context.pushView(view);
if (!reminder.isRead && context.mounted) {
context.read<ReminderBloc>().add(
ReminderEvent.markAsRead([reminder.id]),
);
}
},
);
},
),
);
}
}
void _onMarkAsRead(BuildContext context) {
if (reminder.isRead) {
return;
}
class _InnerNotificationItem extends StatelessWidget {
const _InnerNotificationItem({
required this.reminder,
required this.tabType,
});
showToastNotification(
context,
message: LocaleKeys.settings_notifications_markAsReadNotifications_success
.tr(),
final MobileNotificationTabType tabType;
final ReminderPB reminder;
@override
Widget build(BuildContext context) {
return 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),
),
],
);
context.read<ReminderBloc>().add(
ReminderEvent.update(
ReminderUpdate(
id: context.read<NotificationReminderBloc>().reminder.id,
isRead: true,
),
),
);
}
}
@ -125,8 +123,6 @@ class _SlidableNotificationItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
// only show the actions in the inbox tab
final List<NotificationPaneActionType> actions = switch (tabType) {
MobileNotificationTabType.inbox => [
NotificationPaneActionType.more,
@ -166,201 +162,3 @@ class _SlidableNotificationItem extends StatelessWidget {
);
}
}
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

@ -5,6 +5,7 @@ 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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
@ -13,6 +14,8 @@ enum _NotificationSettingsPopupMenuItem {
settings,
markAllAsRead,
archiveAll,
// only visible in debug mode
unarchiveAll;
}
class NotificationSettingsPopupMenu extends StatelessWidget {
@ -56,6 +59,15 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
svg: FlowySvgs.m_notification_archived_s,
text: LocaleKeys.settings_notifications_settings_archiveAll.tr(),
),
// only visible in debug mode
if (kDebugMode) ...[
const PopupMenuDivider(height: 0.5),
_buildItem(
value: _NotificationSettingsPopupMenuItem.unarchiveAll,
svg: FlowySvgs.m_notification_archived_s,
text: 'Unarchive all (Debug Mode)',
),
],
],
onSelected: (_NotificationSettingsPopupMenuItem value) {
switch (value) {
@ -68,6 +80,9 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
case _NotificationSettingsPopupMenuItem.settings:
context.push(MobileHomeSettingPage.routeName);
break;
case _NotificationSettingsPopupMenuItem.unarchiveAll:
_onUnarchiveAll(context);
break;
}
},
);
@ -108,6 +123,19 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
context.read<ReminderBloc>().add(const ReminderEvent.archiveAll());
}
void _onUnarchiveAll(BuildContext context) {
if (!kDebugMode) {
return;
}
showToastNotification(
context,
message: 'Unarchive all success (Debug Mode)',
);
context.read<ReminderBloc>().add(const ReminderEvent.unarchiveAll());
}
}
class _PopupButton extends StatelessWidget {

View File

@ -0,0 +1,237 @@
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/notifications/widgets/color.dart';
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
const _kNotificationIconHeight = 36.0;
class NotificationIcon extends StatelessWidget {
const NotificationIcon({
super.key,
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 NotificationCheckIcon extends StatelessWidget {
const NotificationCheckIcon({super.key, required this.isSelected});
final bool isSelected;
@override
Widget build(BuildContext context) {
return SizedBox(
height: _kNotificationIconHeight,
child: Center(
child: FlowySvg(
isSelected
? FlowySvgs.m_notification_multi_select_s
: FlowySvgs.m_notification_multi_unselect_s,
blendMode: isSelected ? null : BlendMode.srcIn,
),
),
);
}
}
class UnreadRedDot extends StatelessWidget {
const UnreadRedDot({super.key});
@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({
super.key,
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 NotificationEllipse(),
],
FlowyText.regular(
pageTitle,
fontSize: 12,
figmaLineHeight: 18,
color: context.notificationItemTextColor,
),
],
),
);
}
}
class NotificationEllipse extends StatelessWidget {
const NotificationEllipse({super.key});
@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({
super.key,
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 IgnorePointer(
child: AppFlowyEditor(
editorState: editorState,
editorStyle: editorStyle,
disableSelectionService: true,
disableKeyboardService: true,
disableScrollService: true,
editable: false,
shrinkWrap: true,
blockComponentBuilders: blockBuilders,
),
);
}
}

View File

@ -4,6 +4,7 @@ import 'package:appflowy/mobile/application/notification/notification_reminder_b
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/presentation.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';
@ -88,13 +89,21 @@ enum NotificationPaneActionType {
showDivider: false,
useRootNavigator: true,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (context) {
builder: (_) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: reminderBloc),
BlocProvider.value(value: notificationReminderBloc),
],
child: const _NotificationMoreActions(),
child: _NotificationMoreActions(
onClickMultipleChoice: () {
Future.delayed(const Duration(milliseconds: 250), () {
bottomNavigationBarType.value =
BottomNavigationBarActionType
.notificationMultiSelect;
});
},
),
);
},
);
@ -105,7 +114,11 @@ enum NotificationPaneActionType {
}
class _NotificationMoreActions extends StatelessWidget {
const _NotificationMoreActions();
const _NotificationMoreActions({
required this.onClickMultipleChoice,
});
final VoidCallback onClickMultipleChoice;
@override
Widget build(BuildContext context) {
@ -172,6 +185,8 @@ class _NotificationMoreActions extends StatelessWidget {
void _onMultipleChoice(BuildContext context) {
Navigator.of(context).pop();
onClickMultipleChoice();
}
void _onArchive(BuildContext context) {

View File

@ -1,6 +1,6 @@
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/shared/list_extension.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';
@ -37,25 +37,28 @@ class _NotificationTabState extends State<NotificationTab>
final reminders = _filterReminders(state.reminders);
if (reminders.isEmpty) {
// add refresh indicator to the empty notification.
return EmptyNotification(
type: widget.tabType,
);
}
final 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,
);
},
);
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,
);
},
),
child: child,
);
},
);
@ -82,15 +85,53 @@ class _NotificationTabState extends State<NotificationTab>
case MobileNotificationTabType.inbox:
return reminders.reversed
.where((reminder) => !reminder.isArchived)
.toList();
.toList()
.unique((reminder) => reminder.id);
case MobileNotificationTabType.archive:
return reminders.reversed
.where((reminder) => reminder.isArchived)
.toList();
.toList()
.unique((reminder) => reminder.id);
case MobileNotificationTabType.unread:
return reminders.reversed
.where((reminder) => !reminder.isRead)
.toList();
.toList()
.unique((reminder) => reminder.id);
}
}
}
class MultiSelectNotificationTab extends StatelessWidget {
const MultiSelectNotificationTab({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<ReminderBloc, ReminderState>(
builder: (context, state) {
// find the reminders that are not archived or read.
final reminders = state.reminders.reversed
.where((reminder) => !reminder.isArchived || !reminder.isRead)
.toList();
if (reminders.isEmpty) {
// add refresh indicator to the empty notification.
return const SizedBox.shrink();
}
return ListView.separated(
itemCount: reminders.length,
separatorBuilder: (context, index) => const VSpace(8.0),
itemBuilder: (context, index) {
final reminder = reminders[index];
return MultiSelectNotificationItem(
key: ValueKey(reminder.id),
reminder: reminder,
);
},
);
},
);
}
}

View File

@ -1,6 +1,9 @@
export 'empty.dart';
export 'header.dart';
export 'multi_select_notification_item.dart';
export 'notification_item.dart';
export 'settings_popup_menu.dart';
export 'shared.dart';
export 'slide_actions.dart';
export 'tab.dart';
export 'tab_bar.dart';

View File

@ -0,0 +1,47 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class NavigationBarButton extends StatelessWidget {
const NavigationBarButton({
super.key,
required this.text,
required this.icon,
required this.onTap,
this.enable = true,
});
final String text;
final FlowySvgData icon;
final VoidCallback onTap;
final bool enable;
@override
Widget build(BuildContext context) {
return Opacity(
opacity: enable ? 1.0 : 0.3,
child: Container(
height: 40,
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(color: Color(0x3F1F2329)),
borderRadius: BorderRadius.circular(10),
),
),
child: FlowyButton(
useIntrinsicWidth: true,
expandText: false,
iconPadding: 8,
leftIcon: FlowySvg(icon),
onTap: enable ? onTap : null,
text: FlowyText(
text,
fontSize: 15.0,
figmaLineHeight: 18.0,
fontWeight: FontWeight.w400,
),
),
),
);
}
}

View File

@ -10,6 +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_multiple_select_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';
@ -92,6 +93,9 @@ GoRouter generateRouter(Widget child) {
_mobileCalendarEventsPageRoute(),
_mobileBlockSettingsPageRoute(),
// notifications
_mobileNotificationMultiSelectPageRoute(),
],
// Desktop and Mobile
@ -181,6 +185,18 @@ GoRoute _mobileHomeSettingPageRoute() {
);
}
GoRoute _mobileNotificationMultiSelectPageRoute() {
return GoRoute(
parentNavigatorKey: AppGlobals.rootNavKey,
path: MobileNotificationsMultiSelectScreen.routeName,
pageBuilder: (context, state) {
return const MaterialExtendedPage(
child: MobileNotificationsMultiSelectScreen(),
);
},
);
}
GoRoute _mobileCloudSettingAppFlowyCloudPageRoute() {
return GoRoute(
parentNavigatorKey: AppGlobals.rootNavKey,

View File

@ -155,57 +155,46 @@ 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();
markAsRead: (reminderIds) async {
final reminders = await _onMarkAsRead(reminderIds: reminderIds);
emit(
state.copyWith(
reminders: reminder,
reminders: reminders,
),
);
},
archive: (reminderIds) async {
final reminders = await _onArchived(
isArchived: true,
reminderIds: reminderIds,
);
emit(
state.copyWith(
reminders: reminders,
),
);
},
markAllRead: () async {
final reminders = await _onMarkAsRead();
emit(
state.copyWith(
reminders: reminders,
),
);
},
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();
final reminders = await _onArchived(isArchived: true);
emit(
state.copyWith(
reminders: reminder,
reminders: reminders,
),
);
},
unarchiveAll: () async {
final reminders = await _onArchived(isArchived: false);
emit(
state.copyWith(
reminders: reminders,
),
);
},
@ -222,6 +211,96 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
);
}
/// Mark the reminder as read
///
/// If the [reminderIds] is null, all unread reminders will be marked as read
/// Otherwise, only the reminders with the given IDs will be marked as read
Future<List<ReminderPB>> _onMarkAsRead({
List<String>? reminderIds,
}) async {
final Iterable<ReminderPB> remindersToUpdate;
if (reminderIds != null) {
remindersToUpdate = state.reminders.where(
(reminder) => reminderIds.contains(reminder.id) && !reminder.isRead,
);
} else {
// Get all reminders that are not matching the isArchived flag
remindersToUpdate = state.reminders.where(
(reminder) => !reminder.isRead,
);
}
for (final reminder in remindersToUpdate) {
reminder.isRead = true;
await _reminderService.updateReminder(reminder: reminder);
Log.info('Mark reminder ${reminder.id} as read');
}
return state.reminders.map((e) {
if (reminderIds != null && !reminderIds.contains(e.id)) {
return e;
}
if (e.isRead) {
return e;
}
e.freeze();
return e.rebuild((update) {
update.isRead = true;
});
}).toList();
}
/// Archive or unarchive reminders
///
/// If the [reminderIds] is null, all reminders will be archived
/// Otherwise, only the reminders with the given IDs will be archived or unarchived
Future<List<ReminderPB>> _onArchived({
required bool isArchived,
List<String>? reminderIds,
}) async {
final Iterable<ReminderPB> remindersToUpdate;
if (reminderIds != null) {
remindersToUpdate = state.reminders.where(
(reminder) =>
reminderIds.contains(reminder.id) &&
reminder.isArchived != isArchived,
);
} else {
// Get all reminders that are not matching the isArchived flag
remindersToUpdate = state.reminders.where(
(reminder) => reminder.isArchived != isArchived,
);
}
for (final reminder in remindersToUpdate) {
reminder.isRead = isArchived;
reminder.meta[ReminderMetaKeys.isArchived] = isArchived.toString();
await _reminderService.updateReminder(reminder: reminder);
Log.info('Reminder ${reminder.id} is archived: $isArchived');
}
return state.reminders.map((e) {
if (reminderIds != null && !reminderIds.contains(e.id)) {
return e;
}
if (e.isArchived == isArchived) {
return e;
}
e.freeze();
return e.rebuild((update) {
update.isRead = isArchived;
update.meta[ReminderMetaKeys.isArchived] = isArchived.toString();
});
}).toList();
}
Timer _periodicCheck() {
return Timer.periodic(
const Duration(minutes: 1),
@ -285,17 +364,30 @@ class ReminderEvent with _$ReminderEvent {
// Update a reminder (eg. isAck, isRead, etc.)
const factory ReminderEvent.update(ReminderUpdate update) = _Update;
// Mark all unread reminders as read
// Event to mark specific reminders as read, takes a list of reminder IDs
const factory ReminderEvent.markAsRead(List<String> reminderIds) =
_MarkAsRead;
// Event to mark all unread reminders as read
const factory ReminderEvent.markAllRead() = _MarkAllRead;
// Event to archive specific reminders, takes a list of reminder IDs
const factory ReminderEvent.archive(List<String> reminderIds) = _Archive;
// Event to archive all reminders
const factory ReminderEvent.archiveAll() = _ArchiveAll;
// Event to unarchive all reminders
const factory ReminderEvent.unarchiveAll() = _UnarchiveAll;
// Event to handle reminder press action
const factory ReminderEvent.pressReminder({
required String reminderId,
@Default(null) int? path,
@Default(null) ViewPB? view,
}) = _PressReminder;
// Event to refresh reminders
const factory ReminderEvent.refresh() = _Refresh;
}

View File

@ -53,8 +53,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: c8e0ca9
resolved-ref: c8e0ca946b99b59286fabb811c39de5347f8bebd
ref: "7202c34"
resolved-ref: "7202c340724eef2c20e3f32ec75c0d91e4290cb0"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "3.1.0"

View File

@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.6.6
version: 0.6.7
environment:
flutter: ">=3.22.0"
@ -196,7 +196,7 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "c8e0ca9"
ref: "7202c34"
appflowy_editor_plugins:
git:

View File

@ -1,4 +1,8 @@
<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"/>
<path d="M1.80005 3.25L3.02359 4.43061C3.13543 4.53852 3.31311 4.53694 3.42301 4.42704L5.60005 2.25" stroke="#1E2022" stroke-width="1.4" stroke-linecap="round"/>
<path d="M1.80005 9.625L3.02359 10.8056C3.13543 10.9135 3.31311 10.9119 3.42301 10.802L5.60005 8.625" stroke="#1E2022" stroke-width="1.4" stroke-linecap="round"/>
<path d="M1.80005 16L3.02359 17.1806C3.13543 17.2885 3.31311 17.2869 3.42301 17.177L5.60005 15" stroke="#1E2022" stroke-width="1.4" stroke-linecap="round"/>
<path d="M8.19995 3.5H17.7" stroke="#1E2022" stroke-width="1.4" stroke-linecap="round"/>
<path d="M8.19995 10H17.7" stroke="#1E2022" stroke-width="1.4" stroke-linecap="round"/>
<path d="M8.19995 16.5H17.7" stroke="#1E2022" stroke-width="1.4" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 789 B

After

Width:  |  Height:  |  Size: 852 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">
<rect width="20" height="20" rx="10" fill="#00BCF0"/>
<path d="M6.25 10.3125L9.27885 13.125L14.6875 7.5" stroke="white" stroke-width="1.6875" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="19" height="19" rx="9.5" stroke="#1F2329" stroke-opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 197 B

View File

@ -964,7 +964,7 @@
},
"action": {
"markAsRead": "Mark as read",
"multipleChoice": "Multiple choice",
"multipleChoice": "Select more",
"archive": "Archive"
},
"settings": {