From b80b4e3a9e3b07685012ab263ffab4970eda7086 Mon Sep 17 00:00:00 2001 From: Username1548 Date: Wed, 28 May 2025 00:30:05 +0700 Subject: [PATCH] added logic of unread notifications --- ios/Flutter/ephemeral/flutter_lldb_helper.py | 32 ++++++ ios/Flutter/ephemeral/flutter_lldbinit | 5 + .../xcshareddata/xcschemes/Runner.xcscheme | 2 + .../widgets/bottom_navigation_bar_widget.dart | 7 +- .../models/notification_model.dart | 106 +++++++++++++----- .../notifications_cubit.dart | 20 +++- .../notifications_state.dart | 4 +- .../presentation/notifications_screen.dart | 83 +++++++++----- .../widgets/notification_widget.dart | 49 ++++++-- .../repository/notifications_repository.dart | 8 ++ .../service/notifications_service.dart | 44 +++++--- lib/generated/intl/messages_en.dart | 3 + lib/generated/l10n.dart | 10 ++ lib/l10n/intl_en.arb | 3 +- 14 files changed, 291 insertions(+), 85 deletions(-) create mode 100644 ios/Flutter/ephemeral/flutter_lldb_helper.py create mode 100644 ios/Flutter/ephemeral/flutter_lldbinit diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 00000000..a88caf99 --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 00000000..e3ba6fbe --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c53e2b31..9c12df59 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> { return badges.Badge( position: badges.BadgePosition.topEnd(), badgeAnimation: const badges.BadgeAnimation.fade(), - badgeContent: Text( - (state is NotificationsStateLoaded) ? state.notificationsCount.toString() : '0', - style: const TextStyle( + showBadge: state is NotificationsStateLoaded && state.hasUnreadNotifications, + badgeContent: const Text( + '', + style: TextStyle( color: Colors.white, fontSize: 8, ), diff --git a/lib/features/notifications/models/notification_model.dart b/lib/features/notifications/models/notification_model.dart index 5bcdf18c..a8813247 100644 --- a/lib/features/notifications/models/notification_model.dart +++ b/lib/features/notifications/models/notification_model.dart @@ -1,10 +1,13 @@ sealed class BaseNotificationModel { abstract final NotificationType type; + abstract final bool isRead; } class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; final String planName; final String customerName; final String chatId; @@ -14,14 +17,16 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod required this.planName, required this.customerName, required this.chatId, + required this.isRead, }); - factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map json, NotificationType type) { + factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { return PlanSubscriptionStartedToAuthorNotification( type: type, planName: json['plan_name'], customerName: json['user_name'], chatId: json['chat_id'], + isRead: isRead ?? false, ); } } @@ -29,6 +34,8 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; final String planName; final String customerName; @@ -36,13 +43,15 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo required this.type, required this.planName, required this.customerName, + required this.isRead, }); - factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map json, NotificationType type) { + factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { return PlanSubscriptionCanceledToAuthorNotification( type: type, planName: json['plan_name'], customerName: json['user_name'], + isRead: isRead ?? false, ); } } @@ -50,6 +59,8 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo class ChosenPlanStartedToClientNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; final String planName; final int planId; @@ -57,13 +68,15 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel { required this.type, required this.planName, required this.planId, + required this.isRead, }); - factory ChosenPlanStartedToClientNotification.fromJson(Map json, NotificationType type) { + factory ChosenPlanStartedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { return ChosenPlanStartedToClientNotification( type: type, planName: json['plan_name'], planId: json['plan_id'], + isRead: isRead ?? false, ); } } @@ -71,58 +84,70 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel { class ChosenPlanTomorrowReminderToClientNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; final String planName; ChosenPlanTomorrowReminderToClientNotification({ required this.type, required this.planName, + required this.isRead, }); - factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map json, NotificationType type) { - return ChosenPlanTomorrowReminderToClientNotification( - type: type, - planName: json['plan_name'], - ); + factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + return ChosenPlanTomorrowReminderToClientNotification(type: type, planName: json['plan_name'], isRead: isRead ?? false); } } class DiaryReminderNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; + DiaryReminderNotification({required this.type, required this.isRead}); - DiaryReminderNotification({required this.type}); - - factory DiaryReminderNotification.fromJson(Map json, NotificationType type) { - return DiaryReminderNotification( - type: type, - ); + factory DiaryReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + return DiaryReminderNotification(type: type, isRead: isRead ?? false); } } class ConsultantLeftCommentNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; final DateTime date; ConsultantLeftCommentNotification({ required this.type, required this.date, + required this.isRead, }); - factory ConsultantLeftCommentNotification.fromJson(Map json, NotificationType type) { - return ConsultantLeftCommentNotification(type: type, date: json['diary_date']); + factory ConsultantLeftCommentNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + return ConsultantLeftCommentNotification( + type: type, + date: json['diary_date'], + isRead: isRead ?? false, + ); } } class PersonalizedDietIsDefinedNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; - PersonalizedDietIsDefinedNotification({required this.type}); + PersonalizedDietIsDefinedNotification({ + required this.type, + required this.isRead, + }); - factory PersonalizedDietIsDefinedNotification.fromJson(Map json, NotificationType type) { + factory PersonalizedDietIsDefinedNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { return PersonalizedDietIsDefinedNotification( type: type, + isRead: isRead ?? false, ); } } @@ -130,30 +155,41 @@ class PersonalizedDietIsDefinedNotification implements BaseNotificationModel { class ChosenPlanFinishedToClientNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; final String planName; ChosenPlanFinishedToClientNotification({ required this.type, required this.planName, + required this.isRead, }); - factory ChosenPlanFinishedToClientNotification.fromJson(Map json, NotificationType type) { - return ChosenPlanFinishedToClientNotification(type: type, planName: json['plan_name']); + factory ChosenPlanFinishedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + return ChosenPlanFinishedToClientNotification( + type: type, + planName: json['plan_name'], + isRead: isRead ?? false, + ); } } class PlanSubscriptionCanceledToCustomerNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; final String planName; PlanSubscriptionCanceledToCustomerNotification({ required this.type, required this.planName, + required this.isRead, }); - factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map json, NotificationType type) { + factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { return PlanSubscriptionCanceledToCustomerNotification( type: type, planName: json['plan_name'], + isRead: isRead ?? false, ); } } @@ -161,17 +197,21 @@ class PlanSubscriptionCanceledToCustomerNotification implements BaseNotification class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; final String planName; final String customerName; ChosenPlanStartedToConsultantNotification({ required this.type, + required this.isRead, required this.planName, required this.customerName, }); - factory ChosenPlanStartedToConsultantNotification.fromJson(Map json, NotificationType type) { + factory ChosenPlanStartedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { return ChosenPlanStartedToConsultantNotification( type: type, + isRead: isRead ?? false, planName: json['plan_name'], customerName: json['client_name'], ); @@ -181,19 +221,23 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; final String planName; final String customerName; ChosenPlanTomorrowReminderToConsultantNotification({ required this.type, required this.planName, required this.customerName, + required this.isRead, }); - factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map json, NotificationType type) { + factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { return ChosenPlanTomorrowReminderToConsultantNotification( type: type, planName: json['plan_name'], customerName: json['client_name'], + isRead: isRead ?? false, ); } } @@ -201,17 +245,21 @@ class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotifica class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; final String planName; final String customerName; PlanSubscriptionPaidToAuthorNotification({ required this.type, + required this.isRead, required this.planName, required this.customerName, }); - factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map json, NotificationType type) { + factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { return PlanSubscriptionPaidToAuthorNotification( type: type, + isRead: isRead ?? false, planName: json['plan_name'], customerName: json['user_name'], ); @@ -221,6 +269,8 @@ class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel class ChosenPlanFinishedToConsultantNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; final String planName; final String customerName; final int planId; @@ -229,14 +279,16 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode required this.planName, required this.customerName, required this.planId, + required this.isRead, }); - factory ChosenPlanFinishedToConsultantNotification.fromJson(Map json, NotificationType type) { + factory ChosenPlanFinishedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { return ChosenPlanFinishedToConsultantNotification( type: type, planName: json['plan_name'], customerName: json['client_name'], planId: json['plan_id'], + isRead: isRead ?? false, ); } } @@ -244,8 +296,12 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode class UnknownNotification implements BaseNotificationModel { @override final NotificationType type; + @override + final bool isRead; + UnknownNotification({ required this.type, + this.isRead = true, }); } diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart index 091d1f9d..5e1f1139 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -25,8 +25,7 @@ class NotificationsCubit extends Cubit { NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson, type), NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type), NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanTomorrowReminderToClientNotification => - ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type), + NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type), NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type), NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type), NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson, type), @@ -54,6 +53,7 @@ class NotificationsCubit extends Cubit { NotificationsState.loaded( notifications: _notifications, notificationsCount: _notifications.length, + hasUnreadNotifications: true, ), ); } @@ -67,10 +67,26 @@ class NotificationsCubit extends Cubit { NotificationsState.loaded( notifications: _notifications, notificationsCount: _notifications.length, + hasUnreadNotifications: (notifications ?? []).where((elem) => elem.isRead == false).isNotEmpty, ), ); } catch (e, st) { log(e.toString(), stackTrace: st); } } + + Future markNotificationsAsRead() async { + final currentState = state; + if (currentState is NotificationsStateLoaded) { + try { + emit(currentState.copyWith(isLoading: true)); + await notificationsRepository.markNotificationsAsRead(); + await getNotifications(); + emit(currentState.copyWith(isLoading: false, hasUnreadNotifications: false)); + } catch (e, st) { + log(e.toString(), stackTrace: st); + emit(currentState.copyWith(isLoading: false)); + } + } + } } diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart index 63d899b5..9c221b31 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart @@ -10,8 +10,10 @@ abstract class NotificationsState with _$NotificationsState { const factory NotificationsState.empty() = NotificationsStateEmpty; const factory NotificationsState.loaded({ - required List notifications, + required List notifications, required int notificationsCount, + @Default(false) bool isLoading, + @Default(false) bool hasUnreadNotifications, }) = NotificationsStateLoaded; const factory NotificationsState.error({ diff --git a/lib/features/notifications/presentation/notifications_screen.dart b/lib/features/notifications/presentation/notifications_screen.dart index 7c3e4581..ff48617f 100644 --- a/lib/features/notifications/presentation/notifications_screen.dart +++ b/lib/features/notifications/presentation/notifications_screen.dart @@ -1,8 +1,12 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart'; import 'package:eatvio_app/features/notifications/presentation/widgets/notification_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -11,6 +15,7 @@ class NotificationsScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final s = S.of(context); return BlocBuilder( builder: (BuildContext context, NotificationsState state) { return Scaffold( @@ -19,38 +24,58 @@ class NotificationsScreen extends StatelessWidget { title: const Text('Уведомления'), ), body: state is NotificationsStateLoaded - ? Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Container( - padding: const EdgeInsets.only(top: 16), - decoration: BoxDecoration( - color: appTheme.whiteA700, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), + ? Stack(children: [ + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Container( + padding: const EdgeInsets.only(top: 16), + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), ), - ), - child: CustomScrollView( - slivers: [ - SliverList.separated( - separatorBuilder: (context, index) => const Padding( - padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: CustomDivider(), - ), - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: NotificationWidget( - notification: state.notifications[index], - ), - ); - }, - itemCount: state.notifications.length, - ) - ], + child: state.isLoading + ? const Center(child: Loader()) + : CustomScrollView( + slivers: [ + SliverList.separated( + separatorBuilder: (context, index) => const Padding( + padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: CustomDivider(), + ), + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: NotificationWidget( + notification: state.notifications[index], + ), + ); + }, + itemCount: state.notifications.length, + ) + ], + ), ), ), - ) + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + child: CustomOutlinedButton( + text: s.markAsRead, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: + ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), + onPressed: () async { + context.read().markNotificationsAsRead(); + }, + ), + ), + ) + ]) : const Placeholder(), ); }, diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index f4d2b22b..ed63520c 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -65,7 +65,8 @@ class _PlanSubscriptionStartedToAuthorWidget extends StatelessWidget { final s = S.of(context); return _BaseNotificationWidget( - notificationsText: s.userSubscribedToPlan(notification.customerName, notification.planName), + isRead: notification.isRead, + notificationsText: s.fillYasterday, ); } } @@ -78,7 +79,8 @@ class _PlanSubscriptionCanceledToAuthorWidget extends StatelessWidget { final s = S.of(context); return _BaseNotificationWidget( - notificationsText: s.userCanceldThePlanSubscription(notification.customerName, notification.planName), + isRead: notification.isRead, + notificationsText: s.fillYasterday, ); } } @@ -91,7 +93,8 @@ class _ChosenPlanStartedToClientWidget extends StatelessWidget { final s = S.of(context); return _BaseNotificationWidget( - notificationsText: s.planStarted(notification.planName), + isRead: notification.isRead, + notificationsText: s.fillYasterday, ); } } @@ -104,7 +107,8 @@ class _ChosenPlanTomorrowReminderToClientWidget extends StatelessWidget { final s = S.of(context); return _BaseNotificationWidget( - notificationsText: s.tomorrowYourPlanWillStart(notification.planName), + isRead: notification.isRead, + notificationsText: s.fillYasterday, ); } } @@ -116,6 +120,7 @@ class _DiaryReminderWidget extends StatelessWidget { Widget build(BuildContext context) { final s = S.of(context); return _BaseNotificationWidget( + isRead: notification.isRead, notificationsText: s.fillYasterday, ); } @@ -129,6 +134,7 @@ class _ConsultantLeftCommentWidget extends StatelessWidget { final s = S.of(context); return _BaseNotificationWidget( + isRead: notification.isRead, notificationsText: s.consultantLeftComment(notification.date), ); } @@ -151,6 +157,7 @@ class _ChosenPlanFinishedToClientWidget extends StatelessWidget { final s = S.of(context); return _BaseNotificationWidget( + isRead: notification.isRead, notificationsText: '${s.congratsYouHaveFinishedPlan} «${notification.planName}»', ); } @@ -164,6 +171,7 @@ class _PlanSubscriptionCanceledToCustomerWidget extends StatelessWidget { final s = S.of(context); return _BaseNotificationWidget( + isRead: notification.isRead, notificationsText: s.youCanceledSubscriptionToPlan(notification.planName), ); } @@ -177,6 +185,7 @@ class _ChosenPlanStartedToConsultantWidget extends StatelessWidget { final s = S.of(context); return _BaseNotificationWidget( + isRead: notification.isRead, notificationsText: s.planWithUserStarted(notification.planName, notification.customerName), ); } @@ -190,6 +199,7 @@ class _ChosenPlanTomorrowReminderToConsultantWidget extends StatelessWidget { final s = S.of(context); return _BaseNotificationWidget( + isRead: notification.isRead, notificationsText: s.tommorowPlanWithUserWillStart(notification.planName, notification.customerName), ); } @@ -203,6 +213,7 @@ class _PlanSubscriptionPaidToAuthorWidget extends StatelessWidget { final s = S.of(context); return _BaseNotificationWidget( + isRead: notification.isRead, notificationsText: s.userPaidThePlan(notification.customerName, notification.planName), ); } @@ -240,16 +251,32 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget { // CardSettingsScreen class _BaseNotificationWidget extends StatelessWidget { - const _BaseNotificationWidget({required this.notificationsText}); + const _BaseNotificationWidget({required this.notificationsText, required this.isRead}); final String notificationsText; + final bool isRead; @override Widget build(BuildContext context) { - return SizedBox( - width: MediaQuery.sizeOf(context).width * 0.6, - child: Text( - notificationsText, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), + return Row( + children: [ + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.6, + child: Text( + notificationsText, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + if (!isRead) ...[ + const Spacer(), + Container( + decoration: BoxDecoration( + color: appTheme.blueA400, + shape: BoxShape.circle, + ), + width: 8, + height: 8, + ) + ] + ], ); } } diff --git a/lib/features/notifications/repository/notifications_repository.dart b/lib/features/notifications/repository/notifications_repository.dart index d9ea934d..beef645f 100644 --- a/lib/features/notifications/repository/notifications_repository.dart +++ b/lib/features/notifications/repository/notifications_repository.dart @@ -12,4 +12,12 @@ class NotificationsRepository { rethrow; } } + + Future markNotificationsAsRead() async { + try { + await notificationsService.markNotificationsAsRead(); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/notifications/service/notifications_service.dart b/lib/features/notifications/service/notifications_service.dart index b5b46e39..860aab55 100644 --- a/lib/features/notifications/service/notifications_service.dart +++ b/lib/features/notifications/service/notifications_service.dart @@ -13,23 +13,33 @@ class NotificationsService { final notifications = data.map((notificationJson) { try { final stringifiedType = notificationJson['data']['notification_class_name']; + final readDate = notificationJson['read_at']; final type = NotificationType.getTypeFromString(stringifiedType); return switch (type) { - NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type), - NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type), - NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type), + NotificationType.planSubscriptionStartedToAuthor => + PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.planSubscriptionCanceledToAuthor => + PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.chosenPlanStartedToClientNotification => + ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), NotificationType.chosenPlanTomorrowReminderToClientNotification => - ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type), - NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson['data'], type), - NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type), - NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type), - NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type), - NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type), - NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type), + ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.personalizedDietIsDefined => + PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.chosenPlanFinishedToClient => + ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.planSubscriptionCanceledToCustomer => + PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.chosenPlanStartedToConsultantNotification => + ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), NotificationType.chosenPlanTomorrowReminderToConsultantNotification => - ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type), - NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type), - NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type), + ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.planSubscriptionPaidToAuthor => + PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.chosenPlanFinishedToConsultant => + ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), NotificationType.unknown => UnknownNotification(type: type), }; } catch (e) { @@ -43,4 +53,12 @@ class NotificationsService { rethrow; } } + + Future markNotificationsAsRead() async { + try { + await dio.post('/api/profile/mark_notifications_as_readed'); + } catch (e) { + rethrow; + } + } } diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 5b68cf0c..d9e1ff07 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -424,6 +424,9 @@ class MessageLookup extends MessageLookupByLibrary { "Почта успешно скопирована", ), "male": MessageLookupByLibrary.simpleMessage("Мужской"), + "markAsRead": MessageLookupByLibrary.simpleMessage( + "Отметить всё прочитанным", + ), "meal": MessageLookupByLibrary.simpleMessage("Приём пищи"), "mealComment": MessageLookupByLibrary.simpleMessage( "Комментарий к приему пищи", diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 1e319c1d..80bc98b6 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -3334,6 +3334,16 @@ class S { args: [name, planName], ); } + + /// `Отметить всё прочитанным` + String get markAsRead { + return Intl.message( + 'Отметить всё прочитанным', + name: 'markAsRead', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 08de0607..8bc7964c 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -396,6 +396,7 @@ "tomorrowYourPlanWillStart": "Завтра начнется прохождение плана «{planName}»", "planStarted": "План «{planName}» начался", "userCanceldThePlanSubscription": "Пользователь {name} отменил подписку на план «{planName}»", - "userSubscribedToPlan": "Пользователь {name} подписался на план «{planName}»" + "userSubscribedToPlan": "Пользователь {name} подписался на план «{planName}»", + "markAsRead": "Отметить всё прочитанным" }