shop full
This commit is contained in:
@@ -51,11 +51,8 @@ export function buildProfileSeo(
|
||||
user.bio ||
|
||||
translateCommon(lang, "profileSeo.defaultBio", { siteName });
|
||||
|
||||
const cityPart = city
|
||||
? translateCommon(lang, "profileSeo.inCity", { city })
|
||||
: "";
|
||||
|
||||
const titleCore = [fullName || username, expertise, cityPart]
|
||||
const nameSegments = fullName ? [fullName, username] : [username];
|
||||
const titleCore = [...nameSegments, expertise, city]
|
||||
.filter(Boolean)
|
||||
.join(" – ");
|
||||
|
||||
@@ -69,7 +66,7 @@ export function buildProfileSeo(
|
||||
: "";
|
||||
|
||||
return {
|
||||
title: formatLocalizedSeoTitle(titleCore, lang),
|
||||
title: `${titleCore} | ${siteName}`,
|
||||
description,
|
||||
path,
|
||||
h1: fullName
|
||||
|
||||
@@ -13,11 +13,13 @@ export function buildFullPostCaption(
|
||||
packages: SelectedPostLink[],
|
||||
projects: SelectedPostLink[],
|
||||
billboards: SelectedPostLink[],
|
||||
lang?: AppLanguage
|
||||
lang?: AppLanguage,
|
||||
shops: SelectedPostLink[] = [],
|
||||
shopProducts: SelectedPostLink[] = []
|
||||
): string {
|
||||
const tagLine = taggedUsers.map((u) => `@${u.user_name}`).join(" ");
|
||||
const linkHeader = buildPostLinkCaptionHeader(
|
||||
[...packages, ...projects, ...billboards],
|
||||
[...packages, ...projects, ...billboards, ...shops, ...shopProducts],
|
||||
lang
|
||||
);
|
||||
return [description.trim(), tagLine, linkHeader].filter(Boolean).join("\n");
|
||||
@@ -29,7 +31,9 @@ export function countPostCaptionLength(
|
||||
packages: SelectedPostLink[],
|
||||
projects: SelectedPostLink[],
|
||||
billboards: SelectedPostLink[],
|
||||
lang?: AppLanguage
|
||||
lang?: AppLanguage,
|
||||
shops: SelectedPostLink[] = [],
|
||||
shopProducts: SelectedPostLink[] = []
|
||||
): number {
|
||||
return buildFullPostCaption(
|
||||
description,
|
||||
@@ -37,6 +41,8 @@ export function countPostCaptionLength(
|
||||
packages,
|
||||
projects,
|
||||
billboards,
|
||||
lang
|
||||
lang,
|
||||
shops,
|
||||
shopProducts
|
||||
).length;
|
||||
}
|
||||
|
||||
@@ -55,14 +55,19 @@ export async function getDmConversationKey(
|
||||
const cached = dmKeyCache.get(ck);
|
||||
if (cached) return cached;
|
||||
|
||||
const { privateKey } = await ensureIdentityKeys(me);
|
||||
const peerPubB64 = await fetchPeerPublicKey(peer);
|
||||
if (!peerPubB64) return null;
|
||||
try {
|
||||
const { privateKey } = await ensureIdentityKeys(me);
|
||||
const peerPubB64 = await fetchPeerPublicKey(peer);
|
||||
if (!peerPubB64) return null;
|
||||
|
||||
const peerPub = await importPublicKeySpkiB64(peerPubB64);
|
||||
const key = await deriveDmAesKey(privateKey, peerPub, me, peer);
|
||||
dmKeyCache.set(ck, key);
|
||||
return key;
|
||||
const peerPub = await importPublicKeySpkiB64(peerPubB64);
|
||||
const key = await deriveDmAesKey(privateKey, peerPub, me, peer);
|
||||
dmKeyCache.set(ck, key);
|
||||
return key;
|
||||
} catch {
|
||||
// شبکه/IndexedDB میتواند گذرا خطا بدهد — نباید کل صفحه رمزگشایی را خراب کند
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearDmKeyCache(): void {
|
||||
@@ -95,15 +100,20 @@ export async function decryptDmContent(
|
||||
const remembered = recallOutboundPlaintext(raw);
|
||||
if (remembered) return remembered;
|
||||
|
||||
const key = await getDmConversationKey(myUserId, peerUserId);
|
||||
if (!key) return E2EE_LOCKED_PREVIEW;
|
||||
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;
|
||||
const pt = await decryptText(key, raw);
|
||||
if (pt != null) {
|
||||
rememberOutboundPlaintext(raw, pt);
|
||||
return pt;
|
||||
}
|
||||
return E2EE_LOCKED_PREVIEW;
|
||||
} catch {
|
||||
// خطای گذرا در رمزگشایی یک پیام نباید بقیه پیامهای صفحه را قفل نشان دهد
|
||||
return E2EE_LOCKED_PREVIEW;
|
||||
}
|
||||
return E2EE_LOCKED_PREVIEW;
|
||||
}
|
||||
|
||||
export async function decryptDmMessageList<
|
||||
@@ -120,26 +130,38 @@ export async function decryptDmMessageList<
|
||||
): Promise<T[]> {
|
||||
const out: T[] = [];
|
||||
for (const msg of messages) {
|
||||
// خطای رمزگشایی یک پیام نباید کل صفحه را از حالت رمزگشاییشده خارج کند —
|
||||
// هر پیام مستقل پردازش میشود و در بدترین حالت فقط همان یکی قفل نمایش داده میشود
|
||||
let next = msg;
|
||||
const content = normalizeMessageContent(msg.content);
|
||||
if (content && isE2eeEnvelope(content)) {
|
||||
const decrypted = await decryptDmContent(myUserId, peerUserId, content);
|
||||
next = { ...next, content: decrypted };
|
||||
} else if (typeof msg.content !== "string" && content) {
|
||||
next = { ...next, content };
|
||||
try {
|
||||
const content = normalizeMessageContent(msg.content);
|
||||
if (content && isE2eeEnvelope(content)) {
|
||||
const decrypted = await decryptDmContent(myUserId, peerUserId, content);
|
||||
next = { ...next, content: decrypted };
|
||||
} else if (typeof msg.content !== "string" && content) {
|
||||
next = { ...next, content };
|
||||
}
|
||||
} catch {
|
||||
next = { ...next, content: E2EE_LOCKED_PREVIEW };
|
||||
}
|
||||
if (next.replyTo?.content) {
|
||||
const replyRaw = normalizeMessageContent(next.replyTo.content);
|
||||
if (isE2eeEnvelope(replyRaw)) {
|
||||
const replyContent = await decryptDmContent(
|
||||
myUserId,
|
||||
peerUserId,
|
||||
replyRaw
|
||||
);
|
||||
next = {
|
||||
...next,
|
||||
replyTo: { ...next.replyTo, content: replyContent },
|
||||
};
|
||||
try {
|
||||
if (next.replyTo?.content) {
|
||||
const replyRaw = normalizeMessageContent(next.replyTo.content);
|
||||
if (isE2eeEnvelope(replyRaw)) {
|
||||
const replyContent = await decryptDmContent(
|
||||
myUserId,
|
||||
peerUserId,
|
||||
replyRaw
|
||||
);
|
||||
next = {
|
||||
...next,
|
||||
replyTo: { ...next.replyTo, content: replyContent },
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (next.replyTo) {
|
||||
next = { ...next, replyTo: { ...next.replyTo, content: E2EE_LOCKED_PREVIEW } };
|
||||
}
|
||||
}
|
||||
out.push(next);
|
||||
|
||||
@@ -144,7 +144,10 @@ export function buildLocalizedMetadataFields(
|
||||
const finalDescription = options.description || site.defaultDescription;
|
||||
|
||||
return {
|
||||
title: finalTitle,
|
||||
// {absolute} skips the root layout's "%s | siteName" template — finalTitle
|
||||
// already carries the site-name suffix, so applying the template again
|
||||
// would duplicate it.
|
||||
title: { absolute: finalTitle },
|
||||
description: finalDescription,
|
||||
keywords: options.keywords ?? site.keywords,
|
||||
alternates: buildHreflangAlternates(options.path),
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
const POST_EDIT_WINDOW_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export function canEditPostWithinWindow(
|
||||
createdAt?: string | Date | null
|
||||
): boolean {
|
||||
if (!createdAt) return false;
|
||||
const ts = new Date(createdAt).getTime();
|
||||
if (Number.isNaN(ts)) return false;
|
||||
return Date.now() - ts < POST_EDIT_WINDOW_MS;
|
||||
}
|
||||
|
||||
export function postEditRemainingMs(
|
||||
createdAt?: string | Date | null
|
||||
): number {
|
||||
if (!createdAt) return 0;
|
||||
const ts = new Date(createdAt).getTime();
|
||||
if (Number.isNaN(ts)) return 0;
|
||||
return Math.max(0, POST_EDIT_WINDOW_MS - (Date.now() - ts));
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { DEFAULT_LANGUAGE, type AppLanguage } from "@/lib/i18n/registry";
|
||||
|
||||
export type PostLinkKind = "pkg" | "project" | "billboard";
|
||||
export type PostLinkKind = "pkg" | "project" | "billboard" | "shop" | "shopproduct";
|
||||
|
||||
/** مارکر لینک داخل کپشن */
|
||||
export const POST_LINK_TOKEN_REGEX =
|
||||
/\[\[(pkg|project|billboard):([a-f0-9]{24})\]\]([\s\S]*?)\[\[\/\1\]\]/gi;
|
||||
/\[\[(pkg|project|billboard|shop|shopproduct):([a-f0-9]{24})\]\]([\s\S]*?)\[\[\/\1\]\]/gi;
|
||||
|
||||
/** @deprecated use POST_LINK_TOKEN_REGEX */
|
||||
export const POST_PACKAGE_TOKEN_REGEX =
|
||||
@@ -19,12 +19,16 @@ const FA_LINK_LABELS = {
|
||||
pkg: "پکیج آموزشی",
|
||||
project: "پروژه",
|
||||
billboard: "بیلبورد",
|
||||
shop: "فروشگاه",
|
||||
shopproduct: "کالا",
|
||||
} as const;
|
||||
|
||||
const EN_LINK_LABELS = {
|
||||
pkg: "Training package",
|
||||
project: "Project",
|
||||
billboard: "Billboard",
|
||||
shop: "Shop",
|
||||
shopproduct: "Product",
|
||||
} as const;
|
||||
|
||||
export function getPostLinkLabels(lang: AppLanguage = DEFAULT_LANGUAGE) {
|
||||
@@ -53,6 +57,10 @@ export function buildPostLinkPath(
|
||||
return `/projects/${id}/${name}`;
|
||||
case "billboard":
|
||||
return `/billboards/${id}/${name}`;
|
||||
case "shop":
|
||||
return `/shops/profile/${id}`;
|
||||
case "shopproduct":
|
||||
return `/shops/listing/${id}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,13 +85,15 @@ export function buildPostLinkCaptionHeader(
|
||||
pkg: [],
|
||||
project: [],
|
||||
billboard: [],
|
||||
shop: [],
|
||||
shopproduct: [],
|
||||
};
|
||||
for (const link of list) {
|
||||
groups[link.kind].push(link);
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
(["pkg", "project", "billboard"] as const).forEach((kind) => {
|
||||
(["pkg", "project", "billboard", "shop", "shopproduct"] as const).forEach((kind) => {
|
||||
if (!groups[kind].length) return;
|
||||
const tokens = groups[kind]
|
||||
.map((l) => buildPostLinkCaptionToken(l.kind, l._id, l.title))
|
||||
|
||||
Reference in New Issue
Block a user