feat: Implement comprehensive navigation logic for various notification types within the NotificationWidget.

This commit is contained in:
mso7nueh
2026-02-12 11:25:06 +03:00
committed by misha
parent b7804db49d
commit bb97c395dd
16 changed files with 1211 additions and 106 deletions

3
.gitignore vendored
View File

@@ -20,4 +20,5 @@ env/.env
.env
android/key.properties
app/upload-keystore.jks
coverage/
coverage/
backend/

View File

@@ -17,12 +17,25 @@ class ContentRoutes {
name: 'aboutPlanScreen',
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
final commentIdStr = state.uri.queryParameters['commentId'];
final commentId = int.tryParse(commentIdStr ?? '');
if (slug != null) {
return AboutPlanScreen(slug: slug, navigateToComments: false, index: null);
return AboutPlanScreen(
slug: slug,
navigateToComments: commentId != null,
index: null,
highlightedCommentId: commentId,
);
}
final data = state.extra as Map<String, dynamic>;
return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
return AboutPlanScreen(
slug: data['slug'] as String,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null),
highlightedCommentId: data['commentId'] as int?,
);
},
),
GoRoute(
@@ -30,12 +43,25 @@ class ContentRoutes {
name: 'aboutRecipesScreen',
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
final commentIdStr = state.uri.queryParameters['commentId'];
final commentId = int.tryParse(commentIdStr ?? '');
if (slug != null) {
return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null);
return AboutRecipeScreen(
recipeSlug: slug,
navigateToComments: commentId != null,
index: null,
highlightedCommentId: commentId,
);
}
final data = state.extra as Map<String, dynamic>;
return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
return AboutRecipeScreen(
recipeSlug: data['recipe'] as String,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null),
highlightedCommentId: data['commentId'] as int?,
);
},
),
GoRoute(
@@ -43,15 +69,25 @@ class ContentRoutes {
name: 'aboutDietScreen',
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
final commentIdStr = state.uri.queryParameters['commentId'];
final commentId = int.tryParse(commentIdStr ?? '');
if (slug != null) {
return AboutDietScreen(id: null, slug: slug, index: null, navigateToComments: false);
return AboutDietScreen(
id: null,
slug: slug,
index: null,
navigateToComments: commentId != null,
highlightedCommentId: commentId,
);
}
final data = state.extra as Map<String, dynamic>;
return AboutDietScreen(
id: data['id'] as int?,
slug: data['slug'] as String?,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool,
navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null),
highlightedCommentId: data['commentId'] as int?,
);
},
),
@@ -65,12 +101,26 @@ class ContentRoutes {
name: 'blogPostScreen',
builder: (context, state) {
final id = state.uri.queryParameters['id'];
final commentIdStr = state.uri.queryParameters['commentId'];
final commentId = int.tryParse(commentIdStr ?? '');
final parsedId = int.tryParse(id ?? '');
if (parsedId != null) {
return BlogPostScreen(id: parsedId);
return BlogPostScreen(
id: parsedId,
highlightedCommentId: commentId,
);
}
return BlogPostScreen(id: state.extra as int);
if (state.extra is int) {
return BlogPostScreen(id: state.extra as int);
}
final data = state.extra as Map<String, dynamic>;
return BlogPostScreen(
id: data['id'] as int,
highlightedCommentId: data['commentId'] as int?,
);
},
),
];

View File

@@ -21,8 +21,9 @@ import 'package:share_plus/share_plus.dart';
class BlogPostScreen extends StatefulWidget {
final int id;
final int? highlightedCommentId;
const BlogPostScreen({super.key, required this.id});
const BlogPostScreen({super.key, required this.id, this.highlightedCommentId});
@override
State<BlogPostScreen> createState() => _BlogPostScreenState();
@@ -303,7 +304,13 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
const SizedBox(height: 16),
if (state is CommentsStateLoading) const Loader(),
if (state is CommentsStateLoaded)
CommentsWidget(id: widget.id.toString(), commentSourceType: CommentSourceType.blogPost, comment: state.comment, replyId: replyId),
CommentsWidget(
id: widget.id.toString(),
commentSourceType: CommentSourceType.blogPost,
comment: state.comment,
replyId: replyId,
highlightedCommentId: widget.highlightedCommentId,
),
],
),
),

View File

@@ -568,6 +568,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
CustomTextFormField(key: AppKeys.blogPostCreateScreenVideoLinkTextField, controller: videoUrlController),
const SizedBox(height: 80),
_createBlogPostButton(),
const SizedBox(height: 80),
],
),
),

View File

@@ -37,8 +37,9 @@ class AboutDietScreen extends StatefulWidget {
final int? id;
final int? index;
final bool navigateToComments;
final int? highlightedCommentId;
const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug, required this.id});
const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug, required this.id, this.highlightedCommentId});
@override
State<AboutDietScreen> createState() => _AboutDietScreenState();
@@ -433,6 +434,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
commentSourceType: CommentSourceType.diet,
comment: state.comment,
replyId: replyId,
highlightedCommentId: widget.highlightedCommentId,
),
],
),

View File

@@ -23,14 +23,60 @@ class CommentsWidget extends StatefulWidget {
final CommentSourceType commentSourceType;
final CommentModel comment;
final int? replyId;
final int? highlightedCommentId;
const CommentsWidget({super.key, required this.id, required this.commentSourceType, required this.comment, this.replyId});
const CommentsWidget({
super.key,
required this.id,
required this.commentSourceType,
required this.comment,
this.replyId,
this.highlightedCommentId,
});
@override
State<CommentsWidget> createState() => _CommentsWidgetState();
}
class _CommentsWidgetState extends State<CommentsWidget> {
final Map<int, GlobalKey> commentKeys = {};
@override
void initState() {
super.initState();
if (widget.highlightedCommentId != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToHighlightedComment();
});
}
}
@override
void didUpdateWidget(CommentsWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.highlightedCommentId != null && widget.highlightedCommentId != oldWidget.highlightedCommentId) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToHighlightedComment();
});
} else if (widget.comment != oldWidget.comment && widget.highlightedCommentId != null) {
// Comments might have just loaded
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToHighlightedComment();
});
}
}
void _scrollToHighlightedComment() {
final key = commentKeys[widget.highlightedCommentId];
if (key != null && key.currentContext != null) {
Scrollable.ensureVisible(
key.currentContext!,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -49,8 +95,10 @@ class _CommentsWidgetState extends State<CommentsWidget> {
}
Widget _comment(S s, Comment comment, [double radius = 18, bool isChild = false]) {
final key = commentKeys.putIfAbsent(comment.id, () => GlobalKey());
final isMe = (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug;
return GestureDetector(
key: key,
onTap: () {
showCommentsActionsBottomSheet(
context,
@@ -83,7 +131,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
child: Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: widget.replyId == comment.id ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white,
color: (widget.replyId == comment.id || widget.highlightedCommentId == comment.id) ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Padding(

View File

@@ -215,6 +215,8 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel
final bool isRead;
final String planName;
final String customerName;
final int planId;
final int customerId;
@override
final DateTime createdAt;
ChosenPlanStartedToConsultantNotification({
@@ -222,6 +224,8 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel
required this.isRead,
required this.planName,
required this.customerName,
required this.planId,
required this.customerId,
required this.createdAt,
});
@@ -231,6 +235,8 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel
isRead: isRead ?? false,
planName: json['plan_name'],
customerName: json['client_name'],
planId: json['plan_id'] ?? 0,
customerId: json['client_id'] ?? 0,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
@@ -420,6 +426,7 @@ class CommentReplied implements BaseNotificationModel {
final String author;
final int authorId;
final int parentCommentId;
final int? commentId;
@override
final DateTime createdAt;
@@ -431,6 +438,7 @@ class CommentReplied implements BaseNotificationModel {
required this.author,
required this.authorId,
required this.parentCommentId,
this.commentId,
required this.isRead,
required this.createdAt,
});
@@ -444,6 +452,7 @@ class CommentReplied implements BaseNotificationModel {
author: json['author'],
authorId: json['author_id'],
parentCommentId: json['parrent_comment_id'],
commentId: json['comment_id'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
@@ -504,6 +513,7 @@ class EntityCommented implements BaseNotificationModel {
final String authorName;
final int authorId;
final String authorSlug;
final int? commentId;
@override
final DateTime createdAt;
@@ -517,6 +527,7 @@ class EntityCommented implements BaseNotificationModel {
required this.authorName,
required this.authorId,
required this.authorSlug,
this.commentId,
required this.isRead,
required this.createdAt,
});
@@ -532,6 +543,7 @@ class EntityCommented implements BaseNotificationModel {
authorName: json['author_name'],
authorId: json['author_id'],
authorSlug: json['author_slug'],
commentId: json['comment_id'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
@@ -831,7 +843,15 @@ class UnknownNotification implements BaseNotificationModel {
@override
final DateTime createdAt;
UnknownNotification({required this.type, this.isRead = true, DateTime? createdAt}) : createdAt = createdAt ?? DateTime.fromMillisecondsSinceEpoch(0);
UnknownNotification({required this.type, this.isRead = true, required this.createdAt});
factory UnknownNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return UnknownNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class DietCancelledNotification implements BaseNotificationModel {
@@ -1015,16 +1035,28 @@ class StayOnTrackToAuthorNotification implements BaseNotificationModel {
final bool isRead;
final String planName;
final String clientName;
final int planId;
final int clientId;
@override
final DateTime createdAt;
StayOnTrackToAuthorNotification({required this.type, required this.planName, required this.clientName, required this.isRead, required this.createdAt});
StayOnTrackToAuthorNotification({
required this.type,
required this.planName,
required this.clientName,
required this.planId,
required this.clientId,
required this.isRead,
required this.createdAt,
});
factory StayOnTrackToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return StayOnTrackToAuthorNotification(
type: type,
planName: json['plan_name'] ?? '',
clientName: json['client_name'] ?? '',
planId: json['plan_id'] ?? 0,
clientId: json['client_id'] ?? 0,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
@@ -1326,16 +1358,28 @@ class StayOnTrackToCustomerNotification implements BaseNotificationModel {
final bool isRead;
final String planName;
final String consultantName;
final int planId;
final int consultantId;
@override
final DateTime createdAt;
StayOnTrackToCustomerNotification({required this.type, required this.planName, required this.consultantName, required this.isRead, required this.createdAt});
StayOnTrackToCustomerNotification({
required this.type,
required this.planName,
required this.consultantName,
required this.planId,
required this.consultantId,
required this.isRead,
required this.createdAt,
});
factory StayOnTrackToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return StayOnTrackToCustomerNotification(
type: type,
planName: json['plan_name'] ?? '',
consultantName: json['consultant_name'] ?? '',
planId: json['plan_id'] ?? 0,
consultantId: json['consultant_id'] ?? 0,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
@@ -1652,14 +1696,16 @@ class ProductStatusUpdatedForAuthorNotification implements BaseNotificationModel
final String productName;
final String oldStatus;
final String newStatus;
final String status; // Added to capture single status if old/new aren't provided
@override
final DateTime createdAt;
ProductStatusUpdatedForAuthorNotification({
required this.type,
required this.productName,
required this.oldStatus,
required this.newStatus,
this.oldStatus = '',
this.newStatus = '',
this.status = '',
required this.isRead,
required this.createdAt,
});
@@ -1670,6 +1716,7 @@ class ProductStatusUpdatedForAuthorNotification implements BaseNotificationModel
productName: json['product_name'] ?? '',
oldStatus: json['old_status'] ?? '',
newStatus: json['new_status'] ?? '',
status: json['status'] ?? '',
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
@@ -1724,6 +1771,309 @@ class PersonalDietIsDefinedToCustomerNotification implements BaseNotificationMod
}
}
// New Model Classes for Comprehensive Support
class ConsultantEducationUpdatedNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final String consultantName;
ConsultantEducationUpdatedNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.consultantName,
});
factory ConsultantEducationUpdatedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ConsultantEducationUpdatedNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
consultantName: json['consultant_name'] ?? '',
);
}
}
class ComplaintRegisteredNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final String complaintType;
final String entityName;
ComplaintRegisteredNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.complaintType,
required this.entityName,
});
factory ComplaintRegisteredNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ComplaintRegisteredNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
complaintType: json['complaint_type'] ?? '',
entityName: json['entity_name'] ?? '',
);
}
}
class ContentUnpublishedNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final String reason;
final String entityName;
ContentUnpublishedNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.reason,
required this.entityName,
});
factory ContentUnpublishedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ContentUnpublishedNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
reason: json['reason'] ?? '',
entityName: json['entity_name'] ?? '',
);
}
}
class FinanceQuestionCreatedNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final String questionText;
FinanceQuestionCreatedNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.questionText,
});
factory FinanceQuestionCreatedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return FinanceQuestionCreatedNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
questionText: json['question_text'] ?? '',
);
}
}
class NewChatMessageNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final String senderName;
final String messagePreview;
final int chatId;
NewChatMessageNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.senderName,
required this.messagePreview,
required this.chatId,
});
factory NewChatMessageNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return NewChatMessageNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
senderName: json['sender_name'] ?? '',
messagePreview: json['message_preview'] ?? '',
chatId: json['chat_id'] ?? 0,
);
}
}
class CompleteRegistrationNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
CompleteRegistrationNotification({
required this.type,
required this.isRead,
required this.createdAt,
});
factory CompleteRegistrationNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return CompleteRegistrationNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class ChosenDietExpiredNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final String dietName;
ChosenDietExpiredNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.dietName,
});
factory ChosenDietExpiredNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ChosenDietExpiredNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
dietName: json['diet_name'] ?? '',
);
}
}
class MotivateUserNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final String message;
MotivateUserNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.message,
});
factory MotivateUserNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return MotivateUserNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
message: json['message'] ?? '',
);
}
}
class CommentComplaintNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final String commentPreview;
final String reason;
CommentComplaintNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.commentPreview,
required this.reason,
});
factory CommentComplaintNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return CommentComplaintNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
commentPreview: json['comment_preview'] ?? '',
reason: json['reason'] ?? '',
);
}
}
class CommentDeletedNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final String commentPreview;
CommentDeletedNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.commentPreview,
});
factory CommentDeletedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return CommentDeletedNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
commentPreview: json['comment_preview'] ?? '',
);
}
}
class UserSubscribedNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final String subscriberName;
final int subscriberId;
UserSubscribedNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.subscriberName,
required this.subscriberId,
});
factory UserSubscribedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return UserSubscribedNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
subscriberName: json['subscriber_name'] ?? '',
subscriberId: json['subscriber_id'] ?? 0,
);
}
}
enum NotificationType {
// Diet related
dietCancelled,
@@ -1731,6 +2081,7 @@ enum NotificationType {
dietExpired,
dietWillStartedIn,
dietCanNotBeStarted,
chosenDietExpired, // New
// Plan related
personalDietCreationNeeded,
@@ -1760,12 +2111,17 @@ enum NotificationType {
// Comments
entityCommented,
commentReplied,
commentComplaint, // New
commentDeleted, // New
// Profile
phoneChanged,
consultantEducationUpdated, // New
completeRegistration, // New
// Subscription
subscription,
userSubscribed, // New
// Badges
badgeAwarded,
@@ -1773,16 +2129,21 @@ enum NotificationType {
// Consultant
consultantRequestStarted,
motivateUser, // New
// Complaints
clientCloseComplaintSuccess,
complaintToModerationForUser,
complaintRegistered, // New
contentUnpublished, // New
contentUnpublishedComplaint, // New
// Finance
clientRefundSuccess,
consultantRefundSuccess,
clientRejectSuccess,
consultantRejectSuccess,
financeQuestionCreated, // New
// Score
scoreAdded,
@@ -1794,6 +2155,9 @@ enum NotificationType {
// Diary
diaryReminderForUsers,
// Chat
newChatMessage, // New
// Legacy types (keeping for backward compatibility)
planSubscriptionStartedToAuthor,
planSubscriptionCanceledToAuthor,
@@ -1865,7 +2229,23 @@ enum NotificationType {
'stay-on-track-to-customer' => NotificationType.stayOnTrackToCustomer,
'diary-reminder-for-users' => NotificationType.diaryReminderForUsers,
'personal-diet-is-defined-to-customer' => NotificationType.personalDietIsDefinedToCustomer,
// New additions
'consultant-education-updated' => NotificationType.consultantEducationUpdated,
'complaint-registered' => NotificationType.complaintRegistered,
'content-unpublished' => NotificationType.contentUnpublished,
'content-unpublished-complaint' => NotificationType.contentUnpublishedComplaint,
'finance-question-created' => NotificationType.financeQuestionCreated,
'new-chat-message' => NotificationType.newChatMessage,
'complete-registration' => NotificationType.completeRegistration,
'chosen-diet-expired' => NotificationType.chosenDietExpired,
'motivate-user' => NotificationType.motivateUser,
'comment-complaint' => NotificationType.commentComplaint,
'comment-deleted' => NotificationType.commentDeleted,
'user-subscribed' => NotificationType.userSubscribed,
// 'diary-reminder' => NotificationType.diaryReminder, // Already legacy? Check if it maps to diaryReminderNotification
'meal-reminder' => NotificationType.mealReminder, // Maps to legacy if string matches? No, legacy was App\\...
// Legacy mapping
'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor,
'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor,

View File

@@ -267,10 +267,82 @@ class NotificationsCubit extends Cubit<NotificationsState> {
isRead: isRead,
createdAt: createdAt,
),
NotificationType.unknown => UnknownNotification(type: type),
NotificationType.consultantEducationUpdated => ConsultantEducationUpdatedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.complaintRegistered => ComplaintRegisteredNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.contentUnpublished => ContentUnpublishedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.contentUnpublishedComplaint => ContentUnpublishedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.financeQuestionCreated => FinanceQuestionCreatedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.newChatMessage => NewChatMessageNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.completeRegistration => CompleteRegistrationNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenDietExpired => ChosenDietExpiredNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.motivateUser => MotivateUserNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.commentComplaint => CommentComplaintNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.commentDeleted => CommentDeletedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.userSubscribed => UserSubscribedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt ?? jsonEncode(notificationJson))),
};
} catch (e) {
return UnknownNotification(type: NotificationType.unknown);
return UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now());
}
}

View File

@@ -413,6 +413,78 @@ class NotificationsService {
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantEducationUpdated => ConsultantEducationUpdatedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.complaintRegistered => ComplaintRegisteredNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.contentUnpublished => ContentUnpublishedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.contentUnpublishedComplaint => ContentUnpublishedNotification.fromJson( // Reusing ContentUnpublishedNotification if structure is same, or create specific if different. Assuming similar for now or check backend.
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.financeQuestionCreated => FinanceQuestionCreatedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.newChatMessage => NewChatMessageNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.completeRegistration => CompleteRegistrationNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenDietExpired => ChosenDietExpiredNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.motivateUser => MotivateUserNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.commentComplaint => CommentComplaintNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.commentDeleted => CommentDeletedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.userSubscribed => UserSubscribedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)),
};
} catch (e) {

View File

@@ -32,10 +32,18 @@ import 'package:go_router/go_router.dart';
import '../../../../core/widgets/option_widget.dart';
class AboutPlanScreen extends StatefulWidget {
const AboutPlanScreen({super.key, required this.slug, required this.navigateToComments, required this.index});
final String slug;
final bool navigateToComments;
final int? index;
final int? highlightedCommentId;
const AboutPlanScreen({
super.key,
required this.slug,
required this.navigateToComments,
required this.index,
this.highlightedCommentId,
});
@override
State<AboutPlanScreen> createState() => _AboutPlanScreenState();
}
@@ -468,13 +476,14 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
),
const SizedBox(height: 16),
if (commentsState is CommentsStateLoading) const Loader(),
if (commentsState is CommentsStateLoaded)
CommentsWidget(
id: state.planModel?.id.toString() ?? '',
commentSourceType: CommentSourceType.plan,
comment: commentsState.comment,
replyId: replyId,
),
if (commentsState is CommentsStateLoaded)
CommentsWidget(
id: state.planModel?.id.toString() ?? '',
commentSourceType: CommentSourceType.plan,
comment: commentsState.comment,
replyId: replyId,
highlightedCommentId: widget.highlightedCommentId,
),
],
),
),

View File

@@ -36,8 +36,9 @@ class AboutRecipeScreen extends StatefulWidget {
final String recipeSlug;
final int? index;
final bool navigateToComments;
final int? highlightedCommentId;
const AboutRecipeScreen({super.key, required this.recipeSlug, required this.index, required this.navigateToComments});
const AboutRecipeScreen({super.key, required this.recipeSlug, required this.index, required this.navigateToComments, this.highlightedCommentId});
@override
State<AboutRecipeScreen> createState() => _AboutRecipeScreenState();
@@ -391,6 +392,7 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
commentSourceType: CommentSourceType.recipe,
comment: state.comment,
replyId: replyId,
highlightedCommentId: widget.highlightedCommentId,
),
],
),

View File

@@ -101,7 +101,7 @@ void main() {
registerMocks(cubit, commentsCubit);
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false)));
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null)));
await tester.pump();
expect(find.byType(AboutDietScreen), findsOneWidget);
@@ -127,7 +127,7 @@ void main() {
registerMocks(cubit, commentsCubit);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false)));
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null)));
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
});
@@ -157,7 +157,7 @@ void main() {
registerMocks(cubit, commentsCubit);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false)));
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null)));
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
});
@@ -183,7 +183,7 @@ void main() {
registerMocks(cubit, commentsCubit);
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false)));
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null)));
await tester.pump();
expect(find.byType(AboutDietScreen), findsOneWidget);

View File

@@ -124,7 +124,7 @@ void main() {
});
test('UnknownNotification uses default createdAt when not provided', () {
final notification = UnknownNotification(type: NotificationType.unknown);
final notification = UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now());
expect(notification.type, NotificationType.unknown);
expect(notification.isRead, true);

View File

@@ -160,6 +160,8 @@ void main() {
type: NotificationType.chosenPlanStartedToConsultant,
planName: 'Test Plan',
customerName: 'Test Customer',
planId: 1,
customerId: 1,
isRead: false,
createdAt: testDate,
);
@@ -586,6 +588,8 @@ void main() {
type: NotificationType.stayOnTrackToAuthor,
planName: 'Test Plan',
clientName: 'Test Client',
planId: 1,
clientId: 1,
isRead: false,
createdAt: testDate,
);
@@ -752,6 +756,8 @@ void main() {
type: NotificationType.stayOnTrackToCustomer,
planName: 'Test Plan',
consultantName: 'Test Consultant',
planId: 1,
consultantId: 1,
isRead: false,
createdAt: testDate,
);

View File

@@ -123,7 +123,7 @@ void main() {
registerMocks(planCubit, commentsCubit);
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null)));
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
await tester.pump();
expect(find.byType(AboutPlanScreen), findsOneWidget);
@@ -149,7 +149,7 @@ void main() {
registerMocks(planCubit, commentsCubit);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null)));
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
await tester.pump();
await tester.pump();
});
@@ -177,7 +177,7 @@ void main() {
registerMocks(planCubit, commentsCubit);
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null)));
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
await tester.pump();
expect(find.byType(AboutPlanScreen), findsOneWidget);