"use client"; import { useCallback, useEffect, useState } from "react"; import Image from "next/image"; import { Post } from "@/types/types"; import useAxios from "@/hooks/useAxios"; import toast from "react-hot-toast"; import { AxiosError } from "axios"; import { useRequireAuth } from "@/lib/auth/useRequireAuth"; import { getStoredUserId } from "@/lib/auth/session"; import useInfiniteScroll from "@/hooks/useInfiniteScroll"; import { buildPostPublicUrl } from "@/lib/postSlug"; import Cookies from "js-cookie"; import { trackExploreInteraction } from "@/api/trackExploreInteraction"; import { updateReelsSeedPostEngagement } from "@/lib/reelsSeedPost"; import { useTranslation } from "react-i18next"; function coerceBool(value: unknown): boolean { return value === true || value === 1 || value === "true" || value === "1"; } export function usePostSocialState(postData: Post) { const { _id: postId, user_id, likesCount, is_liked, savesCount: initialSaves = 0, sendsCount: initialSends = 0, sharesCount: initialShares = 0, offersCount: initialOffers = 0, is_saved: initialSaved = false, is_following: initialFollowing = false, type: postType, } = postData; const [liked, setLiked] = useState(() => coerceBool(is_liked)); const [likesCountState, setLikesCount] = useState(likesCount || 0); const [saved, setSaved] = useState(() => coerceBool(initialSaved)); const [savesCount, setSavesCount] = useState(initialSaves); const [sendsCount, setSendsCount] = useState(initialSends); const [sharesCount, setSharesCount] = useState(initialShares); const [offersCount, setOffersCount] = useState(initialOffers); const [following, setFollowing] = useState(() => coerceBool(initialFollowing)); useEffect(() => { setLiked(coerceBool(is_liked)); setLikesCount(likesCount || 0); setSaved(coerceBool(initialSaved)); setSavesCount(initialSaves); setSendsCount(initialSends); setSharesCount(initialShares); setOffersCount(initialOffers); setFollowing(coerceBool(initialFollowing)); }, [ postId, is_liked, likesCount, initialSaved, initialSaves, initialSends, initialShares, initialOffers, initialFollowing, ]); const { t } = useTranslation("common"); const { request, loading } = useAxios(); const requireAuth = useRequireAuth(); const currentUserId = getStoredUserId(); const isOwnPost = Boolean(currentUserId && user_id && currentUserId === String(user_id)); const shareUrl = buildPostPublicUrl(postId, postData); const { data } = useInfiniteScroll({ endpoint: "/users/comments", queryKey: ["comments", user_id, postId || "all"], params: postId ? { user_id, post_id: postId } : { user_id }, }); const commentCount = data?.pages ?.flatMap((p) => p?.comments || []) .filter((c: { post_id?: string; postId?: string }) => postId ? c.post_id === postId || c.postId === postId || (!c.post_id && !c.postId) : true ).length ?? 0; const toggleLike = useCallback(async () => { if (!requireAuth()) return; if (!user_id || !postId) { toast.error(t("posts.loginToPost")); return; } if (loading) return; const newLiked = !liked; const newLikesCount = newLiked ? likesCountState + 1 : likesCountState - 1; setLiked(newLiked); setLikesCount(newLikesCount); try { const response = await request<{ likesCount?: number; is_liked?: boolean }>( "POST", "/posts/like", { postId }, { noToast: true } ); setLikesCount(response?.likesCount ?? newLikesCount); const resolvedLiked = coerceBool(response?.is_liked ?? newLiked); setLiked(resolvedLiked); updateReelsSeedPostEngagement(String(postId), { is_liked: resolvedLiked, likesCount: response?.likesCount ?? newLikesCount, }); const token = Cookies.get("token") || ""; if (token && newLiked) { trackExploreInteraction( { targetType: "post", targetId: postId, authorId: user_id, contentType: postType === "video" ? "video" : "image", action: "like", }, token ); } } catch (error: unknown) { let errorMessage = t("errors.serverConnection"); if (error instanceof Error) errorMessage = error.message; else if (error instanceof AxiosError) errorMessage = error.response?.data?.message ?? error.message; toast.error(errorMessage); setLiked(!newLiked); setLikesCount(likesCountState); } }, [ requireAuth, user_id, postId, loading, liked, likesCountState, request, postType, t, ]); const toggleSave = useCallback(async () => { if (!requireAuth()) return; if (!postId || loading) return; const newSaved = !saved; const newCount = newSaved ? savesCount + 1 : Math.max(0, savesCount - 1); setSaved(newSaved); setSavesCount(newCount); try { const response = await request<{ savesCount?: number; is_saved?: boolean }>( "POST", "/posts/bookmark", { postId }, { noToast: true } ); setSavesCount(response?.savesCount ?? newCount); const resolvedSaved = coerceBool(response?.is_saved ?? newSaved); setSaved(resolvedSaved); updateReelsSeedPostEngagement(String(postId), { is_saved: resolvedSaved, savesCount: response?.savesCount ?? newCount, }); } catch { toast.error(t("posts.bookmarkError")); setSaved(!newSaved); setSavesCount(savesCount); } }, [requireAuth, postId, loading, saved, savesCount, request, t]); const toggleFollow = useCallback(async () => { if (!requireAuth()) return; if (!user_id || isOwnPost || loading) return; const newFollowing = !following; setFollowing(newFollowing); try { const response = await request<{ is_following?: boolean }>( "POST", "/posts/follow", { userId: user_id }, { noToast: true } ); setFollowing(response?.is_following ?? newFollowing); toast.success( newFollowing ? t("posts.followed") : t("stories.unfollowed") ); } catch { toast.error(t("stories.followError")); setFollowing(!newFollowing); } }, [requireAuth, user_id, isOwnPost, loading, following, request, t]); const trackShare = useCallback(async () => { const token = Cookies.get("token") || ""; if (!token) return; trackExploreInteraction( { targetType: "post", targetId: postId, authorId: user_id, contentType: postType === "video" ? "video" : "image", action: "share", }, token ); if (!postId) return; try { const response = await request<{ sharesCount?: number }>( "POST", "/posts/share", { postId }, { noToast: true } ); if (typeof response?.sharesCount === "number") { setSharesCount(response.sharesCount); } } catch { /* best-effort share count */ } }, [postId, user_id, postType, request]); // برخلافِ share/send، درخواستِ همکاری endpoint اختصاصیِ برگردوننده‌ی شمارشِ تازه // نداره — فقط رویدادِ ExploreInteraction(action:"offer") ثبت می‌شه (همونی که // postEngagement.js سمتِ بک‌اند برای offersCount می‌شمردش) و شمارش سمتِ کلاینت // خوش‌بینانه +۱ می‌شه const trackOffer = useCallback(() => { const token = Cookies.get("token") || ""; if (!token || !postId) return; setOffersCount((c) => c + 1); trackExploreInteraction( { targetType: "post", targetId: postId, authorId: user_id, contentType: postType === "video" ? "video" : "image", action: "offer", }, token ); }, [postId, user_id, postType]); return { liked, likesCountState, saved, savesCount, sendsCount, sharesCount, offersCount, setSendsCount, following, commentCount, shareUrl, currentUserId, isOwnPost, toggleLike, toggleSave, toggleFollow, trackShare, trackOffer, }; } export type PostSocialIconSize = "sm" | "md"; export const POST_SOCIAL_ICONS = { like: (liked: boolean, size: PostSocialIconSize) => ({ src: liked ? "/images/icons/red-heart.svg" : "/images/icons/heart.svg", w: size === "sm" ? 20 : 28, className: liked ? "drop-shadow-lg" : size === "sm" ? "dark:invert drop-shadow-lg" : "invert brightness-125 drop-shadow-lg", }), comment: (size: PostSocialIconSize) => ({ src: "/images/icons/message-text.svg", w: size === "sm" ? 20 : 28, className: size === "sm" ? "dark:invert" : "invert brightness-125 drop-shadow-lg", }), share: (size: PostSocialIconSize) => ({ src: "/images/icons/share.svg", w: size === "sm" ? 20 : 28, className: size === "sm" ? "dark:invert" : "invert brightness-125 drop-shadow-lg", }), send: (size: PostSocialIconSize) => ({ src: "/images/icons/send-post.svg", w: size === "sm" ? 20 : 28, className: size === "sm" ? "dark:invert" : "invert brightness-125 drop-shadow-lg", }), save: (saved: boolean, size: PostSocialIconSize) => ({ src: "/images/icons/bookmark-post.svg", w: size === "sm" ? 20 : 28, className: saved ? "drop-shadow-lg" : size === "sm" ? "dark:invert" : "invert brightness-125 drop-shadow-lg", }), follow: (following: boolean, size: PostSocialIconSize) => ({ src: "/images/icons/follow-user.svg", w: size === "sm" ? 20 : 28, className: following ? "opacity-60 drop-shadow-lg" : size === "sm" ? "dark:invert" : "invert brightness-125 drop-shadow-lg", }), collaboration: (size: PostSocialIconSize) => ({ src: "/images/icons/collaboration-request.svg", w: size === "sm" ? 20 : 28, className: size === "sm" ? "dark:invert" : "invert brightness-125 drop-shadow-lg", }), } as const; export function PostSocialIcon({ src, w, className, alt = "", }: { src: string; w: number; className?: string; alt?: string; }) { return ( {alt} ); }