mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
7261d1e8da
commit
9fbba5fb60
@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
|||||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||||
LIB_NAME = "dart_ffi"
|
LIB_NAME = "dart_ffi"
|
||||||
APPFLOWY_VERSION = "0.6.6"
|
APPFLOWY_VERSION = "0.6.7"
|
||||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||||
PRODUCT_NAME = "AppFlowy"
|
PRODUCT_NAME = "AppFlowy"
|
||||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||||
|
@ -9,7 +9,7 @@ class AnimatedGestureDetector extends StatefulWidget {
|
|||||||
this.duration = const Duration(milliseconds: 100),
|
this.duration = const Duration(milliseconds: 100),
|
||||||
this.alignment = Alignment.center,
|
this.alignment = Alignment.center,
|
||||||
this.behavior = HitTestBehavior.opaque,
|
this.behavior = HitTestBehavior.opaque,
|
||||||
required this.onTapUp,
|
this.onTapUp,
|
||||||
required this.child,
|
required this.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ class AnimatedGestureDetector extends StatefulWidget {
|
|||||||
final Alignment alignment;
|
final Alignment alignment;
|
||||||
final bool feedback;
|
final bool feedback;
|
||||||
final HitTestBehavior behavior;
|
final HitTestBehavior behavior;
|
||||||
final VoidCallback onTapUp;
|
final VoidCallback? onTapUp;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AnimatedGestureDetector> createState() =>
|
State<AnimatedGestureDetector> createState() =>
|
||||||
@ -38,7 +38,7 @@ class _AnimatedGestureDetectorState extends State<AnimatedGestureDetector> {
|
|||||||
|
|
||||||
HapticFeedbackType.light.call();
|
HapticFeedbackType.light.call();
|
||||||
|
|
||||||
widget.onTapUp();
|
widget.onTapUp?.call();
|
||||||
},
|
},
|
||||||
onTapDown: (details) {
|
onTapDown: (details) {
|
||||||
setState(() => scale = widget.scaleFactor);
|
setState(() => scale = widget.scaleFactor);
|
||||||
|
@ -1,17 +1,30 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/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/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
import 'package:appflowy/util/theme_extension.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_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
enum BottomNavigationBarActionType {
|
||||||
|
home,
|
||||||
|
notificationMultiSelect,
|
||||||
|
}
|
||||||
|
|
||||||
final PropertyValueNotifier<ViewLayoutPB?> createNewPageNotifier =
|
final PropertyValueNotifier<ViewLayoutPB?> createNewPageNotifier =
|
||||||
PropertyValueNotifier(null);
|
PropertyValueNotifier(null);
|
||||||
|
final ValueNotifier<BottomNavigationBarActionType> bottomNavigationBarType =
|
||||||
|
ValueNotifier(BottomNavigationBarActionType.home);
|
||||||
|
|
||||||
const _homeLabel = 'home';
|
const _homeLabel = 'home';
|
||||||
const _addLabel = 'add';
|
const _addLabel = 'add';
|
||||||
@ -37,7 +50,7 @@ final _items = <BottomNavigationBarItem>[
|
|||||||
|
|
||||||
/// Builds the "shell" for the app by building a Scaffold with a
|
/// Builds the "shell" for the app by building a Scaffold with a
|
||||||
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
|
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
|
||||||
class MobileBottomNavigationBar extends StatelessWidget {
|
class MobileBottomNavigationBar extends StatefulWidget {
|
||||||
/// Constructs an [MobileBottomNavigationBar].
|
/// Constructs an [MobileBottomNavigationBar].
|
||||||
const MobileBottomNavigationBar({
|
const MobileBottomNavigationBar({
|
||||||
required this.navigationShell,
|
required this.navigationShell,
|
||||||
@ -47,68 +60,76 @@ class MobileBottomNavigationBar extends StatelessWidget {
|
|||||||
/// The navigation shell and container for the branch Navigators.
|
/// The navigation shell and container for the branch Navigators.
|
||||||
final StatefulNavigationShell navigationShell;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isLightMode = Theme.of(context).isLightMode;
|
_bottomNavigationBar = switch (bottomNavigationBarType.value) {
|
||||||
final backgroundColor = isLightMode
|
BottomNavigationBarActionType.home =>
|
||||||
? Colors.white.withOpacity(0.95)
|
_buildHomePageNavigationBar(context),
|
||||||
: const Color(0xFF23262B).withOpacity(0.95);
|
BottomNavigationBarActionType.notificationMultiSelect =>
|
||||||
final borderColor = isLightMode
|
_buildNotificationNavigationBar(context),
|
||||||
? const Color(0x141F2329)
|
};
|
||||||
: const Color(0xFF23262B).withOpacity(0.5);
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: navigationShell,
|
body: widget.navigationShell,
|
||||||
extendBody: true,
|
extendBody: true,
|
||||||
bottomNavigationBar: ClipRRect(
|
bottomNavigationBar: AnimatedSwitcher(
|
||||||
child: BackdropFilter(
|
duration: const Duration(milliseconds: 250),
|
||||||
filter: ImageFilter.blur(
|
switchInCurve: Curves.easeInOut,
|
||||||
sigmaX: 3,
|
switchOutCurve: Curves.easeInOut,
|
||||||
sigmaY: 3,
|
transitionBuilder: _transitionBuilder,
|
||||||
),
|
child: _bottomNavigationBar,
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigate to the current location of the branch at the provided index when
|
Widget _buildHomePageNavigationBar(BuildContext context) {
|
||||||
/// tapping an item in the BottomNavigationBar.
|
return _HomePageNavigationBar(
|
||||||
void _onTap(BuildContext context, int bottomBarIndex) {
|
navigationShell: widget.navigationShell,
|
||||||
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 _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 {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,17 @@
|
|||||||
import 'package:appflowy/mobile/application/user_profile/user_profile_bloc.dart';
|
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/notifications/widgets/widgets.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||||
import 'package:appflowy/startup/startup.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/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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
final PropertyValueNotifier<List<String>> mSelectedNotificationIds =
|
||||||
|
PropertyValueNotifier([]);
|
||||||
|
|
||||||
class MobileNotificationsScreenV2 extends StatefulWidget {
|
class MobileNotificationsScreenV2 extends StatefulWidget {
|
||||||
const MobileNotificationsScreenV2({super.key});
|
const MobileNotificationsScreenV2({super.key});
|
||||||
|
|
||||||
@ -20,29 +24,33 @@ class MobileNotificationsScreenV2 extends StatefulWidget {
|
|||||||
|
|
||||||
class _MobileNotificationsScreenV2State
|
class _MobileNotificationsScreenV2State
|
||||||
extends State<MobileNotificationsScreenV2>
|
extends State<MobileNotificationsScreenV2>
|
||||||
with SingleTickerProviderStateMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
|
||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider<UserProfileBloc>(
|
BlocProvider<UserProfileBloc>(
|
||||||
create: (context) =>
|
create: (context) => UserProfileBloc()
|
||||||
UserProfileBloc()..add(const UserProfileEvent.started()),
|
..add(
|
||||||
|
const UserProfileEvent.started(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
BlocProvider<ReminderBloc>.value(value: getIt<ReminderBloc>()),
|
BlocProvider<ReminderBloc>.value(value: getIt<ReminderBloc>()),
|
||||||
BlocProvider<NotificationFilterBloc>(
|
|
||||||
create: (_) => NotificationFilterBloc(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
child: BlocBuilder<UserProfileBloc, UserProfileState>(
|
child: ValueListenableBuilder(
|
||||||
builder: (context, state) {
|
valueListenable: bottomNavigationBarType,
|
||||||
return state.maybeWhen(
|
builder: (_, value, __) {
|
||||||
orElse: () =>
|
switch (value) {
|
||||||
const Center(child: CircularProgressIndicator.adaptive()),
|
case BottomNavigationBarActionType.home:
|
||||||
workspaceFailure: () => const WorkspaceFailedScreen(),
|
return const MobileNotificationsTab();
|
||||||
success: (workspaceSetting, userProfile) =>
|
case BottomNavigationBarActionType.notificationMultiSelect:
|
||||||
const MobileNotificationsTab(),
|
return const MobileNotificationMultiSelect();
|
||||||
);
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -52,11 +60,8 @@ class _MobileNotificationsScreenV2State
|
|||||||
class MobileNotificationsTab extends StatefulWidget {
|
class MobileNotificationsTab extends StatefulWidget {
|
||||||
const MobileNotificationsTab({
|
const MobileNotificationsTab({
|
||||||
super.key,
|
super.key,
|
||||||
// required this.userProfile,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// final UserProfilePB userProfile;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MobileNotificationsTab> createState() => _MobileNotificationsTabState();
|
State<MobileNotificationsTab> createState() => _MobileNotificationsTabState();
|
||||||
}
|
}
|
||||||
@ -79,12 +84,10 @@ class _MobileNotificationsTabState extends State<MobileNotificationsTab>
|
|||||||
length: 3,
|
length: 3,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
tabController.addListener(_onTabChange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
tabController.removeListener(_onTabChange);
|
|
||||||
tabController.dispose();
|
tabController.dispose();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@ -114,6 +117,4 @@ class _MobileNotificationsTabState extends State<MobileNotificationsTab>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onTabChange() {}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.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/notifications/widgets/settings_popup_menu.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,10 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.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/base/gesture.dart';
|
||||||
import 'package:appflowy/mobile/presentation/notifications/widgets/color.dart';
|
|
||||||
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.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/user/application/reminder/reminder_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -60,55 +52,61 @@ class NotificationItem extends StatelessWidget {
|
|||||||
child: _SlidableNotificationItem(
|
child: _SlidableNotificationItem(
|
||||||
tabType: tabType,
|
tabType: tabType,
|
||||||
reminder: reminder,
|
reminder: reminder,
|
||||||
child: Row(
|
child: _InnerNotificationItem(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
tabType: tabType,
|
||||||
children: [
|
reminder: reminder,
|
||||||
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(
|
return AnimatedGestureDetector(
|
||||||
scaleFactor: 0.99,
|
scaleFactor: 0.99,
|
||||||
onTapUp: () => _onMarkAsRead(context),
|
|
||||||
child: child,
|
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) {
|
class _InnerNotificationItem extends StatelessWidget {
|
||||||
if (reminder.isRead) {
|
const _InnerNotificationItem({
|
||||||
return;
|
required this.reminder,
|
||||||
}
|
required this.tabType,
|
||||||
|
});
|
||||||
|
|
||||||
showToastNotification(
|
final MobileNotificationTabType tabType;
|
||||||
context,
|
final ReminderPB reminder;
|
||||||
message: LocaleKeys.settings_notifications_markAsReadNotifications_success
|
|
||||||
.tr(),
|
@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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// only show the actions in the inbox tab
|
|
||||||
|
|
||||||
final List<NotificationPaneActionType> actions = switch (tabType) {
|
final List<NotificationPaneActionType> actions = switch (tabType) {
|
||||||
MobileNotificationTabType.inbox => [
|
MobileNotificationTabType.inbox => [
|
||||||
NotificationPaneActionType.more,
|
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -5,6 +5,7 @@ import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
|||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -13,6 +14,8 @@ enum _NotificationSettingsPopupMenuItem {
|
|||||||
settings,
|
settings,
|
||||||
markAllAsRead,
|
markAllAsRead,
|
||||||
archiveAll,
|
archiveAll,
|
||||||
|
// only visible in debug mode
|
||||||
|
unarchiveAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationSettingsPopupMenu extends StatelessWidget {
|
class NotificationSettingsPopupMenu extends StatelessWidget {
|
||||||
@ -56,6 +59,15 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
|
|||||||
svg: FlowySvgs.m_notification_archived_s,
|
svg: FlowySvgs.m_notification_archived_s,
|
||||||
text: LocaleKeys.settings_notifications_settings_archiveAll.tr(),
|
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) {
|
onSelected: (_NotificationSettingsPopupMenuItem value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
@ -68,6 +80,9 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
|
|||||||
case _NotificationSettingsPopupMenuItem.settings:
|
case _NotificationSettingsPopupMenuItem.settings:
|
||||||
context.push(MobileHomeSettingPage.routeName);
|
context.push(MobileHomeSettingPage.routeName);
|
||||||
break;
|
break;
|
||||||
|
case _NotificationSettingsPopupMenuItem.unarchiveAll:
|
||||||
|
_onUnarchiveAll(context);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -108,6 +123,19 @@ class NotificationSettingsPopupMenu extends StatelessWidget {
|
|||||||
|
|
||||||
context.read<ReminderBloc>().add(const ReminderEvent.archiveAll());
|
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 {
|
class _PopupButton extends StatelessWidget {
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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/bottom_sheet/bottom_sheet.dart';
|
||||||
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.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/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/mobile/presentation/widgets/widgets.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||||
@ -88,13 +89,21 @@ enum NotificationPaneActionType {
|
|||||||
showDivider: false,
|
showDivider: false,
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
builder: (context) {
|
builder: (_) {
|
||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider.value(value: reminderBloc),
|
BlocProvider.value(value: reminderBloc),
|
||||||
BlocProvider.value(value: notificationReminderBloc),
|
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 {
|
class _NotificationMoreActions extends StatelessWidget {
|
||||||
const _NotificationMoreActions();
|
const _NotificationMoreActions({
|
||||||
|
required this.onClickMultipleChoice,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onClickMultipleChoice;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -172,6 +185,8 @@ class _NotificationMoreActions extends StatelessWidget {
|
|||||||
|
|
||||||
void _onMultipleChoice(BuildContext context) {
|
void _onMultipleChoice(BuildContext context) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
onClickMultipleChoice();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onArchive(BuildContext context) {
|
void _onArchive(BuildContext context) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
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/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_bloc.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
@ -37,25 +37,28 @@ class _NotificationTabState extends State<NotificationTab>
|
|||||||
final reminders = _filterReminders(state.reminders);
|
final reminders = _filterReminders(state.reminders);
|
||||||
|
|
||||||
if (reminders.isEmpty) {
|
if (reminders.isEmpty) {
|
||||||
|
// add refresh indicator to the empty notification.
|
||||||
return EmptyNotification(
|
return EmptyNotification(
|
||||||
type: widget.tabType,
|
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(
|
return RefreshIndicator.adaptive(
|
||||||
onRefresh: () async => _onRefresh(context),
|
onRefresh: () async => _onRefresh(context),
|
||||||
child: ListView.separated(
|
child: child,
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -82,15 +85,53 @@ class _NotificationTabState extends State<NotificationTab>
|
|||||||
case MobileNotificationTabType.inbox:
|
case MobileNotificationTabType.inbox:
|
||||||
return reminders.reversed
|
return reminders.reversed
|
||||||
.where((reminder) => !reminder.isArchived)
|
.where((reminder) => !reminder.isArchived)
|
||||||
.toList();
|
.toList()
|
||||||
|
.unique((reminder) => reminder.id);
|
||||||
case MobileNotificationTabType.archive:
|
case MobileNotificationTabType.archive:
|
||||||
return reminders.reversed
|
return reminders.reversed
|
||||||
.where((reminder) => reminder.isArchived)
|
.where((reminder) => reminder.isArchived)
|
||||||
.toList();
|
.toList()
|
||||||
|
.unique((reminder) => reminder.id);
|
||||||
case MobileNotificationTabType.unread:
|
case MobileNotificationTabType.unread:
|
||||||
return reminders.reversed
|
return reminders.reversed
|
||||||
.where((reminder) => !reminder.isRead)
|
.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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
export 'empty.dart';
|
export 'empty.dart';
|
||||||
export 'header.dart';
|
export 'header.dart';
|
||||||
|
export 'multi_select_notification_item.dart';
|
||||||
|
export 'notification_item.dart';
|
||||||
export 'settings_popup_menu.dart';
|
export 'settings_popup_menu.dart';
|
||||||
|
export 'shared.dart';
|
||||||
export 'slide_actions.dart';
|
export 'slide_actions.dart';
|
||||||
export 'tab.dart';
|
export 'tab.dart';
|
||||||
export 'tab_bar.dart';
|
export 'tab_bar.dart';
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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_calendar_screen.dart';
|
||||||
import 'package:appflowy/mobile/presentation/database/mobile_grid_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/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/notifications/mobile_notifications_screen.dart';
|
||||||
import 'package:appflowy/mobile/presentation/presentation.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/cloud/appflowy_cloud_page.dart';
|
||||||
@ -92,6 +93,9 @@ GoRouter generateRouter(Widget child) {
|
|||||||
_mobileCalendarEventsPageRoute(),
|
_mobileCalendarEventsPageRoute(),
|
||||||
|
|
||||||
_mobileBlockSettingsPageRoute(),
|
_mobileBlockSettingsPageRoute(),
|
||||||
|
|
||||||
|
// notifications
|
||||||
|
_mobileNotificationMultiSelectPageRoute(),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Desktop and Mobile
|
// 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() {
|
GoRoute _mobileCloudSettingAppFlowyCloudPageRoute() {
|
||||||
return GoRoute(
|
return GoRoute(
|
||||||
parentNavigatorKey: AppGlobals.rootNavKey,
|
parentNavigatorKey: AppGlobals.rootNavKey,
|
||||||
|
@ -155,57 +155,46 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
markAllRead: () async {
|
markAsRead: (reminderIds) async {
|
||||||
final unreadReminders = state.reminders.where(
|
final reminders = await _onMarkAsRead(reminderIds: reminderIds);
|
||||||
(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(
|
emit(
|
||||||
state.copyWith(
|
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 {
|
archiveAll: () async {
|
||||||
final unArchivedReminders = state.reminders.where(
|
final reminders = await _onArchived(isArchived: true);
|
||||||
(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(
|
emit(
|
||||||
state.copyWith(
|
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() {
|
Timer _periodicCheck() {
|
||||||
return Timer.periodic(
|
return Timer.periodic(
|
||||||
const Duration(minutes: 1),
|
const Duration(minutes: 1),
|
||||||
@ -285,17 +364,30 @@ class ReminderEvent with _$ReminderEvent {
|
|||||||
// Update a reminder (eg. isAck, isRead, etc.)
|
// Update a reminder (eg. isAck, isRead, etc.)
|
||||||
const factory ReminderEvent.update(ReminderUpdate update) = _Update;
|
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;
|
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;
|
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({
|
const factory ReminderEvent.pressReminder({
|
||||||
required String reminderId,
|
required String reminderId,
|
||||||
@Default(null) int? path,
|
@Default(null) int? path,
|
||||||
@Default(null) ViewPB? view,
|
@Default(null) ViewPB? view,
|
||||||
}) = _PressReminder;
|
}) = _PressReminder;
|
||||||
|
|
||||||
|
// Event to refresh reminders
|
||||||
const factory ReminderEvent.refresh() = _Refresh;
|
const factory ReminderEvent.refresh() = _Refresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,8 +53,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: c8e0ca9
|
ref: "7202c34"
|
||||||
resolved-ref: c8e0ca946b99b59286fabb811c39de5347f8bebd
|
resolved-ref: "7202c340724eef2c20e3f32ec75c0d91e4290cb0"
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||||
source: git
|
source: git
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
|
@ -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.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 0.6.6
|
version: 0.6.7
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
flutter: ">=3.22.0"
|
flutter: ">=3.22.0"
|
||||||
@ -196,7 +196,7 @@ dependency_overrides:
|
|||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
ref: "c8e0ca9"
|
ref: "7202c34"
|
||||||
|
|
||||||
appflowy_editor_plugins:
|
appflowy_editor_plugins:
|
||||||
git:
|
git:
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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="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="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 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>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 789 B After Width: | Height: | Size: 852 B |
@ -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 |
@ -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 |
@ -964,7 +964,7 @@
|
|||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"markAsRead": "Mark as read",
|
"markAsRead": "Mark as read",
|
||||||
"multipleChoice": "Multiple choice",
|
"multipleChoice": "Select more",
|
||||||
"archive": "Archive"
|
"archive": "Archive"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user