mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: mobile notifications screen (#4100)
* fix: update username on mobile header on change * feat: notifications page * feat: refactor and refinement * fix: code review
This commit is contained in:
@ -0,0 +1,65 @@
|
|||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'user_profile_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
|
||||||
|
UserProfileBloc() : super(const _Initial()) {
|
||||||
|
on<UserProfileEvent>((event, emit) async {
|
||||||
|
await event.when(
|
||||||
|
started: () async => _initalize(emit),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initalize(Emitter<UserProfileState> emit) async {
|
||||||
|
emit(const UserProfileState.loading());
|
||||||
|
|
||||||
|
final workspaceOrFailure =
|
||||||
|
await FolderEventGetCurrentWorkspaceSetting().send();
|
||||||
|
|
||||||
|
final userOrFailure = await getIt<AuthService>().getUser();
|
||||||
|
|
||||||
|
final workspaceSetting = workspaceOrFailure.fold(
|
||||||
|
(workspaceSettingPB) => workspaceSettingPB,
|
||||||
|
(error) => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final userProfile = userOrFailure.fold(
|
||||||
|
(error) => null,
|
||||||
|
(userProfilePB) => userProfilePB,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (workspaceSetting == null || userProfile == null) {
|
||||||
|
return emit(const UserProfileState.workspaceFailure());
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(
|
||||||
|
UserProfileState.success(
|
||||||
|
workspaceSettings: workspaceSetting,
|
||||||
|
userProfile: userProfile,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class UserProfileEvent with _$UserProfileEvent {
|
||||||
|
const factory UserProfileEvent.started() = _Started;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class UserProfileState with _$UserProfileState {
|
||||||
|
const factory UserProfileState.initial() = _Initial;
|
||||||
|
const factory UserProfileState.loading() = _Loading;
|
||||||
|
const factory UserProfileState.workspaceFailure() = _WorkspaceFailure;
|
||||||
|
const factory UserProfileState.success({
|
||||||
|
required WorkspaceSettingPB workspaceSettings,
|
||||||
|
required UserProfilePB userProfile,
|
||||||
|
}) = _Success;
|
||||||
|
}
|
@ -14,16 +14,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class MobileHomePageHeader extends StatelessWidget {
|
class MobileHomePageHeader extends StatelessWidget {
|
||||||
const MobileHomePageHeader({
|
const MobileHomePageHeader({super.key, required this.userProfile});
|
||||||
super.key,
|
|
||||||
required this.userProfile,
|
|
||||||
});
|
|
||||||
|
|
||||||
final UserProfilePB userProfile;
|
final UserProfilePB userProfile;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => getIt<SettingsUserViewBloc>(param1: userProfile)
|
create: (context) => getIt<SettingsUserViewBloc>(param1: userProfile)
|
||||||
..add(const SettingsUserEvent.initial()),
|
..add(const SettingsUserEvent.initial()),
|
||||||
@ -41,29 +37,23 @@ class MobileHomePageHeader extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const FlowyText.medium(
|
const FlowyText.medium('AppFlowy', fontSize: 18),
|
||||||
'AppFlowy',
|
|
||||||
fontSize: 18,
|
|
||||||
),
|
|
||||||
const VSpace(4),
|
const VSpace(4),
|
||||||
FlowyText.regular(
|
FlowyText.regular(
|
||||||
userProfile.email.isNotEmpty
|
userProfile.email.isNotEmpty
|
||||||
? userProfile.email
|
? state.userProfile.email
|
||||||
: userProfile.name,
|
: state.userProfile.name,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: theme.colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () =>
|
||||||
context.push(MobileHomeSettingPage.routeName);
|
context.push(MobileHomeSettingPage.routeName),
|
||||||
},
|
icon: const FlowySvg(FlowySvgs.m_setting_m),
|
||||||
icon: const FlowySvg(
|
|
||||||
FlowySvgs.m_setting_m,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -0,0 +1,167 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/application/user_profile/user_profile_bloc.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/notifications/widgets/mobile_notification_tab_bar.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/application/menu/menu_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_view.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class MobileNotificationsScreen extends StatefulWidget {
|
||||||
|
const MobileNotificationsScreen({super.key});
|
||||||
|
|
||||||
|
static const routeName = '/notifications';
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileNotificationsScreen> createState() =>
|
||||||
|
_MobileNotificationsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileNotificationsScreenState extends State<MobileNotificationsScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
final ReminderBloc _reminderBloc = getIt<ReminderBloc>();
|
||||||
|
late final TabController _controller = TabController(length: 2, vsync: this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider<UserProfileBloc>(
|
||||||
|
create: (context) =>
|
||||||
|
UserProfileBloc()..add(const UserProfileEvent.started()),
|
||||||
|
),
|
||||||
|
BlocProvider<ReminderBloc>.value(value: _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) =>
|
||||||
|
_NotificationScreenContent(
|
||||||
|
workspaceSetting: workspaceSetting,
|
||||||
|
userProfile: userProfile,
|
||||||
|
controller: _controller,
|
||||||
|
reminderBloc: _reminderBloc,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotificationScreenContent extends StatelessWidget {
|
||||||
|
const _NotificationScreenContent({
|
||||||
|
required this.workspaceSetting,
|
||||||
|
required this.userProfile,
|
||||||
|
required this.controller,
|
||||||
|
required this.reminderBloc,
|
||||||
|
});
|
||||||
|
|
||||||
|
final WorkspaceSettingPB workspaceSetting;
|
||||||
|
final UserProfilePB userProfile;
|
||||||
|
final TabController controller;
|
||||||
|
final ReminderBloc reminderBloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => MenuBloc(
|
||||||
|
workspaceId: workspaceSetting.workspaceId,
|
||||||
|
user: userProfile,
|
||||||
|
)..add(const MenuEvent.initial()),
|
||||||
|
child: BlocBuilder<MenuBloc, MenuState>(
|
||||||
|
builder: (context, menuState) =>
|
||||||
|
BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
|
||||||
|
builder: (context, filterState) =>
|
||||||
|
BlocBuilder<ReminderBloc, ReminderState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
// Workaround for rebuilding the Blocks by brightness
|
||||||
|
Theme.of(context).brightness;
|
||||||
|
|
||||||
|
final List<ReminderPB> pastReminders = state.pastReminders
|
||||||
|
.where(
|
||||||
|
(r) => filterState.showUnreadsOnly ? !r.isRead : true,
|
||||||
|
)
|
||||||
|
.sortByScheduledAt();
|
||||||
|
|
||||||
|
final List<ReminderPB> upcomingReminders =
|
||||||
|
state.upcomingReminders.sortByScheduledAt();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
elevation: 0,
|
||||||
|
title: Text(LocaleKeys.notificationHub_mobile_title.tr()),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
MobileNotificationTabBar(controller: controller),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
controller: controller,
|
||||||
|
children: [
|
||||||
|
NotificationsView(
|
||||||
|
shownReminders: pastReminders,
|
||||||
|
reminderBloc: reminderBloc,
|
||||||
|
views: menuState.views,
|
||||||
|
onAction: _onAction,
|
||||||
|
onDelete: _onDelete,
|
||||||
|
onReadChanged: _onReadChanged,
|
||||||
|
actionBar: InboxActionBar(
|
||||||
|
hasUnreads: state.hasUnreads,
|
||||||
|
showUnreadsOnly: filterState.showUnreadsOnly,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NotificationsView(
|
||||||
|
shownReminders: upcomingReminders,
|
||||||
|
reminderBloc: reminderBloc,
|
||||||
|
views: menuState.views,
|
||||||
|
isUpcoming: true,
|
||||||
|
onAction: _onAction,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAction(ReminderPB reminder, int? path, ViewPB? view) =>
|
||||||
|
reminderBloc.add(
|
||||||
|
ReminderEvent.pressReminder(
|
||||||
|
reminderId: reminder.id,
|
||||||
|
path: path,
|
||||||
|
view: view,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
void _onDelete(ReminderPB reminder) =>
|
||||||
|
reminderBloc.add(ReminderEvent.remove(reminder: reminder));
|
||||||
|
|
||||||
|
void _onReadChanged(ReminderPB reminder, bool isRead) => reminderBloc.add(
|
||||||
|
ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)),
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MobileNotificationTabBar extends StatefulWidget {
|
||||||
|
const MobileNotificationTabBar({super.key, required this.controller});
|
||||||
|
|
||||||
|
final TabController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileNotificationTabBar> createState() =>
|
||||||
|
_MobileNotificationTabBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileNotificationTabBarState extends State<MobileNotificationTabBar> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.controller.addListener(_updateState);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateState() => setState(() {});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final borderSide = BorderSide(
|
||||||
|
color: AFThemeExtension.of(context).calloutBGColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: borderSide,
|
||||||
|
top: borderSide,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TabBar(
|
||||||
|
controller: widget.controller,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
labelPadding: EdgeInsets.zero,
|
||||||
|
indicatorSize: TabBarIndicatorSize.label,
|
||||||
|
indicator: UnderlineTabIndicator(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isScrollable: true,
|
||||||
|
tabs: [
|
||||||
|
FlowyTabItem(
|
||||||
|
label: LocaleKeys.notificationHub_tabs_inbox.tr(),
|
||||||
|
isSelected: widget.controller.index == 0,
|
||||||
|
),
|
||||||
|
FlowyTabItem(
|
||||||
|
label: LocaleKeys.notificationHub_tabs_upcoming.tr(),
|
||||||
|
isSelected: widget.controller.index == 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -50,13 +50,9 @@ class PersonalInfoSettingGroup extends StatelessWidget {
|
|||||||
return EditUsernameBottomSheet(
|
return EditUsernameBottomSheet(
|
||||||
context,
|
context,
|
||||||
userName: userName,
|
userName: userName,
|
||||||
onSubmitted: (value) {
|
onSubmitted: (value) => context
|
||||||
context.read<SettingsUserViewBloc>().add(
|
.read<SettingsUserViewBloc>()
|
||||||
SettingsUserEvent.updateUserName(
|
.add(SettingsUserEvent.updateUserName(value)),
|
||||||
value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -87,39 +87,37 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
child: BlocListener<NotificationActionBloc, NotificationActionState>(
|
child: BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||||
listener: _onNotificationAction,
|
listener: _onNotificationAction,
|
||||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) => state.loadingState.when(
|
||||||
return state.loadingState.when(
|
loading: () =>
|
||||||
loading: () =>
|
const Center(child: CircularProgressIndicator.adaptive()),
|
||||||
const Center(child: CircularProgressIndicator.adaptive()),
|
finish: (result) => result.fold(
|
||||||
finish: (result) => result.fold(
|
(error) {
|
||||||
(error) {
|
Log.error(error);
|
||||||
Log.error(error);
|
return FlowyErrorPage.message(
|
||||||
return FlowyErrorPage.message(
|
error.toString(),
|
||||||
error.toString(),
|
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
);
|
||||||
|
},
|
||||||
|
(data) {
|
||||||
|
if (state.forceClose) {
|
||||||
|
widget.onDeleted();
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
} else if (documentBloc.editorState == null) {
|
||||||
|
return Center(
|
||||||
|
child: ExportPageWidget(
|
||||||
|
onTap: () async => await _exportPage(data),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
} else {
|
||||||
(data) {
|
editorState = documentBloc.editorState!;
|
||||||
if (state.forceClose) {
|
return _buildEditorPage(
|
||||||
widget.onDeleted();
|
context,
|
||||||
return const SizedBox.shrink();
|
state,
|
||||||
} else if (documentBloc.editorState == null) {
|
);
|
||||||
return Center(
|
}
|
||||||
child: ExportPageWidget(
|
},
|
||||||
onTap: () async => await _exportPage(data),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
} else {
|
|
||||||
editorState = documentBloc.editorState!;
|
|
||||||
return _buildEditorPage(
|
|
||||||
context,
|
|
||||||
state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -46,10 +46,10 @@ class ErrorBlockComponentWidget extends BlockComponentStatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<ErrorBlockComponentWidget> createState() =>
|
State<ErrorBlockComponentWidget> createState() =>
|
||||||
_DividerBlockComponentWidgetState();
|
_ErrorBlockComponentWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DividerBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
|
class _ErrorBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
|
||||||
with BlockComponentConfigurable {
|
with BlockComponentConfigurable {
|
||||||
@override
|
@override
|
||||||
BlockComponentConfiguration get configuration => widget.configuration;
|
BlockComponentConfiguration get configuration => widget.configuration;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/user_settings_service.dart';
|
import 'package:appflowy/user/application/user_settings_service.dart';
|
||||||
|
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
|
||||||
|
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/notifications/notification_service.dart';
|
import 'package:appflowy/workspace/application/notifications/notification_service.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/application/settings/notifications/notification_settings_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
|
||||||
@ -137,18 +140,44 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
|
|||||||
BlocProvider<DocumentAppearanceCubit>(
|
BlocProvider<DocumentAppearanceCubit>(
|
||||||
create: (_) => DocumentAppearanceCubit()..fetch(),
|
create: (_) => DocumentAppearanceCubit()..fetch(),
|
||||||
),
|
),
|
||||||
|
BlocProvider.value(value: getIt<NotificationActionBloc>()),
|
||||||
],
|
],
|
||||||
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
child: BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||||
builder: (context, state) => MaterialApp.router(
|
listener: (context, state) {
|
||||||
builder: overlayManagerBuilder(),
|
if (state.action?.type == ActionType.openView) {
|
||||||
debugShowCheckedModeBanner: false,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
theme: state.lightTheme,
|
final view =
|
||||||
darkTheme: state.darkTheme,
|
state.action!.arguments?[ActionArgumentKeys.view.name];
|
||||||
themeMode: state.themeMode,
|
if (view != null) {
|
||||||
localizationsDelegates: context.localizationDelegates,
|
AppGlobals.rootNavKey.currentContext?.pushView(view);
|
||||||
supportedLocales: context.supportedLocales,
|
|
||||||
locale: state.locale,
|
final nodePath = state.action!
|
||||||
routerConfig: routerConfig,
|
.arguments?[ActionArgumentKeys.nodePath.name] as int?;
|
||||||
|
|
||||||
|
if (nodePath != null) {
|
||||||
|
context.read<NotificationActionBloc>().add(
|
||||||
|
NotificationActionEvent.performAction(
|
||||||
|
action: state.action!
|
||||||
|
.copyWith(type: ActionType.jumpToBlock),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||||
|
builder: (context, state) => MaterialApp.router(
|
||||||
|
builder: overlayManagerBuilder(),
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: state.lightTheme,
|
||||||
|
darkTheme: state.darkTheme,
|
||||||
|
themeMode: state.themeMode,
|
||||||
|
localizationsDelegates: context.localizationDelegates,
|
||||||
|
supportedLocales: context.supportedLocales,
|
||||||
|
locale: state.locale,
|
||||||
|
routerConfig: routerConfig,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -163,7 +192,6 @@ class AppGlobals {
|
|||||||
|
|
||||||
class ApplicationBlocObserver extends BlocObserver {
|
class ApplicationBlocObserver extends BlocObserver {
|
||||||
@override
|
@override
|
||||||
// ignore: unnecessary_overrides
|
|
||||||
void onTransition(Bloc bloc, Transition transition) {
|
void onTransition(Bloc bloc, Transition transition) {
|
||||||
// Log.debug("[current]: ${transition.currentState} \n\n[next]: ${transition.nextState}");
|
// Log.debug("[current]: ${transition.currentState} \n\n[next]: ${transition.nextState}");
|
||||||
// Log.debug("${transition.nextState}");
|
// Log.debug("${transition.nextState}");
|
||||||
|
@ -9,6 +9,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_page.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';
|
||||||
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
|
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
|
||||||
@ -181,21 +182,8 @@ StatefulShellRoute _mobileHomeScreenWithNavigationBarRoute() {
|
|||||||
StatefulShellBranch(
|
StatefulShellBranch(
|
||||||
routes: <RouteBase>[
|
routes: <RouteBase>[
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/e',
|
path: MobileNotificationsScreen.routeName,
|
||||||
builder: (BuildContext context, GoRouterState state) =>
|
builder: (_, __) => const MobileNotificationsScreen(),
|
||||||
const RootPlaceholderScreen(
|
|
||||||
label: 'Notification',
|
|
||||||
detailsPath: '/e/details',
|
|
||||||
),
|
|
||||||
routes: <RouteBase>[
|
|
||||||
GoRoute(
|
|
||||||
path: 'details',
|
|
||||||
builder: (BuildContext context, GoRouterState state) =>
|
|
||||||
const DetailsPlaceholderScreen(
|
|
||||||
label: 'Notification Page details',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -492,12 +480,8 @@ GoRoute _mobileEditorScreenRoute() {
|
|||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
final id = state.uri.queryParameters[MobileEditorScreen.viewId]!;
|
final id = state.uri.queryParameters[MobileEditorScreen.viewId]!;
|
||||||
final title = state.uri.queryParameters[MobileEditorScreen.viewTitle];
|
final title = state.uri.queryParameters[MobileEditorScreen.viewTitle];
|
||||||
return MaterialPage(
|
|
||||||
child: MobileEditorScreen(
|
return MaterialPage(child: MobileEditorScreen(id: id, title: title));
|
||||||
id: id,
|
|
||||||
title: title,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import 'package:appflowy/workspace/application/notifications/notification_action
|
|||||||
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
|
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/notifications/notification_service.dart';
|
import 'package:appflowy/workspace/application/notifications/notification_service.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@ -106,7 +107,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
pressReminder: (reminderId, path) {
|
pressReminder: (reminderId, path, view) {
|
||||||
final reminder =
|
final reminder =
|
||||||
state.reminders.firstWhereOrNull((r) => r.id == reminderId);
|
state.reminders.firstWhereOrNull((r) => r.id == reminderId);
|
||||||
|
|
||||||
@ -129,6 +130,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
|||||||
objectId: reminder.objectId,
|
objectId: reminder.objectId,
|
||||||
arguments: {
|
arguments: {
|
||||||
ActionArgumentKeys.nodePath.name: path,
|
ActionArgumentKeys.nodePath.name: path,
|
||||||
|
ActionArgumentKeys.view.name: view,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -201,6 +203,7 @@ class ReminderEvent with _$ReminderEvent {
|
|||||||
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,
|
||||||
}) = _PressReminder;
|
}) = _PressReminder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ class NotificationAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum ActionArgumentKeys {
|
enum ActionArgumentKeys {
|
||||||
|
view('view'),
|
||||||
nodePath('node_path');
|
nodePath('node_path');
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
|
@ -154,7 +154,7 @@ class SettingsUserState with _$SettingsUserState {
|
|||||||
factory SettingsUserState.initial(UserProfilePB userProfile) =>
|
factory SettingsUserState.initial(UserProfilePB userProfile) =>
|
||||||
SettingsUserState(
|
SettingsUserState(
|
||||||
userProfile: userProfile,
|
userProfile: userProfile,
|
||||||
historicalUsers: [],
|
historicalUsers: const [],
|
||||||
successOrFailure: left(unit),
|
successOrFailure: left(unit),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,17 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.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/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/notifications/reminder_extension.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_hub_title.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_hub_title.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_view.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_view.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra/size.dart';
|
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.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';
|
||||||
|
|
||||||
extension _ReminderSort on Iterable<ReminderPB> {
|
|
||||||
List<ReminderPB> sortByScheduledAt() =>
|
|
||||||
sorted((a, b) => b.scheduledAt.compareTo(a.scheduledAt));
|
|
||||||
}
|
|
||||||
|
|
||||||
class NotificationDialog extends StatefulWidget {
|
class NotificationDialog extends StatefulWidget {
|
||||||
const NotificationDialog({
|
const NotificationDialog({
|
||||||
super.key,
|
super.key,
|
||||||
@ -98,7 +87,7 @@ class _NotificationDialogState extends State<NotificationDialog>
|
|||||||
onDelete: _onDelete,
|
onDelete: _onDelete,
|
||||||
onAction: _onAction,
|
onAction: _onAction,
|
||||||
onReadChanged: _onReadChanged,
|
onReadChanged: _onReadChanged,
|
||||||
actionBar: _InboxActionBar(
|
actionBar: InboxActionBar(
|
||||||
hasUnreads: state.hasUnreads,
|
hasUnreads: state.hasUnreads,
|
||||||
showUnreadsOnly: filterState.showUnreadsOnly,
|
showUnreadsOnly: filterState.showUnreadsOnly,
|
||||||
),
|
),
|
||||||
@ -121,7 +110,7 @@ class _NotificationDialogState extends State<NotificationDialog>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAction(ReminderPB reminder, int? path) {
|
void _onAction(ReminderPB reminder, int? path, ViewPB? view) {
|
||||||
_reminderBloc.add(
|
_reminderBloc.add(
|
||||||
ReminderEvent.pressReminder(reminderId: reminder.id, path: path),
|
ReminderEvent.pressReminder(reminderId: reminder.id, path: path),
|
||||||
);
|
);
|
||||||
@ -139,165 +128,3 @@ class _NotificationDialogState extends State<NotificationDialog>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InboxActionBar extends StatelessWidget {
|
|
||||||
const _InboxActionBar({
|
|
||||||
required this.hasUnreads,
|
|
||||||
required this.showUnreadsOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
final bool hasUnreads;
|
|
||||||
final bool showUnreadsOnly;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
_MarkAsReadButton(
|
|
||||||
onMarkAllRead: !hasUnreads
|
|
||||||
? null
|
|
||||||
: () => context
|
|
||||||
.read<ReminderBloc>()
|
|
||||||
.add(const ReminderEvent.markAllRead()),
|
|
||||||
),
|
|
||||||
_ToggleUnreadsButton(
|
|
||||||
showUnreadsOnly: showUnreadsOnly,
|
|
||||||
onToggled: (showUnreadsOnly) => context
|
|
||||||
.read<NotificationFilterBloc>()
|
|
||||||
.add(const NotificationFilterEvent.toggleShowUnreadsOnly()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ToggleUnreadsButton extends StatefulWidget {
|
|
||||||
const _ToggleUnreadsButton({
|
|
||||||
required this.onToggled,
|
|
||||||
this.showUnreadsOnly = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Function(bool) onToggled;
|
|
||||||
final bool showUnreadsOnly;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_ToggleUnreadsButton> createState() => _ToggleUnreadsButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ToggleUnreadsButtonState extends State<_ToggleUnreadsButton> {
|
|
||||||
late bool showUnreadsOnly = widget.showUnreadsOnly;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SegmentedButton<bool>(
|
|
||||||
onSelectionChanged: (Set<bool> newSelection) {
|
|
||||||
setState(() => showUnreadsOnly = newSelection.first);
|
|
||||||
widget.onToggled(showUnreadsOnly);
|
|
||||||
},
|
|
||||||
showSelectedIcon: false,
|
|
||||||
style: ButtonStyle(
|
|
||||||
side: MaterialStatePropertyAll(
|
|
||||||
BorderSide(color: Theme.of(context).dividerColor),
|
|
||||||
),
|
|
||||||
shape: const MaterialStatePropertyAll(
|
|
||||||
RoundedRectangleBorder(borderRadius: Corners.s6Border),
|
|
||||||
),
|
|
||||||
foregroundColor: MaterialStateProperty.resolveWith<Color>(
|
|
||||||
(state) {
|
|
||||||
if (state.contains(MaterialState.hovered) ||
|
|
||||||
state.contains(MaterialState.selected) ||
|
|
||||||
state.contains(MaterialState.pressed)) {
|
|
||||||
return Theme.of(context).colorScheme.onSurface;
|
|
||||||
}
|
|
||||||
|
|
||||||
return AFThemeExtension.of(context).textColor;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
|
||||||
(state) {
|
|
||||||
if (state.contains(MaterialState.hovered) ||
|
|
||||||
state.contains(MaterialState.selected) ||
|
|
||||||
state.contains(MaterialState.pressed)) {
|
|
||||||
return Theme.of(context).colorScheme.primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Theme.of(context).cardColor;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
segments: [
|
|
||||||
ButtonSegment<bool>(
|
|
||||||
value: false,
|
|
||||||
label: Text(
|
|
||||||
LocaleKeys.notificationHub_actions_showAll.tr(),
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ButtonSegment<bool>(
|
|
||||||
value: true,
|
|
||||||
label: Text(
|
|
||||||
LocaleKeys.notificationHub_actions_showUnreads.tr(),
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selected: <bool>{showUnreadsOnly},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MarkAsReadButton extends StatefulWidget {
|
|
||||||
final VoidCallback? onMarkAllRead;
|
|
||||||
|
|
||||||
const _MarkAsReadButton({this.onMarkAllRead});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_MarkAsReadButton> createState() => _MarkAsReadButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MarkAsReadButtonState extends State<_MarkAsReadButton> {
|
|
||||||
bool _isHovering = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Opacity(
|
|
||||||
opacity: widget.onMarkAllRead != null ? 1 : 0.5,
|
|
||||||
child: FlowyHover(
|
|
||||||
onHover: (isHovering) => setState(() => _isHovering = isHovering),
|
|
||||||
resetHoverOnRebuild: false,
|
|
||||||
child: FlowyTextButton(
|
|
||||||
LocaleKeys.notificationHub_actions_markAllRead.tr(),
|
|
||||||
fontColor: widget.onMarkAllRead != null && _isHovering
|
|
||||||
? Theme.of(context).colorScheme.onSurface
|
|
||||||
: AFThemeExtension.of(context).textColor,
|
|
||||||
heading: FlowySvg(
|
|
||||||
FlowySvgs.checklist_s,
|
|
||||||
color: widget.onMarkAllRead != null && _isHovering
|
|
||||||
? Theme.of(context).colorScheme.onSurface
|
|
||||||
: AFThemeExtension.of(context).textColor,
|
|
||||||
),
|
|
||||||
hoverColor: widget.onMarkAllRead != null && _isHovering
|
|
||||||
? Theme.of(context).colorScheme.primary
|
|
||||||
: null,
|
|
||||||
onPressed: widget.onMarkAllRead,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
extension ReminderSort on Iterable<ReminderPB> {
|
||||||
|
List<ReminderPB> sortByScheduledAt() =>
|
||||||
|
sorted((a, b) => b.scheduledAt.compareTo(a.scheduledAt));
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import 'package:appflowy/util/platform_extension.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FlowyTabItem extends StatelessWidget {
|
||||||
|
static const double mobileHeight = 40;
|
||||||
|
static const EdgeInsets mobilePadding = EdgeInsets.symmetric(horizontal: 12);
|
||||||
|
|
||||||
|
static const double desktopHeight = 26;
|
||||||
|
static const EdgeInsets desktopPadding = EdgeInsets.symmetric(horizontal: 8);
|
||||||
|
|
||||||
|
const FlowyTabItem({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.isSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final bool isSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Tab(
|
||||||
|
height: PlatformExtension.isMobile ? mobileHeight : desktopHeight,
|
||||||
|
child: Padding(
|
||||||
|
padding: PlatformExtension.isMobile ? mobilePadding : desktopPadding,
|
||||||
|
child: FlowyText.regular(
|
||||||
|
label,
|
||||||
|
color: isSelected
|
||||||
|
? AFThemeExtension.of(context).textColor
|
||||||
|
: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,174 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';
|
||||||
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/size.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class InboxActionBar extends StatelessWidget {
|
||||||
|
const InboxActionBar({
|
||||||
|
super.key,
|
||||||
|
required this.hasUnreads,
|
||||||
|
required this.showUnreadsOnly,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool hasUnreads;
|
||||||
|
final bool showUnreadsOnly;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: AFThemeExtension.of(context).calloutBGColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_MarkAsReadButton(
|
||||||
|
onMarkAllRead: !hasUnreads
|
||||||
|
? null
|
||||||
|
: () => context
|
||||||
|
.read<ReminderBloc>()
|
||||||
|
.add(const ReminderEvent.markAllRead()),
|
||||||
|
),
|
||||||
|
_ToggleUnreadsButton(
|
||||||
|
showUnreadsOnly: showUnreadsOnly,
|
||||||
|
onToggled: (_) => context
|
||||||
|
.read<NotificationFilterBloc>()
|
||||||
|
.add(const NotificationFilterEvent.toggleShowUnreadsOnly()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ToggleUnreadsButton extends StatefulWidget {
|
||||||
|
const _ToggleUnreadsButton({
|
||||||
|
required this.onToggled,
|
||||||
|
this.showUnreadsOnly = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Function(bool) onToggled;
|
||||||
|
final bool showUnreadsOnly;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ToggleUnreadsButton> createState() => _ToggleUnreadsButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ToggleUnreadsButtonState extends State<_ToggleUnreadsButton> {
|
||||||
|
late bool showUnreadsOnly = widget.showUnreadsOnly;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SegmentedButton<bool>(
|
||||||
|
onSelectionChanged: (Set<bool> newSelection) {
|
||||||
|
setState(() => showUnreadsOnly = newSelection.first);
|
||||||
|
widget.onToggled(showUnreadsOnly);
|
||||||
|
},
|
||||||
|
showSelectedIcon: false,
|
||||||
|
style: ButtonStyle(
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
side: MaterialStatePropertyAll(
|
||||||
|
BorderSide(color: Theme.of(context).dividerColor),
|
||||||
|
),
|
||||||
|
shape: const MaterialStatePropertyAll(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: Corners.s6Border,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
foregroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||||
|
(state) {
|
||||||
|
if (state.contains(MaterialState.selected)) {
|
||||||
|
return Theme.of(context).colorScheme.onPrimary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AFThemeExtension.of(context).textColor;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||||
|
(state) {
|
||||||
|
if (state.contains(MaterialState.selected)) {
|
||||||
|
return Theme.of(context).colorScheme.primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.contains(MaterialState.hovered)) {
|
||||||
|
return AFThemeExtension.of(context).lightGreyHover;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Theme.of(context).cardColor;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
segments: [
|
||||||
|
ButtonSegment<bool>(
|
||||||
|
value: false,
|
||||||
|
label: Text(
|
||||||
|
LocaleKeys.notificationHub_actions_showAll.tr(),
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ButtonSegment<bool>(
|
||||||
|
value: true,
|
||||||
|
label: Text(
|
||||||
|
LocaleKeys.notificationHub_actions_showUnreads.tr(),
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: <bool>{showUnreadsOnly},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MarkAsReadButton extends StatefulWidget {
|
||||||
|
final VoidCallback? onMarkAllRead;
|
||||||
|
|
||||||
|
const _MarkAsReadButton({this.onMarkAllRead});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_MarkAsReadButton> createState() => _MarkAsReadButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MarkAsReadButtonState extends State<_MarkAsReadButton> {
|
||||||
|
bool _isHovering = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: widget.onMarkAllRead != null ? 1 : 0.5,
|
||||||
|
child: FlowyHover(
|
||||||
|
onHover: (isHovering) => setState(() => _isHovering = isHovering),
|
||||||
|
resetHoverOnRebuild: false,
|
||||||
|
child: FlowyTextButton(
|
||||||
|
LocaleKeys.notificationHub_actions_markAllRead.tr(),
|
||||||
|
fontColor: widget.onMarkAllRead != null && _isHovering
|
||||||
|
? Theme.of(context).colorScheme.onSurface
|
||||||
|
: AFThemeExtension.of(context).textColor,
|
||||||
|
heading: FlowySvg(
|
||||||
|
FlowySvgs.checklist_s,
|
||||||
|
color: widget.onMarkAllRead != null && _isHovering
|
||||||
|
? Theme.of(context).colorScheme.onSurface
|
||||||
|
: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
hoverColor: widget.onMarkAllRead != null && _isHovering
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: null,
|
||||||
|
onPressed: widget.onMarkAllRead,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_configuration.dart
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_style.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/application/settings/date_time/date_format_ext.dart';
|
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -22,13 +23,13 @@ class NotificationItem extends StatefulWidget {
|
|||||||
required this.scheduled,
|
required this.scheduled,
|
||||||
required this.body,
|
required this.body,
|
||||||
required this.isRead,
|
required this.isRead,
|
||||||
this.path,
|
|
||||||
this.block,
|
this.block,
|
||||||
this.includeTime = false,
|
this.includeTime = false,
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
this.onAction,
|
this.onAction,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
this.onReadChanged,
|
this.onReadChanged,
|
||||||
|
this.view,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String reminderId;
|
final String reminderId;
|
||||||
@ -36,7 +37,7 @@ class NotificationItem extends StatefulWidget {
|
|||||||
final Int64 scheduled;
|
final Int64 scheduled;
|
||||||
final String body;
|
final String body;
|
||||||
final bool isRead;
|
final bool isRead;
|
||||||
final Future<int?>? path;
|
final ViewPB? view;
|
||||||
|
|
||||||
/// If [block] is provided, then [body] will be shown only if
|
/// If [block] is provided, then [body] will be shown only if
|
||||||
/// [block] fails to fetch.
|
/// [block] fails to fetch.
|
||||||
@ -64,7 +65,7 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
widget.path?.then((p) => path = p);
|
widget.block?.then((b) => path = b?.path.first);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -80,117 +81,97 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => widget.onAction?.call(path),
|
onTap: () => widget.onAction?.call(path),
|
||||||
child: AbsorbPointer(
|
child: AbsorbPointer(
|
||||||
child: Opacity(
|
child: DecoratedBox(
|
||||||
opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,
|
decoration: BoxDecoration(
|
||||||
child: DecoratedBox(
|
border: Border(
|
||||||
decoration: BoxDecoration(
|
bottom: PlatformExtension.isMobile
|
||||||
color: _isHovering && widget.onAction != null
|
? BorderSide(
|
||||||
? AFThemeExtension.of(context).lightGreyHover
|
color: AFThemeExtension.of(context).calloutBGColor,
|
||||||
: Colors.transparent,
|
)
|
||||||
border: widget.isRead || widget.readOnly
|
: BorderSide.none,
|
||||||
? null
|
),
|
||||||
: Border(
|
),
|
||||||
left: BorderSide(
|
child: Opacity(
|
||||||
width: 2,
|
opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _isHovering && widget.onAction != null
|
||||||
|
? AFThemeExtension.of(context).lightGreyHover
|
||||||
|
: Colors.transparent,
|
||||||
|
border: widget.isRead || widget.readOnly
|
||||||
|
? null
|
||||||
|
: Border(
|
||||||
|
left: BorderSide(
|
||||||
|
width: PlatformExtension.isMobile ? 4 : 2,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
FlowySvg(
|
||||||
|
FlowySvgs.time_s,
|
||||||
|
size: Size.square(
|
||||||
|
PlatformExtension.isMobile ? 24 : 20,
|
||||||
|
),
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
const HSpace(16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
FlowyText.semibold(
|
||||||
|
widget.title,
|
||||||
|
fontSize:
|
||||||
|
PlatformExtension.isMobile ? 16 : 14,
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
// TODO(Xazin): Relative time
|
||||||
|
FlowyText.regular(
|
||||||
|
'${_scheduledString(
|
||||||
|
widget.scheduled,
|
||||||
|
widget.includeTime,
|
||||||
|
)}${widget.view != null ? " - ${widget.view!.name}" : ""}',
|
||||||
|
fontSize:
|
||||||
|
PlatformExtension.isMobile ? 12 : 10,
|
||||||
|
),
|
||||||
|
const VSpace(5),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: Corners.s8Border,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
child: _NotificationContent(
|
||||||
|
block: widget.block,
|
||||||
|
body: widget.body,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
child: Padding(
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 10,
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
FlowySvg(
|
|
||||||
FlowySvgs.time_s,
|
|
||||||
size: const Size.square(20),
|
|
||||||
color: Theme.of(context).colorScheme.tertiary,
|
|
||||||
),
|
|
||||||
const HSpace(16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
FlowyText.semibold(
|
|
||||||
widget.title,
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context).colorScheme.tertiary,
|
|
||||||
),
|
|
||||||
// TODO(Xazin): Relative time + View Name
|
|
||||||
FlowyText.regular(
|
|
||||||
_scheduledString(
|
|
||||||
widget.scheduled,
|
|
||||||
widget.includeTime,
|
|
||||||
),
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
const VSpace(5),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: Corners.s8Border,
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
),
|
|
||||||
child: FutureBuilder<Node?>(
|
|
||||||
future: widget.block,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasError ||
|
|
||||||
!snapshot.hasData ||
|
|
||||||
snapshot.data == null) {
|
|
||||||
return FlowyText.regular(
|
|
||||||
widget.body,
|
|
||||||
maxLines: 4,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final EditorState editorState = EditorState(
|
|
||||||
document: Document(root: snapshot.data!),
|
|
||||||
);
|
|
||||||
|
|
||||||
final EditorStyleCustomizer
|
|
||||||
styleCustomizer = EditorStyleCustomizer(
|
|
||||||
context: context,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Transform.scale(
|
|
||||||
scale: .9,
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: AppFlowyEditor(
|
|
||||||
editorState: editorState,
|
|
||||||
editorStyle: styleCustomizer.style(),
|
|
||||||
editable: false,
|
|
||||||
shrinkWrap: true,
|
|
||||||
blockComponentBuilders:
|
|
||||||
getEditorBuilderMap(
|
|
||||||
context: context,
|
|
||||||
editorState: editorState,
|
|
||||||
styleCustomizer: styleCustomizer,
|
|
||||||
editable: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_isHovering && !widget.readOnly)
|
if (PlatformExtension.isMobile && !widget.readOnly ||
|
||||||
|
_isHovering && !widget.readOnly)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 4,
|
right: PlatformExtension.isMobile ? 8 : 4,
|
||||||
top: 4,
|
top: PlatformExtension.isMobile ? 8 : 4,
|
||||||
child: NotificationItemActions(
|
child: NotificationItemActions(
|
||||||
isRead: widget.isRead,
|
isRead: widget.isRead,
|
||||||
onDelete: widget.onDelete,
|
onDelete: widget.onDelete,
|
||||||
@ -214,6 +195,54 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
void _onHover(bool isHovering) => setState(() => _isHovering = isHovering);
|
void _onHover(bool isHovering) => setState(() => _isHovering = isHovering);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _NotificationContent extends StatelessWidget {
|
||||||
|
const _NotificationContent({
|
||||||
|
required this.body,
|
||||||
|
required this.block,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String body;
|
||||||
|
final Future<Node?>? block;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<Node?>(
|
||||||
|
future: block,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) {
|
||||||
|
return FlowyText.regular(body, maxLines: 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
final editorState = EditorState(
|
||||||
|
document: Document(root: snapshot.data!),
|
||||||
|
);
|
||||||
|
|
||||||
|
final styleCustomizer = EditorStyleCustomizer(
|
||||||
|
context: context,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Transform.scale(
|
||||||
|
scale: .9,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: AppFlowyEditor(
|
||||||
|
editorState: editorState,
|
||||||
|
editorStyle: styleCustomizer.style(),
|
||||||
|
editable: false,
|
||||||
|
shrinkWrap: true,
|
||||||
|
blockComponentBuilders: getEditorBuilderMap(
|
||||||
|
context: context,
|
||||||
|
editorState: editorState,
|
||||||
|
styleCustomizer: styleCustomizer,
|
||||||
|
editable: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class NotificationItemActions extends StatelessWidget {
|
class NotificationItemActions extends StatelessWidget {
|
||||||
const NotificationItemActions({
|
const NotificationItemActions({
|
||||||
super.key,
|
super.key,
|
||||||
@ -228,11 +257,15 @@ class NotificationItemActions extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final double size = PlatformExtension.isMobile ? 40.0 : 30.0;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 30,
|
height: size,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).cardColor,
|
color: Theme.of(context).cardColor,
|
||||||
border: Border.all(color: Theme.of(context).dividerColor),
|
border: Border.all(
|
||||||
|
color: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: IntrinsicHeight(
|
child: IntrinsicHeight(
|
||||||
@ -240,7 +273,8 @@ class NotificationItemActions extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (isRead) ...[
|
if (isRead) ...[
|
||||||
FlowyIconButton(
|
FlowyIconButton(
|
||||||
height: 28,
|
height: size,
|
||||||
|
width: size,
|
||||||
tooltipText:
|
tooltipText:
|
||||||
LocaleKeys.reminderNotification_tooltipMarkUnread.tr(),
|
LocaleKeys.reminderNotification_tooltipMarkUnread.tr(),
|
||||||
icon: const FlowySvg(FlowySvgs.restore_s),
|
icon: const FlowySvg(FlowySvgs.restore_s),
|
||||||
@ -249,7 +283,8 @@ class NotificationItemActions extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
FlowyIconButton(
|
FlowyIconButton(
|
||||||
height: 28,
|
height: size,
|
||||||
|
width: size,
|
||||||
tooltipText:
|
tooltipText:
|
||||||
LocaleKeys.reminderNotification_tooltipMarkRead.tr(),
|
LocaleKeys.reminderNotification_tooltipMarkRead.tr(),
|
||||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||||
@ -262,10 +297,13 @@ class NotificationItemActions extends StatelessWidget {
|
|||||||
thickness: 1,
|
thickness: 1,
|
||||||
indent: 2,
|
indent: 2,
|
||||||
endIndent: 2,
|
endIndent: 2,
|
||||||
color: Theme.of(context).dividerColor,
|
color: PlatformExtension.isMobile
|
||||||
|
? Theme.of(context).colorScheme.outline
|
||||||
|
: Theme.of(context).dividerColor,
|
||||||
),
|
),
|
||||||
FlowyIconButton(
|
FlowyIconButton(
|
||||||
height: 28,
|
height: size,
|
||||||
|
width: size,
|
||||||
tooltipText: LocaleKeys.reminderNotification_tooltipDelete.tr(),
|
tooltipText: LocaleKeys.reminderNotification_tooltipDelete.tr(),
|
||||||
icon: const FlowySvg(FlowySvgs.delete_s),
|
icon: const FlowySvg(FlowySvgs.delete_s),
|
||||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class NotificationTabBar extends StatelessWidget {
|
class NotificationTabBar extends StatelessWidget {
|
||||||
@ -36,11 +36,11 @@ class NotificationTabBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
isScrollable: true,
|
isScrollable: true,
|
||||||
tabs: [
|
tabs: [
|
||||||
_FlowyTab(
|
FlowyTabItem(
|
||||||
label: LocaleKeys.notificationHub_tabs_inbox.tr(),
|
label: LocaleKeys.notificationHub_tabs_inbox.tr(),
|
||||||
isSelected: tabController.index == 0,
|
isSelected: tabController.index == 0,
|
||||||
),
|
),
|
||||||
_FlowyTab(
|
FlowyTabItem(
|
||||||
label: LocaleKeys.notificationHub_tabs_upcoming.tr(),
|
label: LocaleKeys.notificationHub_tabs_upcoming.tr(),
|
||||||
isSelected: tabController.index == 1,
|
isSelected: tabController.index == 1,
|
||||||
),
|
),
|
||||||
@ -52,27 +52,3 @@ class NotificationTabBar extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FlowyTab extends StatelessWidget {
|
|
||||||
final String label;
|
|
||||||
final bool isSelected;
|
|
||||||
|
|
||||||
const _FlowyTab({
|
|
||||||
required this.label,
|
|
||||||
required this.isSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Tab(
|
|
||||||
height: 26,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: FlowyText.regular(
|
|
||||||
label,
|
|
||||||
color: isSelected ? Theme.of(context).colorScheme.tertiary : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -9,9 +9,15 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Displays a Lsit of Notifications, currently used primarily to
|
||||||
|
/// display Reminders.
|
||||||
|
///
|
||||||
|
/// Optimized for both Mobile & Desktop use
|
||||||
|
///
|
||||||
class NotificationsView extends StatelessWidget {
|
class NotificationsView extends StatelessWidget {
|
||||||
const NotificationsView({
|
const NotificationsView({
|
||||||
super.key,
|
super.key,
|
||||||
@ -29,7 +35,7 @@ class NotificationsView extends StatelessWidget {
|
|||||||
final ReminderBloc reminderBloc;
|
final ReminderBloc reminderBloc;
|
||||||
final List<ViewPB> views;
|
final List<ViewPB> views;
|
||||||
final bool isUpcoming;
|
final bool isUpcoming;
|
||||||
final Function(ReminderPB reminder, int? path)? onAction;
|
final Function(ReminderPB reminder, int? path, ViewPB? view)? onAction;
|
||||||
final Function(ReminderPB reminder)? onDelete;
|
final Function(ReminderPB reminder)? onDelete;
|
||||||
final Function(ReminderPB reminder, bool isRead)? onReadChanged;
|
final Function(ReminderPB reminder, bool isRead)? onReadChanged;
|
||||||
final Widget? actionBar;
|
final Widget? actionBar;
|
||||||
@ -46,47 +52,56 @@ class NotificationsView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
if (actionBar != null) actionBar!,
|
||||||
if (actionBar != null) actionBar!,
|
Expanded(
|
||||||
...shownReminders.map(
|
child: SingleChildScrollView(
|
||||||
(ReminderPB reminder) {
|
child: Column(
|
||||||
final blockId = reminder.meta[ReminderMetaKeys.blockId.name];
|
children: [
|
||||||
|
...shownReminders.map(
|
||||||
|
(ReminderPB reminder) {
|
||||||
|
final blockId =
|
||||||
|
reminder.meta[ReminderMetaKeys.blockId.name];
|
||||||
|
|
||||||
final documentService = DocumentService();
|
final documentService = DocumentService();
|
||||||
final documentFuture = documentService.openDocument(
|
final documentFuture = documentService.openDocument(
|
||||||
viewId: reminder.objectId,
|
viewId: reminder.objectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<Node?>? nodeBuilder;
|
Future<Node?>? nodeBuilder;
|
||||||
Future<int?>? pathFinder;
|
if (blockId != null) {
|
||||||
if (blockId != null) {
|
nodeBuilder =
|
||||||
nodeBuilder = _getNodeFromDocument(documentFuture, blockId);
|
_getNodeFromDocument(documentFuture, blockId);
|
||||||
pathFinder = _getPathFromDocument(documentFuture, blockId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return NotificationItem(
|
final view = views
|
||||||
reminderId: reminder.id,
|
.firstWhereOrNull((v) => v.id == reminder.objectId);
|
||||||
key: ValueKey(reminder.id),
|
|
||||||
title: reminder.title,
|
return NotificationItem(
|
||||||
scheduled: reminder.scheduledAt,
|
reminderId: reminder.id,
|
||||||
body: reminder.message,
|
key: ValueKey(reminder.id),
|
||||||
path: pathFinder,
|
title: reminder.title,
|
||||||
block: nodeBuilder,
|
scheduled: reminder.scheduledAt,
|
||||||
isRead: reminder.isRead,
|
body: reminder.message,
|
||||||
includeTime: reminder.includeTime ?? false,
|
block: nodeBuilder,
|
||||||
readOnly: isUpcoming,
|
isRead: reminder.isRead,
|
||||||
onReadChanged: (isRead) =>
|
includeTime: reminder.includeTime ?? false,
|
||||||
onReadChanged?.call(reminder, isRead),
|
readOnly: isUpcoming,
|
||||||
onDelete: () => onDelete?.call(reminder),
|
onReadChanged: (isRead) =>
|
||||||
onAction: (path) => onAction?.call(reminder, path),
|
onReadChanged?.call(reminder, isRead),
|
||||||
);
|
onDelete: () => onDelete?.call(reminder),
|
||||||
},
|
onAction: (path) => onAction?.call(reminder, path, view),
|
||||||
|
view: view,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,36 +118,12 @@ class NotificationsView extends StatelessWidget {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final blockOrFailure = await DocumentService().getBlockFromDocument(
|
|
||||||
document: document,
|
|
||||||
blockId: blockId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return blockOrFailure.fold(
|
|
||||||
(_) => null,
|
|
||||||
(block) => block.toNode(meta: MetaPB()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int?> _getPathFromDocument(
|
|
||||||
Future<Either<FlowyError, DocumentDataPB>> documentFuture,
|
|
||||||
String blockId,
|
|
||||||
) async {
|
|
||||||
final document = (await documentFuture).fold(
|
|
||||||
(l) => null,
|
|
||||||
(document) => document,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (document == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final rootNode = document.toDocument()?.root;
|
final rootNode = document.toDocument()?.root;
|
||||||
if (rootNode == null) {
|
if (rootNode == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _searchById(rootNode, blockId)?.path.first;
|
return _searchById(rootNode, blockId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ class NotificationsHubEmpty extends StatelessWidget {
|
|||||||
const VSpace(8),
|
const VSpace(8),
|
||||||
FlowyText.regular(
|
FlowyText.regular(
|
||||||
LocaleKeys.notificationHub_emptyBody.tr(),
|
LocaleKeys.notificationHub_emptyBody.tr(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -975,6 +975,9 @@
|
|||||||
},
|
},
|
||||||
"notificationHub": {
|
"notificationHub": {
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
|
"mobile": {
|
||||||
|
"title": "Updates"
|
||||||
|
},
|
||||||
"emptyTitle": "All caught up!",
|
"emptyTitle": "All caught up!",
|
||||||
"emptyBody": "No pending notifications or actions. Enjoy the calm.",
|
"emptyBody": "No pending notifications or actions. Enjoy the calm.",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
@ -1151,4 +1154,4 @@
|
|||||||
"addField": "Add field",
|
"addField": "Add field",
|
||||||
"userIcon": "User icon"
|
"userIcon": "User icon"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user