reels
This commit is contained in:
@@ -45,15 +45,19 @@ export function normalizeMessageContent(content: unknown): string {
|
||||
|
||||
export async function getDmConversationKey(
|
||||
myUserId: string,
|
||||
peerUserId: string
|
||||
peerUserId: string,
|
||||
/** Bypass the in-memory cache and re-derive from a freshly-fetched peer key — used to recover from a peer key that rotated mid-session (e.g. they linked/reset a device). */
|
||||
forceFresh = false
|
||||
): Promise<CryptoKey | null> {
|
||||
const me = String(myUserId || "").trim();
|
||||
const peer = String(peerUserId || "").trim();
|
||||
if (!me || !peer) return null;
|
||||
|
||||
const ck = cacheKey(me, peer);
|
||||
const cached = dmKeyCache.get(ck);
|
||||
if (cached) return cached;
|
||||
if (!forceFresh) {
|
||||
const cached = dmKeyCache.get(ck);
|
||||
if (cached) return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
const { privateKey } = await ensureIdentityKeys(me);
|
||||
@@ -102,13 +106,25 @@ export async function decryptDmContent(
|
||||
|
||||
try {
|
||||
const key = await getDmConversationKey(myUserId, peerUserId);
|
||||
if (!key) return E2EE_LOCKED_PREVIEW;
|
||||
|
||||
const pt = await decryptText(key, raw);
|
||||
if (pt != null) {
|
||||
rememberOutboundPlaintext(raw, pt);
|
||||
return pt;
|
||||
if (key) {
|
||||
const pt = await decryptText(key, raw);
|
||||
if (pt != null) {
|
||||
rememberOutboundPlaintext(raw, pt);
|
||||
return pt;
|
||||
}
|
||||
}
|
||||
|
||||
// کلید کششده ممکن است با کلید فعلی طرف مقابل همخوان نباشد (مثلاً بعد از
|
||||
// لینک/ریست دستگاه) — یک بار با دریافت مجدد کلید عمومی طرف مقابل تلاش کن
|
||||
const freshKey = await getDmConversationKey(myUserId, peerUserId, true);
|
||||
if (freshKey) {
|
||||
const pt = await decryptText(freshKey, raw);
|
||||
if (pt != null) {
|
||||
rememberOutboundPlaintext(raw, pt);
|
||||
return pt;
|
||||
}
|
||||
}
|
||||
|
||||
return E2EE_LOCKED_PREVIEW;
|
||||
} catch {
|
||||
// خطای گذرا در رمزگشایی یک پیام نباید بقیه پیامهای صفحه را قفل نشان دهد
|
||||
|
||||
@@ -82,16 +82,20 @@ async function persistRoomRaw(
|
||||
|
||||
export async function ensureRoomAesKey(
|
||||
myUserId: string,
|
||||
roomId: string
|
||||
roomId: string,
|
||||
/** Bypass the in-memory/IndexedDB cache and re-unwrap from the server's current wrapped key. */
|
||||
forceFresh = false
|
||||
): Promise<CryptoKey | null> {
|
||||
const cached = roomAesCache.get(roomId);
|
||||
if (cached) return cached;
|
||||
if (!forceFresh) {
|
||||
const cached = roomAesCache.get(roomId);
|
||||
if (cached) return cached;
|
||||
}
|
||||
|
||||
const { privateKey } = await ensureIdentityKeys(myUserId);
|
||||
const info = await fetchRoomKeys(roomId);
|
||||
if (!info) return null;
|
||||
|
||||
let raw = await loadCachedRoomRaw(myUserId, roomId);
|
||||
let raw = forceFresh ? null : await loadCachedRoomRaw(myUserId, roomId);
|
||||
|
||||
if (!raw && info.myWrappedKey) {
|
||||
try {
|
||||
@@ -177,10 +181,21 @@ export async function decryptRoomContent(
|
||||
content: string
|
||||
): Promise<string> {
|
||||
if (!isE2eeEnvelope(content)) return content;
|
||||
|
||||
const key = await ensureRoomAesKey(myUserId, roomId);
|
||||
if (!key) return E2EE_LOCKED_PREVIEW;
|
||||
const pt = await decryptText(key, content);
|
||||
return pt ?? E2EE_LOCKED_PREVIEW;
|
||||
if (key) {
|
||||
const pt = await decryptText(key, content);
|
||||
if (pt != null) return pt;
|
||||
}
|
||||
|
||||
// کلید کششده ممکن است دیگر با wrapped key فعلی سرور همخوان نباشد — یک بار تازه امتحان کن
|
||||
const freshKey = await ensureRoomAesKey(myUserId, roomId, true);
|
||||
if (freshKey) {
|
||||
const pt = await decryptText(freshKey, content);
|
||||
if (pt != null) return pt;
|
||||
}
|
||||
|
||||
return E2EE_LOCKED_PREVIEW;
|
||||
}
|
||||
|
||||
export async function decryptRoomMessageList<
|
||||
|
||||
@@ -11,6 +11,8 @@ export type LanguageDefinition = {
|
||||
dateLocale: string;
|
||||
/** Show in language picker */
|
||||
enabled: boolean;
|
||||
/** Shown in the quick-switch grid (fa/en only) — every other enabled language still appears on the full "select language" page. */
|
||||
primary?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -30,6 +32,7 @@ export const LANGUAGE_REGISTRY: Record<string, LanguageDefinition> = {
|
||||
ogLocale: "fa_IR",
|
||||
dateLocale: "fa-IR",
|
||||
enabled: true,
|
||||
primary: true,
|
||||
},
|
||||
en: {
|
||||
code: "en",
|
||||
@@ -40,12 +43,13 @@ export const LANGUAGE_REGISTRY: Record<string, LanguageDefinition> = {
|
||||
ogLocale: "en_US",
|
||||
dateLocale: "en-US",
|
||||
enabled: true,
|
||||
primary: true,
|
||||
},
|
||||
// Below: registered but not yet enabled — no locale files exist for these yet, so
|
||||
// they render in the picker's "Other languages" section without being selectable.
|
||||
// Set enabled: true once src/locales/{code}/common.json (+ seo.json) exist and are
|
||||
// registered in src/lib/i18n/resources.ts.
|
||||
ar: { code: "ar", nativeLabel: "العربية", englishLabel: "Arabic", direction: "rtl", htmlLang: "ar", ogLocale: "ar_SA", dateLocale: "ar-SA", enabled: false },
|
||||
ar: { code: "ar", nativeLabel: "العربية", englishLabel: "Arabic", direction: "rtl", htmlLang: "ar", ogLocale: "ar_SA", dateLocale: "ar-SA", enabled: true },
|
||||
ku: { code: "ku", nativeLabel: "کوردی", englishLabel: "Kurdish", direction: "rtl", htmlLang: "ku", ogLocale: "ku_TR", dateLocale: "ku", enabled: false },
|
||||
tr: { code: "tr", nativeLabel: "Türkçe", englishLabel: "Turkish", direction: "ltr", htmlLang: "tr", ogLocale: "tr_TR", dateLocale: "tr-TR", enabled: false },
|
||||
az: { code: "az", nativeLabel: "Azərbaycan dili", englishLabel: "Azerbaijani", direction: "ltr", htmlLang: "az", ogLocale: "az_AZ", dateLocale: "az-AZ", enabled: false },
|
||||
@@ -66,7 +70,7 @@ export const LANGUAGE_REGISTRY: Record<string, LanguageDefinition> = {
|
||||
bn: { code: "bn", nativeLabel: "বাংলা", englishLabel: "Bengali", direction: "ltr", htmlLang: "bn", ogLocale: "bn_BD", dateLocale: "bn-BD", enabled: false },
|
||||
th: { code: "th", nativeLabel: "ไทย", englishLabel: "Thai", direction: "ltr", htmlLang: "th", ogLocale: "th_TH", dateLocale: "th-TH", enabled: false },
|
||||
ms: { code: "ms", nativeLabel: "Bahasa Melayu", englishLabel: "Malay", direction: "ltr", htmlLang: "ms", ogLocale: "ms_MY", dateLocale: "ms-MY", enabled: false },
|
||||
fil: { code: "fil", nativeLabel: "Filipino", englishLabel: "Filipino", direction: "ltr", htmlLang: "fil", ogLocale: "fil_PH", dateLocale: "fil-PH", enabled: false },
|
||||
tl: { code: "tl", nativeLabel: "Filipino", englishLabel: "Filipino", direction: "ltr", htmlLang: "tl", ogLocale: "fil_PH", dateLocale: "fil-PH", enabled: false },
|
||||
fr: { code: "fr", nativeLabel: "Français", englishLabel: "French", direction: "ltr", htmlLang: "fr", ogLocale: "fr_FR", dateLocale: "fr-FR", enabled: false },
|
||||
nl: { code: "nl", nativeLabel: "Nederlands", englishLabel: "Dutch", direction: "ltr", htmlLang: "nl", ogLocale: "nl_NL", dateLocale: "nl-NL", enabled: false },
|
||||
no: { code: "no", nativeLabel: "Norsk", englishLabel: "Norwegian", direction: "ltr", htmlLang: "no", ogLocale: "nb_NO", dateLocale: "nb-NO", enabled: false },
|
||||
@@ -93,6 +97,11 @@ export const ENABLED_LANGUAGES = SUPPORTED_LANGUAGES.filter(
|
||||
(code) => LANGUAGE_REGISTRY[code]?.enabled
|
||||
);
|
||||
|
||||
/** Languages shown in the quick-switch grid — currently fa/en only; every enabled language is still selectable from the full "select language" page. */
|
||||
export const PRIMARY_LANGUAGES = ENABLED_LANGUAGES.filter(
|
||||
(code) => LANGUAGE_REGISTRY[code]?.primary
|
||||
);
|
||||
|
||||
export function getLanguageDefinition(
|
||||
code: string | null | undefined
|
||||
): LanguageDefinition {
|
||||
|
||||
@@ -2,6 +2,8 @@ import faCommon from "@/locales/fa/common.json";
|
||||
import faSeo from "@/locales/fa/seo.json";
|
||||
import enCommon from "@/locales/en/common.json";
|
||||
import enSeo from "@/locales/en/seo.json";
|
||||
import arCommon from "@/locales/ar/common.json";
|
||||
import arSeo from "@/locales/ar/seo.json";
|
||||
import {
|
||||
DEFAULT_LANGUAGE,
|
||||
ENABLED_LANGUAGES,
|
||||
@@ -29,6 +31,10 @@ export const LOCALE_BUNDLES: Partial<
|
||||
common: enCommon,
|
||||
seo: enSeo,
|
||||
},
|
||||
ar: {
|
||||
common: arCommon,
|
||||
seo: arSeo,
|
||||
},
|
||||
};
|
||||
|
||||
export function buildI18nResources() {
|
||||
|
||||
28
src/lib/shopDiscount.ts
Normal file
28
src/lib/shopDiscount.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
type VariantLike = {
|
||||
price: number;
|
||||
discount_price?: number | null;
|
||||
};
|
||||
|
||||
type ListingLike = {
|
||||
variants?: VariantLike[] | null;
|
||||
};
|
||||
|
||||
/** Highest discount percentage across a listing's variants, or null when none are discounted. */
|
||||
export function bestDiscountPercent(listing: ListingLike): number | null {
|
||||
if (!listing.variants || listing.variants.length === 0) return null;
|
||||
let best: number | null = null;
|
||||
for (const v of listing.variants) {
|
||||
if (!v.discount_price || v.discount_price >= v.price || v.price <= 0) continue;
|
||||
const percent = Math.round(((v.price - v.discount_price) / v.price) * 100);
|
||||
if (best == null || percent > best) best = percent;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/** Discount percentage for a single matched variant, or null when not discounted. */
|
||||
export function variantDiscountPercent(variant: VariantLike | null | undefined): number | null {
|
||||
if (!variant?.discount_price || variant.discount_price >= variant.price || variant.price <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.round(((variant.price - variant.discount_price) / variant.price) * 100);
|
||||
}
|
||||
Reference in New Issue
Block a user