404 lines
13 KiB
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();
|
|
}
|
|
}
|