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:
parent
2a421034d9
commit
649545cdf3
@ -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';
|
||||
|
||||
class MobileHomePageHeader extends StatelessWidget {
|
||||
const MobileHomePageHeader({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
});
|
||||
const MobileHomePageHeader({super.key, required this.userProfile});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<SettingsUserViewBloc>(param1: userProfile)
|
||||
..add(const SettingsUserEvent.initial()),
|
||||
@ -41,29 +37,23 @@ class MobileHomePageHeader extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const FlowyText.medium(
|
||||
'AppFlowy',
|
||||
fontSize: 18,
|
||||
),
|
||||
const FlowyText.medium('AppFlowy', fontSize: 18),
|
||||
const VSpace(4),
|
||||
FlowyText.regular(
|
||||
userProfile.email.isNotEmpty
|
||||
? userProfile.email
|
||||
: userProfile.name,
|
||||
? state.userProfile.email
|
||||
: state.userProfile.name,
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.onSurface,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.push(MobileHomeSettingPage.routeName);
|
||||
},
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.m_setting_m,
|
||||
),
|
||||
onPressed: () =>
|
||||
context.push(MobileHomeSettingPage.routeName),
|
||||
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(
|
||||
context,
|
||||
userName: userName,
|
||||
onSubmitted: (value) {
|
||||
context.read<SettingsUserViewBloc>().add(
|
||||
SettingsUserEvent.updateUserName(
|
||||
value,
|
||||
),
|
||||
);
|
||||
},
|
||||
onSubmitted: (value) => context
|
||||
.read<SettingsUserViewBloc>()
|
||||
.add(SettingsUserEvent.updateUserName(value)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -87,39 +87,37 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
child: BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||
listener: _onNotificationAction,
|
||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||
builder: (context, state) {
|
||||
return state.loadingState.when(
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) => result.fold(
|
||||
(error) {
|
||||
Log.error(error);
|
||||
return FlowyErrorPage.message(
|
||||
error.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
builder: (context, state) => state.loadingState.when(
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) => result.fold(
|
||||
(error) {
|
||||
Log.error(error);
|
||||
return FlowyErrorPage.message(
|
||||
error.toString(),
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
(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 {
|
||||
editorState = documentBloc.editorState!;
|
||||
return _buildEditorPage(
|
||||
context,
|
||||
state,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
} else {
|
||||
editorState = documentBloc.editorState!;
|
||||
return _buildEditorPage(
|
||||
context,
|
||||
state,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -46,10 +46,10 @@ class ErrorBlockComponentWidget extends BlockComponentStatefulWidget {
|
||||
|
||||
@override
|
||||
State<ErrorBlockComponentWidget> createState() =>
|
||||
_DividerBlockComponentWidgetState();
|
||||
_ErrorBlockComponentWidgetState();
|
||||
}
|
||||
|
||||
class _DividerBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
|
||||
class _ErrorBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
|
||||
with BlockComponentConfigurable {
|
||||
@override
|
||||
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/startup/startup.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/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
|
||||
@ -137,18 +140,44 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
|
||||
BlocProvider<DocumentAppearanceCubit>(
|
||||
create: (_) => DocumentAppearanceCubit()..fetch(),
|
||||
),
|
||||
BlocProvider.value(value: getIt<NotificationActionBloc>()),
|
||||
],
|
||||
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,
|
||||
child: BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||
listener: (context, state) {
|
||||
if (state.action?.type == ActionType.openView) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final view =
|
||||
state.action!.arguments?[ActionArgumentKeys.view.name];
|
||||
if (view != null) {
|
||||
AppGlobals.rootNavKey.currentContext?.pushView(view);
|
||||
|
||||
final nodePath = state.action!
|
||||
.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 {
|
||||
@override
|
||||
// ignore: unnecessary_overrides
|
||||
void onTransition(Bloc bloc, Transition transition) {
|
||||
// Log.debug("[current]: ${transition.currentState} \n\n[next]: ${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_grid_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_page.dart';
|
||||
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_page.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/cloud/appflowy_cloud_page.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
|
||||
@ -181,21 +182,8 @@ StatefulShellRoute _mobileHomeScreenWithNavigationBarRoute() {
|
||||
StatefulShellBranch(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/e',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const RootPlaceholderScreen(
|
||||
label: 'Notification',
|
||||
detailsPath: '/e/details',
|
||||
),
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: 'details',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const DetailsPlaceholderScreen(
|
||||
label: 'Notification Page details',
|
||||
),
|
||||
),
|
||||
],
|
||||
path: MobileNotificationsScreen.routeName,
|
||||
builder: (_, __) => const MobileNotificationsScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -492,12 +480,8 @@ GoRoute _mobileEditorScreenRoute() {
|
||||
pageBuilder: (context, state) {
|
||||
final id = state.uri.queryParameters[MobileEditorScreen.viewId]!;
|
||||
final title = state.uri.queryParameters[MobileEditorScreen.viewTitle];
|
||||
return MaterialPage(
|
||||
child: MobileEditorScreen(
|
||||
id: id,
|
||||
title: title,
|
||||
),
|
||||
);
|
||||
|
||||
return MaterialPage(child: MobileEditorScreen(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_service.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:bloc/bloc.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 =
|
||||
state.reminders.firstWhereOrNull((r) => r.id == reminderId);
|
||||
|
||||
@ -129,6 +130,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
||||
objectId: reminder.objectId,
|
||||
arguments: {
|
||||
ActionArgumentKeys.nodePath.name: path,
|
||||
ActionArgumentKeys.view.name: view,
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -201,6 +203,7 @@ class ReminderEvent with _$ReminderEvent {
|
||||
const factory ReminderEvent.pressReminder({
|
||||
required String reminderId,
|
||||
@Default(null) int? path,
|
||||
@Default(null) ViewPB? view,
|
||||
}) = _PressReminder;
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ class NotificationAction {
|
||||
}
|
||||
|
||||
enum ActionArgumentKeys {
|
||||
view('view'),
|
||||
nodePath('node_path');
|
||||
|
||||
final String name;
|
||||
|
@ -154,7 +154,7 @@ class SettingsUserState with _$SettingsUserState {
|
||||
factory SettingsUserState.initial(UserProfilePB userProfile) =>
|
||||
SettingsUserState(
|
||||
userProfile: userProfile,
|
||||
historicalUsers: [],
|
||||
historicalUsers: const [],
|
||||
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/user/application/notification_filter/notification_filter_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_tab_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-user/reminder.pb.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_bloc/flutter_bloc.dart';
|
||||
|
||||
extension _ReminderSort on Iterable<ReminderPB> {
|
||||
List<ReminderPB> sortByScheduledAt() =>
|
||||
sorted((a, b) => b.scheduledAt.compareTo(a.scheduledAt));
|
||||
}
|
||||
|
||||
class NotificationDialog extends StatefulWidget {
|
||||
const NotificationDialog({
|
||||
super.key,
|
||||
@ -98,7 +87,7 @@ class _NotificationDialogState extends State<NotificationDialog>
|
||||
onDelete: _onDelete,
|
||||
onAction: _onAction,
|
||||
onReadChanged: _onReadChanged,
|
||||
actionBar: _InboxActionBar(
|
||||
actionBar: InboxActionBar(
|
||||
hasUnreads: state.hasUnreads,
|
||||
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(
|
||||
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/workspace/application/settings/appearance/appearance_cubit.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_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -22,13 +23,13 @@ class NotificationItem extends StatefulWidget {
|
||||
required this.scheduled,
|
||||
required this.body,
|
||||
required this.isRead,
|
||||
this.path,
|
||||
this.block,
|
||||
this.includeTime = false,
|
||||
this.readOnly = false,
|
||||
this.onAction,
|
||||
this.onDelete,
|
||||
this.onReadChanged,
|
||||
this.view,
|
||||
});
|
||||
|
||||
final String reminderId;
|
||||
@ -36,7 +37,7 @@ class NotificationItem extends StatefulWidget {
|
||||
final Int64 scheduled;
|
||||
final String body;
|
||||
final bool isRead;
|
||||
final Future<int?>? path;
|
||||
final ViewPB? view;
|
||||
|
||||
/// If [block] is provided, then [body] will be shown only if
|
||||
/// [block] fails to fetch.
|
||||
@ -64,7 +65,7 @@ class _NotificationItemState extends State<NotificationItem> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.path?.then((p) => path = p);
|
||||
widget.block?.then((b) => path = b?.path.first);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -80,117 +81,97 @@ class _NotificationItemState extends State<NotificationItem> {
|
||||
GestureDetector(
|
||||
onTap: () => widget.onAction?.call(path),
|
||||
child: AbsorbPointer(
|
||||
child: Opacity(
|
||||
opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,
|
||||
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: 2,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: PlatformExtension.isMobile
|
||||
? BorderSide(
|
||||
color: AFThemeExtension.of(context).calloutBGColor,
|
||||
)
|
||||
: BorderSide.none,
|
||||
),
|
||||
),
|
||||
child: Opacity(
|
||||
opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,
|
||||
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(
|
||||
right: 4,
|
||||
top: 4,
|
||||
right: PlatformExtension.isMobile ? 8 : 4,
|
||||
top: PlatformExtension.isMobile ? 8 : 4,
|
||||
child: NotificationItemActions(
|
||||
isRead: widget.isRead,
|
||||
onDelete: widget.onDelete,
|
||||
@ -214,6 +195,54 @@ class _NotificationItemState extends State<NotificationItem> {
|
||||
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 {
|
||||
const NotificationItemActions({
|
||||
super.key,
|
||||
@ -228,11 +257,15 @@ class NotificationItemActions extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double size = PlatformExtension.isMobile ? 40.0 : 30.0;
|
||||
|
||||
return Container(
|
||||
height: 30,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
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),
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
@ -240,7 +273,8 @@ class NotificationItemActions extends StatelessWidget {
|
||||
children: [
|
||||
if (isRead) ...[
|
||||
FlowyIconButton(
|
||||
height: 28,
|
||||
height: size,
|
||||
width: size,
|
||||
tooltipText:
|
||||
LocaleKeys.reminderNotification_tooltipMarkUnread.tr(),
|
||||
icon: const FlowySvg(FlowySvgs.restore_s),
|
||||
@ -249,7 +283,8 @@ class NotificationItemActions extends StatelessWidget {
|
||||
),
|
||||
] else ...[
|
||||
FlowyIconButton(
|
||||
height: 28,
|
||||
height: size,
|
||||
width: size,
|
||||
tooltipText:
|
||||
LocaleKeys.reminderNotification_tooltipMarkRead.tr(),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
@ -262,10 +297,13 @@ class NotificationItemActions extends StatelessWidget {
|
||||
thickness: 1,
|
||||
indent: 2,
|
||||
endIndent: 2,
|
||||
color: Theme.of(context).dividerColor,
|
||||
color: PlatformExtension.isMobile
|
||||
? Theme.of(context).colorScheme.outline
|
||||
: Theme.of(context).dividerColor,
|
||||
),
|
||||
FlowyIconButton(
|
||||
height: 28,
|
||||
height: size,
|
||||
width: size,
|
||||
tooltipText: LocaleKeys.reminderNotification_tooltipDelete.tr(),
|
||||
icon: const FlowySvg(FlowySvgs.delete_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
|
@ -1,6 +1,6 @@
|
||||
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_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NotificationTabBar extends StatelessWidget {
|
||||
@ -36,11 +36,11 @@ class NotificationTabBar extends StatelessWidget {
|
||||
),
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
_FlowyTab(
|
||||
FlowyTabItem(
|
||||
label: LocaleKeys.notificationHub_tabs_inbox.tr(),
|
||||
isSelected: tabController.index == 0,
|
||||
),
|
||||
_FlowyTab(
|
||||
FlowyTabItem(
|
||||
label: LocaleKeys.notificationHub_tabs_upcoming.tr(),
|
||||
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-user/reminder.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dartz/dartz.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 {
|
||||
const NotificationsView({
|
||||
super.key,
|
||||
@ -29,7 +35,7 @@ class NotificationsView extends StatelessWidget {
|
||||
final ReminderBloc reminderBloc;
|
||||
final List<ViewPB> views;
|
||||
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, bool isRead)? onReadChanged;
|
||||
final Widget? actionBar;
|
||||
@ -46,47 +52,56 @@ class NotificationsView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (actionBar != null) actionBar!,
|
||||
...shownReminders.map(
|
||||
(ReminderPB reminder) {
|
||||
final blockId = reminder.meta[ReminderMetaKeys.blockId.name];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (actionBar != null) actionBar!,
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
...shownReminders.map(
|
||||
(ReminderPB reminder) {
|
||||
final blockId =
|
||||
reminder.meta[ReminderMetaKeys.blockId.name];
|
||||
|
||||
final documentService = DocumentService();
|
||||
final documentFuture = documentService.openDocument(
|
||||
viewId: reminder.objectId,
|
||||
);
|
||||
final documentService = DocumentService();
|
||||
final documentFuture = documentService.openDocument(
|
||||
viewId: reminder.objectId,
|
||||
);
|
||||
|
||||
Future<Node?>? nodeBuilder;
|
||||
Future<int?>? pathFinder;
|
||||
if (blockId != null) {
|
||||
nodeBuilder = _getNodeFromDocument(documentFuture, blockId);
|
||||
pathFinder = _getPathFromDocument(documentFuture, blockId);
|
||||
}
|
||||
Future<Node?>? nodeBuilder;
|
||||
if (blockId != null) {
|
||||
nodeBuilder =
|
||||
_getNodeFromDocument(documentFuture, blockId);
|
||||
}
|
||||
|
||||
return NotificationItem(
|
||||
reminderId: reminder.id,
|
||||
key: ValueKey(reminder.id),
|
||||
title: reminder.title,
|
||||
scheduled: reminder.scheduledAt,
|
||||
body: reminder.message,
|
||||
path: pathFinder,
|
||||
block: nodeBuilder,
|
||||
isRead: reminder.isRead,
|
||||
includeTime: reminder.includeTime ?? false,
|
||||
readOnly: isUpcoming,
|
||||
onReadChanged: (isRead) =>
|
||||
onReadChanged?.call(reminder, isRead),
|
||||
onDelete: () => onDelete?.call(reminder),
|
||||
onAction: (path) => onAction?.call(reminder, path),
|
||||
);
|
||||
},
|
||||
final view = views
|
||||
.firstWhereOrNull((v) => v.id == reminder.objectId);
|
||||
|
||||
return NotificationItem(
|
||||
reminderId: reminder.id,
|
||||
key: ValueKey(reminder.id),
|
||||
title: reminder.title,
|
||||
scheduled: reminder.scheduledAt,
|
||||
body: reminder.message,
|
||||
block: nodeBuilder,
|
||||
isRead: reminder.isRead,
|
||||
includeTime: reminder.includeTime ?? false,
|
||||
readOnly: isUpcoming,
|
||||
onReadChanged: (isRead) =>
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
if (rootNode == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _searchById(rootNode, blockId)?.path.first;
|
||||
return _searchById(rootNode, blockId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ class NotificationsHubEmpty extends StatelessWidget {
|
||||
const VSpace(8),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.notificationHub_emptyBody.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -975,6 +975,9 @@
|
||||
},
|
||||
"notificationHub": {
|
||||
"title": "Notifications",
|
||||
"mobile": {
|
||||
"title": "Updates"
|
||||
},
|
||||
"emptyTitle": "All caught up!",
|
||||
"emptyBody": "No pending notifications or actions. Enjoy the calm.",
|
||||
"tabs": {
|
||||
|
Loading…
Reference in New Issue
Block a user