import 'package:didvan/config/auth_config.dart'; import 'package:didvan/config/design_config.dart'; import 'package:didvan/main.dart'; import 'package:didvan/models/comment/comment.dart'; import 'package:didvan/models/comment/feedback.dart'; import 'package:didvan/models/comment/reply.dart'; import 'package:didvan/models/comment/user.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/view/alert_data.dart'; import 'package:didvan/providers/core.dart'; import 'package:didvan/providers/user.dart'; import 'package:didvan/services/auth/token_storage.dart'; import 'package:didvan/services/content/comment_service.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:provider/provider.dart'; class CommentsState extends CoreProvier { String text = ''; int? commentId; UserOverview? replyingTo; bool showReplyBox = false; bool isSending = false; late void Function(int count) onCommentsChanged; int _count = 0; late String type; final List comments = []; final Map> _feedbackQueue = {}; int itemId = 0; /// UUID از content-service. اگر set باشد، مسیر جدید استفاده می‌شود. String? contentUuid; /// نگاشت id موقت (hashCode) به uuid اصلی برای حذف. final Map _idToUuid = {}; Future getComments() async { if (!AuthConfig.useLegacyApi && contentUuid != null && contentUuid!.isNotEmpty) { _count = 0; comments.clear(); _idToUuid.clear(); final res = await CommentService.instance.listByContent(contentUuid!); if (!res.isSuccess || res.data == null) { appState = AppState.failed; return; } // ساختار بک‌اند جدید flat است (هر کامنت replyTo دارد). برای سازگاری // با UI موجود، کامنت‌های ریشه را در سطح بالا و پاسخ‌ها را به‌عنوان // replies جمع می‌کنیم. ترتیب: قدیمی‌ترین بالا، تازه‌ترین پایین ( // نزدیک input) — مثل چت تا کاربر کامنت جدید خود را فوری ببیند. final all = res.data!; final roots = all.where((c) => c.replyToId == null).toList() ..sort((a, b) => (a.createdAt ?? DateTime(0)) .compareTo(b.createdAt ?? DateTime(0))); for (final r in roots) { final replies = all .where((c) => c.replyToId == r.id) .toList() ..sort((a, b) => (a.createdAt ?? DateTime(0)) .compareTo(b.createdAt ?? DateTime(0))); comments.add(_mapComment(r, replies.map(_mapReply).toList())); _count++; _count += replies.length; } appState = AppState.idle; notifyListeners(); return; } final service = RequestService( RequestHelper.comments(itemId, type), ); await service.httpGet(); if (service.isSuccess) { comments.clear(); final messages = service.result['comments']; for (var i = 0; i < messages.length; i++) { comments.add(CommentData.fromJson(messages[i])); _count++; for (var j = 0; j < messages[i]['replies'].length; j++) { _count++; } } appState = AppState.idle; return; } appState = AppState.failed; } Future feedback({ required int id, required bool like, required bool dislike, required int likeCount, required int dislikeCount, int? replyId, }) async { _feedbackQueue.addAll({id: MapEntry(like, dislike)}); dynamic comment; if (replyId == null) { comment = comments.firstWhere((comment) => comment.id == id); } else { comment = comments .firstWhere((comment) => comment.id == id) .replies .firstWhere((element) => element.id == replyId); } if (comment != null) { comment.feedback.like = likeCount; comment.feedback.dislike = dislikeCount; comment.disliked = dislike; comment.liked = like; notifyListeners(); } // ignore: avoid_print print('👍 feedback called: id=$id replyId=$replyId like=$like dislike=$dislike'); // مسیر جدید: endpoint POST /comments/:id/like|dislike if (!AuthConfig.useLegacyApi) { // اگر کاربر در حال toggle off است (مثلاً قبلاً like بود)، type را // به like می‌فرستیم — backend اگر قبلاً like بوده، حذف می‌کند. final wasLiked = like; // مقدار جدید (می‌خواهد like باشد) final wasDisliked = dislike; // تشخیص type: String? type; if (wasLiked) { type = 'like'; } else if (wasDisliked) { type = 'dislike'; } else { // toggle off (نه like نه dislike). از روی feedback queue تشخیص می‌دهیم. // ساده‌ترین: type قبلی را از previous _feedbackQueue حدس بزن. // در عمل، اگر هر دو false هستند یعنی toggle off؛ همان type قبلی را // می‌فرستیم تا حذف شود. (نمی‌دانیم like بود یا dislike، ولی هر // کدام بود، backend toggle می‌کند.) type = comment != null && comment.feedback.like > 0 ? 'like' : null; } // برای reply باید uuid خود reply استفاده شود نه parent. final targetId = replyId ?? id; final uuid = _idToUuid[targetId]; // ignore: avoid_print print('👍 targetId=$targetId uuid=$uuid type=$type'); if (uuid != null && type != null) { final res = await CommentService.instance.toggleReaction( commentId: uuid, type: type, ); // ignore: avoid_print print('👍 toggleReaction success=${res.isSuccess} status=${res.statusCode} err=${res.errorMessage}'); if (res.isSuccess) { // re-fetch تا likeCount/dislikeCount دقیقاً با سرور یکی شود. await getComments(); } } _feedbackQueue.remove(id); return; } Future.delayed(const Duration(milliseconds: 500), () async { if (!_feedbackQueue.containsKey(id)) return; final service = RequestService( RequestHelper.feedback(itemId, id, type), body: { 'like': _feedbackQueue[id]!.key, 'dislike': _feedbackQueue[id]!.value, }, ); await service.put(); _feedbackQueue.remove(id); }); } // ------ مسیر API جدید ------------------------------------------------- String _displayNameFor(CommentDto c) { // اگر کامنت متعلق به کاربر فعلی است، از پروفایلِ محلی استفاده کن تا // نام دقیقاً همان چیزی باشد که کاربر در پروفایل می‌بیند (ممکن است // با firstName/lastName ذخیره‌شده در Keycloak متفاوت باشد). try { final currentUserId = TokenStorage.userId; if (currentUserId != null && c.userId == currentUserId) { final localFullName = DesignConfig.context! .read() .user .fullName; if (localFullName.isNotEmpty) return localFullName; } } catch (_) {} final fullName = [c.userFirstName, c.userLastName] .where((e) => e != null && e.isNotEmpty) .join(' ') .trim(); return fullName.isEmpty ? 'کاربر' : fullName; } CommentData _mapComment(CommentDto c, List replies) { final id = c.id.hashCode; _idToUuid[id] = c.id; final currentUserId = TokenStorage.userId ?? ''; return CommentData( id: id, text: c.body, createdAt: c.createdAt?.toIso8601String() ?? DateTime.now().toIso8601String(), liked: c.likes.contains(currentUserId), disliked: c.dislikes.contains(currentUserId), feedback: FeedbackData(like: c.likeCount, dislike: c.dislikeCount), user: UserOverview( id: c.userId.hashCode, fullName: _displayNameFor(c), photo: c.userAvatar ?? '', ), replies: replies, status: 1, ); } Reply _mapReply(CommentDto c) { final id = c.id.hashCode; _idToUuid[id] = c.id; final currentUserId = TokenStorage.userId ?? ''; return Reply( id: id, text: c.body, createdAt: c.createdAt?.toIso8601String() ?? DateTime.now().toIso8601String(), liked: c.likes.contains(currentUserId), disliked: c.dislikes.contains(currentUserId), feedback: FeedbackData(like: c.likeCount, dislike: c.dislikeCount), toUser: UserOverview(id: 0, fullName: '', photo: ''), user: UserOverview( id: c.userId.hashCode, fullName: _displayNameFor(c), photo: c.userAvatar ?? '', ), status: 1, ); } Future addComment() async { final user = DesignConfig.context!.read().user; // مسیر جدید: مستقیماً به CommentService بفرست. بدون optimistic — تا // نامِ کاربر دقیقاً همان چیزی باشد که سرور (Keycloak) برمی‌گرداند و // ناهماهنگی بین نام local و server نباشد. if (!AuthConfig.useLegacyApi && contentUuid != null && contentUuid!.isNotEmpty) { final sentText = text; final parentUuid = (commentId != null) ? _idToUuid[commentId!] : null; showReplyBox = false; text = ''; isSending = true; notifyListeners(); final res = await CommentService.instance.create( contentUuid: contentUuid!, body: sentText, replyTo: parentUuid, ); if (res.isSuccess) { _count++; onCommentsChanged(_count); await getComments(); } else { ActionSheetUtils(navigatorKey.currentContext!).showAlert( AlertData( message: res.errorMessage ?? 'خطا در ثبت کامنت.', ), ); } isSending = false; commentId = null; replyingTo = null; update(); return; } if (replyingTo != null) { comments.firstWhere((comment) => comment.id == commentId).replies.add( Reply( id: 0, text: text, createdAt: DateTime.now().toString(), liked: false, disliked: false, feedback: FeedbackData(like: 0, dislike: 0), toUser: replyingTo!, user: UserOverview( id: user.id, fullName: user.fullName, photo: user.photo, ), status: 1, ), ); } else { comments.insert( 0, CommentData( id: 0, text: text, createdAt: DateTime.now().toString(), liked: false, disliked: false, feedback: FeedbackData(like: 0, dislike: 0), user: UserOverview( id: user.id, fullName: user.fullName, photo: user.photo, ), replies: [], status: 1, ), ); } final body = {}; if (commentId != null) { body.addAll({'commentId': commentId}); } if (replyingTo != null) { body.addAll({'replyUserId': replyingTo!.id}); } showReplyBox = false; update(); // نمایش فوری کامنت body.addAll({'text': text}); final service = RequestService( RequestHelper.addComment(itemId, type), body: body, ); await service.post(); if (service.isSuccess) { if (replyingTo != null) { comments .firstWhere((comment) => comment.id == commentId) .replies .firstWhere((reply) => reply.id == 0) .id = service.result['comment']['id']; } else { comments.firstWhere((comment) => comment.id == 0).id = service.result['comment']['id']; } commentId = null; replyingTo = null; update(); // آپدیت ID واقعی } } void reportComment(int id) { final service = RequestService(RequestHelper.reportComment(id)); service.post(); ActionSheetUtils(navigatorKey.currentContext!).showAlert( AlertData( message: 'گزارش شما با موفقیت ثبت شد و به زودی بررسی میگردد.', aLertType: ALertType.success, ), ); } void deleteComment(int id, int status, int? rootId) async { if (!AuthConfig.useLegacyApi && _idToUuid.containsKey(id)) { await CommentService.instance.delete(_idToUuid[id]!); } else { final service = RequestService(RequestHelper.deleteComment(id)); service.delete(); } if (rootId == null) { final comment = comments.firstWhere((element) => element.id == id); if (comment.replies.isNotEmpty) { _count = 0; await getComments(); onCommentsChanged(_count); } else { comments.remove(comment); if (status != 2) { _count--; onCommentsChanged(_count); } } } else { comments .firstWhere((element) => element.id == rootId) .replies .removeWhere((element) => element.id == id); if (status != 2) { _count--; onCommentsChanged(_count); } } notifyListeners(); } }