didvan-app/lib/views/comments/comments_state.dart

404 lines
13 KiB
Dart

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<CommentData> comments = [];
final Map<int, MapEntry<bool, bool>> _feedbackQueue = {};
int itemId = 0;
/// UUID از content-service. اگر set باشد، مسیر جدید استفاده می‌شود.
String? contentUuid;
/// نگاشت id موقت (hashCode) به uuid اصلی برای حذف.
final Map<int, String> _idToUuid = {};
Future<void> 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<void> 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<UserProvider>()
.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<Reply> 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<void> addComment() async {
final user = DesignConfig.context!.read<UserProvider>().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();
}
}