order
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Cookies from "js-cookie";
|
||||
import { clearDmKeyCache } from "@/lib/e2ee/dm";
|
||||
import {
|
||||
readLocalStorage,
|
||||
writeLocalStorage,
|
||||
@@ -65,6 +66,12 @@ export async function setAuthSession(
|
||||
): Promise<void> {
|
||||
if (!token) return;
|
||||
|
||||
// کلید AES مشترکِ هر مکالمه بر اساس جفتآیدی کش میشه — بین سوییچکردنِ اکانتها
|
||||
// روی یه مرورگر، اگه یه نسخهی «بازنده»ی race (نگاه کن به keys.ts) قبلاً این کش رو
|
||||
// پر کرده باشه، پاکش میکنیم تا هر پیام تازه با کلید هویتِ واقعاً فعلی رمزنگاری/
|
||||
// رمزگشایی بشه
|
||||
clearDmKeyCache();
|
||||
|
||||
Cookies.set(TOKEN_KEY, token, cookieOptions());
|
||||
|
||||
let synced = await syncServerCookie(token);
|
||||
@@ -89,6 +96,7 @@ export async function setAuthSession(
|
||||
|
||||
/** پاک کردن کامل session */
|
||||
export async function clearAuthSession(): Promise<void> {
|
||||
clearDmKeyCache();
|
||||
Cookies.remove(TOKEN_KEY, { path: "/" });
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
|
||||
@@ -18,6 +18,18 @@ type StoredIdentity = {
|
||||
let memoryPair: { userId: string; privateKey: CryptoKey; publicKeyB64: string } | null =
|
||||
null;
|
||||
|
||||
// وقتی کلید هویتِ یه کاربر هنوز نه توی حافظه نه توی IndexedDB موجود نیست (اولینبار
|
||||
// روی این مرورگر)، چند نقطهی مختلف اپ (مثلاً ChatSocketProvider + بارگذاری لیست
|
||||
// پیامهای یه ترد) تقریباً همزمان ensureIdentityKeys رو صدا میزنن. بدون این قفل، هر
|
||||
// کدوم جداگانه یه جفت کلید تازه میساختن و جدا-جدا با سرور ثبت میکردن (race) —
|
||||
// برندهی آخر که به سرور میرسید، کلید عمومیِ ثبتشده رو عوض میکرد و پیامهایی که با
|
||||
// کلید «بازنده» رمزنگاری/رمزگشایی شده بودن دیگه با هم جور درنمیاومدن (حتی برای خودِ
|
||||
// فرستنده). این Map تضمین میکنه برای هر کاربر فقط یه عملیاتِ ensure همزمان اجرا بشه
|
||||
const inFlight = new Map<
|
||||
string,
|
||||
Promise<{ privateKey: CryptoKey; publicKeyB64: string }>
|
||||
>();
|
||||
|
||||
function authHeaders(): HeadersInit {
|
||||
const token = Cookies.get("token");
|
||||
return {
|
||||
@@ -35,18 +47,10 @@ async function publishPublicKey(publicKeyB64: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function ensureIdentityKeys(userId: string): Promise<{
|
||||
async function loadOrCreateIdentityKeys(uid: string): Promise<{
|
||||
privateKey: CryptoKey;
|
||||
publicKeyB64: string;
|
||||
}> {
|
||||
const uid = String(userId);
|
||||
if (memoryPair?.userId === uid) {
|
||||
return {
|
||||
privateKey: memoryPair.privateKey,
|
||||
publicKeyB64: memoryPair.publicKeyB64,
|
||||
};
|
||||
}
|
||||
|
||||
const storeKey = `identity:${uid}`;
|
||||
const stored = await idbGet<StoredIdentity>(storeKey);
|
||||
|
||||
@@ -83,6 +87,28 @@ export async function ensureIdentityKeys(userId: string): Promise<{
|
||||
return { privateKey: pair.privateKey, publicKeyB64 };
|
||||
}
|
||||
|
||||
export async function ensureIdentityKeys(userId: string): Promise<{
|
||||
privateKey: CryptoKey;
|
||||
publicKeyB64: string;
|
||||
}> {
|
||||
const uid = String(userId);
|
||||
if (memoryPair?.userId === uid) {
|
||||
return {
|
||||
privateKey: memoryPair.privateKey,
|
||||
publicKeyB64: memoryPair.publicKeyB64,
|
||||
};
|
||||
}
|
||||
|
||||
const existing = inFlight.get(uid);
|
||||
if (existing) return existing;
|
||||
|
||||
const task = loadOrCreateIdentityKeys(uid).finally(() => {
|
||||
inFlight.delete(uid);
|
||||
});
|
||||
inFlight.set(uid, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
export async function fetchPeerPublicKey(
|
||||
peerUserId: string
|
||||
): Promise<string | null> {
|
||||
|
||||
@@ -33,6 +33,13 @@ export function buildExplorePostFilters(
|
||||
return { exploreFilter: "predict_trends" };
|
||||
case "best_month":
|
||||
return { exploreFilter: "best_month" };
|
||||
case "girls":
|
||||
// عمداً exploreFilter ست نمیشه — همون مسیر جستجوی هشتگ (hashtag) موجوده
|
||||
// استفاده میشه تا از فید شخصیسازیشدهی معمولی رد بشه، نه مسیر جدای
|
||||
// فیلتر بر مبنای کاربر (مثل model/photographer/...) — عین RN
|
||||
return { hashtag: "دخترانه" };
|
||||
case "boys":
|
||||
return { hashtag: "پسرانه" };
|
||||
case "near_me":
|
||||
if (coords) {
|
||||
return {
|
||||
|
||||
47
src/lib/firebase.ts
Normal file
47
src/lib/firebase.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { initializeApp, getApps, type FirebaseApp } from "firebase/app";
|
||||
import { getMessaging, isSupported, type Messaging } from "firebase/messaging";
|
||||
|
||||
// این مقادیر عمومی و امن برای مرورگرند (بر خلاف کلید سرویساکانت بکاند که فقط
|
||||
// سمت سرور استفاده میشه) — از NEXT_PUBLIC_FIREBASE_* در .env.local میان
|
||||
const firebaseConfig = {
|
||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
||||
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
||||
};
|
||||
|
||||
let app: FirebaseApp | null = null;
|
||||
|
||||
function getFirebaseApp(): FirebaseApp | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
if (!firebaseConfig.apiKey) return null;
|
||||
if (!app) {
|
||||
app = getApps().length ? getApps()[0] : initializeApp(firebaseConfig);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
let messagingPromise: Promise<Messaging | null> | null = null;
|
||||
|
||||
/** Messaging فقط توی مرورگرهایی که Service Worker/Push API دارن پشتیبانی میشه
|
||||
* (سافاری قدیمی، حالت خصوصیِ بعضی مرورگرها و... نه) — isSupported این رو چک میکنه */
|
||||
export function getFirebaseMessaging(): Promise<Messaging | null> {
|
||||
if (!messagingPromise) {
|
||||
messagingPromise = (async () => {
|
||||
const fbApp = getFirebaseApp();
|
||||
if (!fbApp) return null;
|
||||
try {
|
||||
const supported = await isSupported();
|
||||
if (!supported) return null;
|
||||
return getMessaging(fbApp);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
}
|
||||
return messagingPromise;
|
||||
}
|
||||
|
||||
export { firebaseConfig };
|
||||
60
src/lib/pushNotifications.ts
Normal file
60
src/lib/pushNotifications.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { getToken, onMessage, type MessagePayload } from "firebase/messaging";
|
||||
import { apiClient } from "@/hooks/useAxios";
|
||||
import { getApiBaseUrl } from "@/components/main/BaseUrl";
|
||||
import { getFirebaseMessaging } from "./firebase";
|
||||
|
||||
/** درخواست مجوز نوتیفیکیشن مرورگر و ثبت توکن FCM وب برای کاربر لاگینشده —
|
||||
* عین registerPushToken (pushToken.ts) اندروید، با platform:"web" که بکاند
|
||||
* (notificationController.js) جدا توی fcm_web_tokens ذخیرهش میکنه */
|
||||
export async function registerPushToken(): Promise<void> {
|
||||
try {
|
||||
if (typeof window === "undefined" || !("Notification" in window)) return;
|
||||
if (!("serviceWorker" in navigator)) return;
|
||||
|
||||
const messaging = await getFirebaseMessaging();
|
||||
if (!messaging) return;
|
||||
|
||||
let permission = Notification.permission;
|
||||
if (permission === "default") {
|
||||
permission = await Notification.requestPermission();
|
||||
}
|
||||
if (permission !== "granted") return;
|
||||
|
||||
// sw همون service-worker.js موجوده (RegisterSW.tsx) — پیامهای پوشِ پسزمینه
|
||||
// مستقیم داخلش هندل میشن (importScripts فایربیس)، پس سرویسورکرِ جدا لازم نیست
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const vapidKey = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY;
|
||||
if (!vapidKey) return;
|
||||
|
||||
const fcmToken = await getToken(messaging, {
|
||||
vapidKey,
|
||||
serviceWorkerRegistration: registration,
|
||||
});
|
||||
if (!fcmToken) return;
|
||||
|
||||
await apiClient.post(
|
||||
"/notifications/register-token",
|
||||
{ fcmToken, platform: "web" },
|
||||
{ baseURL: getApiBaseUrl() },
|
||||
);
|
||||
} catch {
|
||||
// نبود مجوز/توکن نباید هیچ جریان دیگهای رو بشکنه
|
||||
}
|
||||
}
|
||||
|
||||
/** پیامِ دریافتشده وقتی تب باز و فعاله — عین onMessage در PushNotificationListener.tsx
|
||||
* اندروید؛ خودِ مرورگر برای این حالت هیچ نوتیفِ سیستمی نشون نمیده */
|
||||
export function subscribeForegroundPush(
|
||||
onPush: (payload: MessagePayload) => void,
|
||||
): () => void {
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
let cancelled = false;
|
||||
void getFirebaseMessaging().then((messaging) => {
|
||||
if (!messaging || cancelled) return;
|
||||
unsubscribe = onMessage(messaging, onPush);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
unsubscribe?.();
|
||||
};
|
||||
}
|
||||
@@ -22,15 +22,19 @@ export function isIosDevice(): boolean {
|
||||
return /iphone|ipad|ipod/i.test(window.navigator.userAgent);
|
||||
}
|
||||
|
||||
export function isDismissed(): boolean {
|
||||
const until = Number(readLocalStorage(DISMISS_KEY) || 0);
|
||||
return Date.now() < until;
|
||||
export function isAndroidDevice(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
return /android/i.test(window.navigator.userAgent);
|
||||
}
|
||||
|
||||
/** عدم نمایش تا ۷ روز */
|
||||
export function dismissInstallPrompt(days = 7): void {
|
||||
const until = Date.now() + days * 24 * 60 * 60 * 1000;
|
||||
writeLocalStorage(DISMISS_KEY, String(until));
|
||||
// قبلاً ۷ روز بود؛ طبق خواستهی صریح کاربر الان دائمیه — با زدنِ «بعداً» دیگه هیچوقت
|
||||
// دوباره نمایش داده نمیشه (نه فقط یه هفته)
|
||||
export function isDismissed(): boolean {
|
||||
return readLocalStorage(DISMISS_KEY) === "1";
|
||||
}
|
||||
|
||||
export function dismissInstallPrompt(): void {
|
||||
writeLocalStorage(DISMISS_KEY, "1");
|
||||
}
|
||||
|
||||
export function clearInstallDismissal(): void {
|
||||
|
||||
@@ -7,7 +7,7 @@ export type PwaLang = AppLanguage;
|
||||
export const PWA_THEME_COLOR = "#FF107D";
|
||||
export const PWA_BACKGROUND_COLOR = "#FF107D";
|
||||
export const PWA_ID = "modstagram-pwa";
|
||||
export const PWA_VERSION = "11";
|
||||
export const PWA_VERSION = "12";
|
||||
|
||||
export { resolvePwaLang, toPwaLang } from "@/lib/i18n/pwaLang";
|
||||
|
||||
@@ -20,6 +20,8 @@ const COPY: Partial<Record<AppLanguage, {
|
||||
installTitle: string;
|
||||
installDescription: string;
|
||||
installButton: string;
|
||||
installWebButton: string;
|
||||
downloadAppButton: string;
|
||||
installing: string;
|
||||
later: string;
|
||||
gotIt: string;
|
||||
@@ -37,6 +39,8 @@ const COPY: Partial<Record<AppLanguage, {
|
||||
installDescription:
|
||||
"برای دسترسی سریعتر و تجربه شبیه اپ موبایل، مدستاگرام را روی گوشی یا سیستم خود نصب کنید.",
|
||||
installButton: "نصب اپلیکیشن",
|
||||
installWebButton: "نصب نسخه وب",
|
||||
downloadAppButton: "دانلود اپلیکیشن",
|
||||
installing: "در حال نصب...",
|
||||
later: "بعداً",
|
||||
gotIt: "متوجه شدم",
|
||||
@@ -58,6 +62,8 @@ const COPY: Partial<Record<AppLanguage, {
|
||||
installDescription:
|
||||
"Install Modstagram on your device for faster access and an app-like experience.",
|
||||
installButton: "Install app",
|
||||
installWebButton: "Install web app",
|
||||
downloadAppButton: "Download app",
|
||||
installing: "Installing...",
|
||||
later: "Later",
|
||||
gotIt: "Got it",
|
||||
|
||||
@@ -8,15 +8,19 @@ export const btnBase =
|
||||
|
||||
export const btnDefault = cn(
|
||||
btnBase,
|
||||
"rounded-3xl bg-white/90 px-4 text-foreground",
|
||||
"rounded-2xl bg-white/90 px-4 text-foreground",
|
||||
BTN_BORDER,
|
||||
"hover:bg-white dark:bg-neutral-900/90 dark:hover:bg-neutral-900"
|
||||
);
|
||||
|
||||
// عین Button.tsx اندروید: variant="primary" یه تینتِ کمرنگِ رنگِ اصلی (نه پرشدنِ
|
||||
// توپر) با متن/بوردرِ رنگِ اصلیه، نه پسزمینهی توپرِ صورتی + متنِ سفید — عمداً از
|
||||
// کلاسِ مشترکِ btn-modern--primary استفاده نمیکنه چون اون کلاس رو چیپهای فیلتر/
|
||||
// toggle هم استفاده میکنن که قرار نیست عوض بشن
|
||||
export const btnPrimary = cn(
|
||||
btnBase,
|
||||
"btn-modern--primary rounded-3xl px-4 text-white",
|
||||
BTN_BORDER
|
||||
"rounded-2xl bg-[rgba(252,142,172,0.1)] px-4 text-[#FC8EAC] border-[#FC8EAC]",
|
||||
"hover:bg-[rgba(252,142,172,0.16)]"
|
||||
);
|
||||
|
||||
export const btnSecondary = cn(btnDefault, "font-medium text-foreground/90");
|
||||
|
||||
Reference in New Issue
Block a user