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_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"
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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/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() {}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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/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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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/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) {
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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_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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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 |
@ -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": {
|
||||
"markAsRead": "Mark as read",
|
||||
"multipleChoice": "Multiple choice",
|
||||
"multipleChoice": "Select more",
|
||||
"archive": "Archive"
|
||||
},
|
||||
"settings": {
|
||||
|
Loading…
Reference in New Issue
Block a user