+
setCommentsOpen(true) : undefined
}
onCommentsModalOpenChange={setCommentsModalOpen}
- columnBottom={actionsColumnBottom ?? undefined}
/>
)}
diff --git a/src/components/ui/SocialStatButton.tsx b/src/components/ui/SocialStatButton.tsx
index 22f3890..6cd039f 100644
--- a/src/components/ui/SocialStatButton.tsx
+++ b/src/components/ui/SocialStatButton.tsx
@@ -2,7 +2,7 @@
import React from "react";
import { cn } from "@/lib/utils";
-import { formatLikeDisplay } from "@/lib/formatLikeDisplay";
+import { formatLikeDisplay, formatRequestDisplay } from "@/lib/formatLikeDisplay";
/** آیکون بالا + عدد پایین — ریلز */
export const socialStatButtonClassVertical =
@@ -18,6 +18,8 @@ export const socialStatButtonClass = socialStatButtonClassVertical;
type SocialStatButtonProps = React.ButtonHTMLAttributes & {
count?: number;
likeCount?: number;
+ /** عینِ likeCount ولی برای دکمهی درخواستِ همکاری — زیرِ آستانه «درخواست»، بالاترش عدد */
+ requestCount?: number;
icon: React.ReactNode;
showZero?: boolean;
layout?: "vertical" | "horizontal";
@@ -26,6 +28,7 @@ type SocialStatButtonProps = React.ButtonHTMLAttributes & {
export function SocialStatButton({
count,
likeCount,
+ requestCount,
icon,
showZero = true,
layout = "vertical",
@@ -37,6 +40,8 @@ export function SocialStatButton({
if (likeCount !== undefined) {
displayLabel = formatLikeDisplay(likeCount);
+ } else if (requestCount !== undefined) {
+ displayLabel = formatRequestDisplay(requestCount);
} else if (count !== undefined && (showZero || count > 0)) {
displayLabel = String(count);
}
diff --git a/src/hooks/usePostSocialActions.tsx b/src/hooks/usePostSocialActions.tsx
index 9b11297..b6f54ee 100644
--- a/src/hooks/usePostSocialActions.tsx
+++ b/src/hooks/usePostSocialActions.tsx
@@ -28,6 +28,7 @@ export function usePostSocialState(postData: Post) {
savesCount: initialSaves = 0,
sendsCount: initialSends = 0,
sharesCount: initialShares = 0,
+ offersCount: initialOffers = 0,
is_saved: initialSaved = false,
is_following: initialFollowing = false,
type: postType,
@@ -39,6 +40,7 @@ export function usePostSocialState(postData: Post) {
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(() => {
@@ -48,6 +50,7 @@ export function usePostSocialState(postData: Post) {
setSavesCount(initialSaves);
setSendsCount(initialSends);
setSharesCount(initialShares);
+ setOffersCount(initialOffers);
setFollowing(coerceBool(initialFollowing));
}, [
postId,
@@ -57,6 +60,7 @@ export function usePostSocialState(postData: Post) {
initialSaves,
initialSends,
initialShares,
+ initialOffers,
initialFollowing,
]);
@@ -230,6 +234,27 @@ export function usePostSocialState(postData: Post) {
}
}, [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,
@@ -237,6 +262,7 @@ export function usePostSocialState(postData: Post) {
savesCount,
sendsCount,
sharesCount,
+ offersCount,
setSendsCount,
following,
commentCount,
@@ -247,6 +273,7 @@ export function usePostSocialState(postData: Post) {
toggleSave,
toggleFollow,
trackShare,
+ trackOffer,
};
}
diff --git a/src/lib/chat/threadCache.ts b/src/lib/chat/threadCache.ts
index 300368b..1e0f169 100644
--- a/src/lib/chat/threadCache.ts
+++ b/src/lib/chat/threadCache.ts
@@ -66,7 +66,13 @@ export function upsertMessageInThreadCache(
return appendMessageToThreadCache(oldData, message);
}
-/** هنگام refetch، پیامهای تازهٔ cache که هنوز در پاسخ API نیستند حفظ میشوند */
+/** هنگام refetch، پیامهای تازهٔ cache که هنوز در پاسخ API نیستند حفظ میشوند —
+ * ولی فقط اگه واقعاً «تازهتر از جدیدترین پیامِ همین پاسخ» باشن (یعنی احتمالاً یه
+ * پیامِ لحظاتیپیشفرستادهشده که هنوز به این fetch نرسیده). قبلاً هر پیامِ کششدهای
+ * که غایب بود حفظ میشد، صرفنظر از تاریخش — یعنی وقتی سوکت قطع بود (پولینگ)، یه
+ * پیامِ حذفشده/یکبارمصرفِ تمامشده هیچوقت از کشِ محلیِ طرفِ مقابل پاک نمیشد، چون
+ * صفحهی اول همیشه شاملِ کلِ پیامهای تا اون لحظهست — پس هر پیامِ قدیمیترِ غایب
+ * یعنی روی سرور دیگه وجود نداره، نه اینکه هنوز sync نشده */
export function mergeFetchedThreadPage(
fetched: ChatMessage[],
cachedFirstPage: ChatMessage[] | undefined
@@ -74,10 +80,15 @@ export function mergeFetchedThreadPage(
if (!cachedFirstPage?.length) return fetched;
const fetchedIds = new Set(fetched.map((m) => String(m._id)));
+ const newestFetchedTime = fetched.reduce(
+ (max, m) => Math.max(max, parseMessageTime(m)),
+ -Infinity
+ );
const preserved = cachedFirstPage.filter(
(m) =>
/^[a-f\d]{24}$/i.test(String(m._id)) &&
- !fetchedIds.has(String(m._id))
+ !fetchedIds.has(String(m._id)) &&
+ parseMessageTime(m) > newestFetchedTime
);
if (!preserved.length) return fetched;
diff --git a/src/lib/formatLikeDisplay.ts b/src/lib/formatLikeDisplay.ts
index 6376df6..899f941 100644
--- a/src/lib/formatLikeDisplay.ts
+++ b/src/lib/formatLikeDisplay.ts
@@ -1,9 +1,20 @@
-export const LIKE_DISPLAY_THRESHOLD = 50;
-export const LIKE_LABEL_BELOW_THRESHOLD = "پسند";
+export const LIKE_DISPLAY_THRESHOLD = 10;
+export const LIKE_LABEL_BELOW_THRESHOLD = "لایک";
-/** زیر ۵۰ → «پسند»، از ۵۰ به بالا → عدد */
+/** زیر ۱۰ → «لایک»، از ۱۰ به بالا → عدد */
export function formatLikeDisplay(count: number): string {
const n = Math.max(0, Math.floor(Number(count) || 0));
if (n < LIKE_DISPLAY_THRESHOLD) return LIKE_LABEL_BELOW_THRESHOLD;
return String(n);
}
+
+export const REQUEST_DISPLAY_THRESHOLD = 10;
+export const REQUEST_LABEL_BELOW_THRESHOLD = "درخواست";
+
+/** زیر ۱۰ → «درخواست»، از ۱۰ به بالا → عدد — عینِ formatLikeDisplay برای دکمهی
+ * درخواستِ همکاری */
+export function formatRequestDisplay(count: number): string {
+ const n = Math.max(0, Math.floor(Number(count) || 0));
+ if (n < REQUEST_DISPLAY_THRESHOLD) return REQUEST_LABEL_BELOW_THRESHOLD;
+ return String(n);
+}
diff --git a/src/lib/pwaConfig.ts b/src/lib/pwaConfig.ts
index 11bd4a5..2f67311 100644
--- a/src/lib/pwaConfig.ts
+++ b/src/lib/pwaConfig.ts
@@ -95,8 +95,11 @@ export function buildWebManifest(lang: PwaLang) {
scope: "/",
start_url: `/?source=pwa&lang=${lang}`,
id: PWA_ID,
+ // طبق خواستهی کاربر: نوارِ وضعیتِ سیستمعامل هم مثلِ اندروید حذف بشه — standalone
+ // فقط نوارِ آدرسِ مرورگر رو مخفی میکنه، نه نوارِ وضعیتِ OS (ساعت/باتری)؛ fullscreen
+ // (که مرورگرهایی که پشتیبانی نمیکنن به standalone برمیگردن) اون رو هم میبره
display: "standalone",
- display_override: ["standalone", "minimal-ui", "browser"],
+ display_override: ["fullscreen", "standalone", "minimal-ui", "browser"],
orientation: "portrait-primary",
prefer_related_applications: false,
theme_color: PWA_THEME_COLOR,
diff --git a/src/types/types.ts b/src/types/types.ts
index 93bcc38..6952782 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -637,6 +637,7 @@ export interface Post {
savesCount?: number;
sendsCount?: number;
sharesCount?: number;
+ offersCount?: number;
viewsCount?: number;
author_allow_save?: boolean;
is_saved?: boolean;