diff --git a/public/service-worker.js b/public/service-worker.js index 3630751..4114631 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,33 +1,35 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +const CACHE_NAME = "pwa-cache-v2"; + // نصب Service Worker self.addEventListener("install", (event) => { console.log("Service Worker installing..."); event.waitUntil( - caches.open("pwa-cache-v1").then((cache) => { + caches.open(CACHE_NAME).then((cache) => { return cache.addAll([ "/", "/manifest.json", "/images/icons/256x256.png", - "/images/icons/512x512.png" + "/images/icons/512x512.png", ]); }) ); + self.skipWaiting(); }); // فعال‌سازی Service Worker self.addEventListener("activate", (event) => { console.log("Service Worker activated."); - // پاک کردن cacheهای قدیمی event.waitUntil( caches.keys().then((keys) => Promise.all( keys - .filter((key) => key !== "pwa-cache-v1") + .filter((key) => key !== CACHE_NAME) .map((key) => caches.delete(key)) ) - ) + ).then(() => self.clients.claim()) ); }); @@ -35,7 +37,23 @@ self.addEventListener("activate", (event) => { self.addEventListener("fetch", (event) => { const url = new URL(event.request.url); - // فقط برای فایل‌های استاتیک از cache استفاده کن + // آیکون‌ها: network-first تا تغییرات فوری اعمال شوند + if (url.pathname.startsWith("/images/icons/")) { + event.respondWith( + fetch(event.request) + .then((res) => { + if (res.ok) { + const clone = res.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); + } + return res; + }) + .catch(() => caches.match(event.request)) + ); + return; + } + + // سایر فایل‌های استاتیک: stale-while-revalidate if ( url.pathname.startsWith("/images/") || url.pathname.endsWith(".png") || @@ -46,17 +64,15 @@ self.addEventListener("fetch", (event) => { url.pathname.endsWith("manifest.json") ) { event.respondWith( - caches.match(event.request).then((response) => { - return ( - response || - fetch(event.request).then((res) => { - // فایل جدید رو هم ذخیره کن - return caches.open("pwa-cache-v1").then((cache) => { - cache.put(event.request, res.clone()); - return res; - }); - }) - ); + caches.match(event.request).then((cached) => { + const networkFetch = fetch(event.request).then((res) => { + if (res.ok) { + const clone = res.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); + } + return res; + }); + return cached || networkFetch; }) ); } diff --git a/public/sitemap-0.xml b/public/sitemap-0.xml index a985089..f311c16 100644 --- a/public/sitemap-0.xml +++ b/public/sitemap-0.xml @@ -1,11 +1,11 @@ -https://modstagram.com/robots.txt2026-07-08T17:38:00.874Zdaily0.7 -https://modstagram.com/sitemap.xml2026-07-08T17:38:00.875Zdaily0.7 -https://modstagram.com/explore2026-07-08T17:38:00.875Zdaily0.7 -https://modstagram.com/academy/payment/failed2026-07-08T17:38:00.875Zdaily0.7 -https://modstagram.com/about-us2026-07-08T17:38:00.875Zmonthly0.8 -https://modstagram.com/academy/payment/success2026-07-08T17:38:00.875Zdaily0.7 +https://modstagram.com/sitemap.xml2026-07-10T11:28:38.990Zdaily0.7 +https://modstagram.com/robots.txt2026-07-10T11:28:38.990Zdaily0.7 +https://modstagram.com/explore2026-07-10T11:28:38.991Zdaily0.7 +https://modstagram.com/academy/payment/success2026-07-10T11:28:38.991Zdaily0.7 +https://modstagram.com/about-us2026-07-10T11:28:38.991Zmonthly0.8 +https://modstagram.com/academy/payment/failed2026-07-10T11:28:38.991Zdaily0.7 https://modstagram.comdaily1 https://modstagram.com/projectsdaily0.9 https://modstagram.com/billboardsdaily0.9 diff --git a/src/app/posts/[id]/page.tsx b/src/app/posts/[id]/page.tsx index de8e093..da5cf8f 100644 --- a/src/app/posts/[id]/page.tsx +++ b/src/app/posts/[id]/page.tsx @@ -16,11 +16,9 @@ export async function generateMetadata({ params }: Props): Promise { const description = post?.caption?.slice(0, 160) || "مشاهده پست در مادستاگرام — پلتفرم مدلینگ و تبلیغات"; - const imagePath = post?.files?.[0]?.path?.replace( - "/root/modstagram-back/storage", - "" - ); - const ogImage = imagePath ? `${IMAGE_BASE_URL}${imagePath}` : undefined; + const ogImage = post?.files?.[0]?.path + ? buildStorageUrl(post.files[0].path) + : undefined; return { title, diff --git a/src/app/settings/chats/[username]/[id]/page.tsx b/src/app/settings/chats/[username]/[id]/page.tsx index 9bcd0c4..ca58a2f 100644 --- a/src/app/settings/chats/[username]/[id]/page.tsx +++ b/src/app/settings/chats/[username]/[id]/page.tsx @@ -253,34 +253,52 @@ function TicketChat({ params }: ITicketChatProps) { } } - const formData = new FormData(); - formData.append("content", textContent); - formData.append("receiverId", targetReceiverId); - formData.append("senderId", currentUserId); - if (replySource?._id && !replySource._id.startsWith("temp")) { - formData.append("replyToId", replySource._id); - } - if (fileForUpload) { - formData.append("file", fileForUpload); - if (fileType) formData.append("fileType", fileType); - } - if (selfDestructSeconds) { - formData.append("selfDestructSeconds", String(selfDestructSeconds)); - } - if (applyViewOnce) { - formData.append("viewOnce", "true"); - } + const isFileUpload = fileForUpload && fileType !== "location"; try { - const endpoint = - fileToUpload && fileType !== "location" ? "/chat/file" : "/chat"; + let response: ChatMessage | { data: ChatMessage }; - const response = await request( - "post", - endpoint, - formData, - { headers: { "Content-Type": "multipart/form-data" } } - ); + if (isFileUpload) { + const formData = new FormData(); + formData.append("content", textContent); + formData.append("receiverId", targetReceiverId); + formData.append("senderId", currentUserId); + if (replySource?._id && !replySource._id.startsWith("temp")) { + formData.append("replyToId", replySource._id); + } + formData.append("file", fileForUpload); + if (fileType) formData.append("fileType", fileType); + if (selfDestructSeconds) { + formData.append("selfDestructSeconds", String(selfDestructSeconds)); + } + if (applyViewOnce) { + formData.append("viewOnce", "true"); + } + + response = await request( + "post", + "/chat/file", + formData + ); + } else { + const payload: Record = { + content: textContent, + receiverId: targetReceiverId, + senderId: currentUserId, + }; + if (replySource?._id && !replySource._id.startsWith("temp")) { + payload.replyToId = replySource._id; + } + if (selfDestructSeconds) { + payload.selfDestructSeconds = selfDestructSeconds; + } + + response = await request( + "post", + "/chat", + payload + ); + } const serverMsg = normalizeMessage(response as ChatMessage); const sentMsg: ChatMessage = { @@ -289,10 +307,10 @@ function TicketChat({ params }: ITicketChatProps) { replyTo: serverMsg.replyTo ?? replyPayload, }; - if (currentUserId && chatPartnerId) { + if (currentUserId && targetReceiverId) { linkIds(tempId, sentMsg._id); queryClient.setQueryData( - chatThreadQueryKey(currentUserId, chatPartnerId), + chatThreadQueryKey(currentUserId, targetReceiverId), (oldData) => appendMessageToThreadCache(oldData, sentMsg) ); } diff --git a/src/components/RegisterSW.tsx b/src/components/RegisterSW.tsx index a3b8755..9ae627a 100644 --- a/src/components/RegisterSW.tsx +++ b/src/components/RegisterSW.tsx @@ -1,12 +1,15 @@ "use client"; import { useEffect } from "react"; +import { ICON_VERSION } from "@/components/main/BaseUrl"; export default function RegisterSW() { useEffect(() => { if ("serviceWorker" in navigator) { - navigator.serviceWorker.register("/service-worker.js").then(() => { - console.log("Service Worker Registered!"); - }); + navigator.serviceWorker + .register(`/service-worker.js?v=${ICON_VERSION}`) + .then(() => { + console.log("Service Worker Registered!"); + }); } }, []); diff --git a/src/components/chat/ChatBoldIcon.tsx b/src/components/chat/ChatBoldIcon.tsx index f730413..aac72f6 100644 --- a/src/components/chat/ChatBoldIcon.tsx +++ b/src/components/chat/ChatBoldIcon.tsx @@ -2,6 +2,7 @@ import { cn } from "@/lib/utils"; import BoldIcon from "@/components/ui/BoldIcon"; +import { staticIconUrl } from "@/components/main/BaseUrl"; export type ChatBoldIconName = | "timer" @@ -20,9 +21,9 @@ export type ChatBoldIconName = /** Chat-specific SVG assets (custom uploads) */ const CHAT_ICON_SRC: Partial> = { - timer: "/images/icons/chat/timer.svg", - search: "/images/icons/chat/search-normal.svg", - back: "/images/icons/chat/back.svg", + timer: staticIconUrl("/images/icons/chat/timer.svg"), + search: staticIconUrl("/images/icons/chat/search-normal.svg"), + back: staticIconUrl("/images/icons/chat/back.svg"), }; const ICON_NAME: Record = { diff --git a/src/components/chat/ChatMessageList.tsx b/src/components/chat/ChatMessageList.tsx index 8a26b60..47e91cf 100644 --- a/src/components/chat/ChatMessageList.tsx +++ b/src/components/chat/ChatMessageList.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useEffect, useState, useRef, useMemo, useCallback } from "react"; -import { SOCKET_URL } from "../main/BaseUrl"; +import { SOCKET_URL, getApiBaseUrl } from "../main/BaseUrl"; import { apiClient } from "@/hooks/useAxios"; import { chatThreadQueryKey } from "@/lib/chat/queryKeys"; import { @@ -97,7 +97,9 @@ const ChatMessageList = ({ ); const senderId = normalizeThreadId(userDetail?._id ?? getStoredUserId()); - const receiverId = normalizeThreadId(chatPartnerId); + const receiverId = normalizeThreadId( + userTwoDetail?._id ?? chatPartnerId + ); const threadReady = Boolean(senderId && receiverId); const queryKey = chatThreadQueryKey(senderId, receiverId); @@ -119,7 +121,8 @@ const ChatMessageList = ({ try { const response = await apiClient.get( - `/chat?senderId=${sid}&receiverId=${rid}&page=${pageParam}&limit=50` + `/chat?senderId=${sid}&receiverId=${rid}&page=${pageParam}&limit=50`, + { baseURL: getApiBaseUrl() } ); const msgs = (response.data.messages || []) as ChatMessage[]; return { diff --git a/src/components/main/BaseUrl.ts b/src/components/main/BaseUrl.ts index f5c9e55..be881d2 100644 --- a/src/components/main/BaseUrl.ts +++ b/src/components/main/BaseUrl.ts @@ -38,6 +38,18 @@ export const BASE_URL = getApiBaseUrl(); /** @deprecated از getStorageBaseUrl() استفاده کنید */ export const IMAGE_BASE_URL = getStorageBaseUrl(); +/** نسخه آیکون‌ها — با هر تغییر آیکون این مقدار را افزایش دهید */ +export const ICON_VERSION = + process.env.NEXT_PUBLIC_ICON_VERSION ?? "2"; + +/** URL آیکون استاتیک با cache-busting */ +export function staticIconUrl(path: string): string { + if (!path) return ""; + const normalized = path.startsWith("/") ? path : `/${path}`; + const separator = normalized.includes("?") ? "&" : "?"; + return `${normalized}${separator}v=${ICON_VERSION}`; +} + /** ساخت URL کامل فایل storage — مسیرهای DB را یکسان می‌کند */ export function buildStorageUrl(path: string | null | undefined): string { if (!path) return ""; diff --git a/src/components/models/MainModelCard/MainModelCard.tsx b/src/components/models/MainModelCard/MainModelCard.tsx index de33447..7e6df68 100644 --- a/src/components/models/MainModelCard/MainModelCard.tsx +++ b/src/components/models/MainModelCard/MainModelCard.tsx @@ -88,10 +88,7 @@ function MainModelCard({ const mediaFiles = files?.map((file) => ({ ...file, - src: `${IMAGE_BASE_URL}${file.path.replace( - "/root/modstagram-back/storage", - "" - )}`, + src: buildStorageUrl(file.path), })) || []; // تابع برای ایجاد افکت ripple و تغییر وضعیت صدا diff --git a/src/components/models/ModelPage/MainModelCardPost.tsx b/src/components/models/ModelPage/MainModelCardPost.tsx index 53771af..c033f49 100644 --- a/src/components/models/ModelPage/MainModelCardPost.tsx +++ b/src/components/models/ModelPage/MainModelCardPost.tsx @@ -71,10 +71,7 @@ function MainModelCard({ postData }: { postData: Post }) { const mediaFiles = files?.map((file) => ({ ...file, - src: `${IMAGE_BASE_URL}${file.path.replace( - "/root/modstagram-back/storage", - "" - )}`, + src: buildStorageUrl(file.path), })) || []; const sliderSettings = { diff --git a/src/components/models/ModelPage/ModelContentPosts.tsx b/src/components/models/ModelPage/ModelContentPosts.tsx index 8637873..06bcc7d 100644 --- a/src/components/models/ModelPage/ModelContentPosts.tsx +++ b/src/components/models/ModelPage/ModelContentPosts.tsx @@ -92,7 +92,7 @@ function ModelContentPosts({ filters, token, _id }: InfinitePostsProps) { const mediaFiles = post.files?.map((file) => ({ ...file, - src: `${IMAGE_BASE_URL}${file.path.replace("/root/modstagram-back/storage", "")}`, + src: buildStorageUrl(file.path), })) || []; const firstMedia = mediaFiles[0]?.src || "/images/placeholder.png"; diff --git a/src/components/posts/ReelsPostCard.tsx b/src/components/posts/ReelsPostCard.tsx index 0b30c21..932ed24 100644 --- a/src/components/posts/ReelsPostCard.tsx +++ b/src/components/posts/ReelsPostCard.tsx @@ -64,10 +64,7 @@ export default function ReelsPostCard({ const mediaFiles = files?.map((file) => ({ ...file, - src: `${IMAGE_BASE_URL}${file.path.replace( - "/root/modstagram-back/storage", - "" - )}`, + src: buildStorageUrl(file.path), })) || []; const handleVideoClick = (e: React.MouseEvent) => { diff --git a/src/components/ui/BoldIcon.tsx b/src/components/ui/BoldIcon.tsx index f900832..39c986c 100644 --- a/src/components/ui/BoldIcon.tsx +++ b/src/components/ui/BoldIcon.tsx @@ -1,6 +1,7 @@ "use client"; import { cn } from "@/lib/utils"; +import { staticIconUrl } from "@/components/main/BaseUrl"; const BOLD_BASE = "/images/icons/vuesax/bold"; @@ -17,7 +18,7 @@ interface BoldIconProps { export function boldIconUrl(name: string): string { const file = name.endsWith(".svg") ? name : `${name}.svg`; - return `${BOLD_BASE}/${file}`; + return staticIconUrl(`${BOLD_BASE}/${file}`); } export default function BoldIcon({