This commit is contained in:
payacom
2026-09-13 21:32:32 +03:30
parent 901ce94443
commit 9d48c0534e
21 changed files with 2326 additions and 336 deletions

View File

@@ -1,7 +1,6 @@
import {
decryptText,
deriveDmAesKey,
encryptText,
importPublicKeySpkiB64,
} from "./crypto";
import { E2EE_LOCKED_PREVIEW, isE2eeEnvelope } from "./envelope";
@@ -78,24 +77,19 @@ export function clearDmKeyCache(): void {
dmKeyCache.clear();
}
// دوباره فعال شده — حالا که بک‌آپِ کلید با PIN (keys.ts: saveKeyBackup/
// recoverIdentityFromBackup) و چکِ ایمنِ «هویتِ قبلی روی سرور هست ولی این دستگاه
// کلیدِ محلی نداره» (keys.ts: IdentityRecoveryNeededError) قبل از این وجود دارن، یه
// کلیدِ محلی هیچ‌وقت بی‌صدا جایگزینِ یه هویتِ قدیمی نمی‌شه. اگه کلیدِ طرفِ مقابل در
// دسترس نباشه (fail شدنِ getDmConversationKey)، متنِ ساده ارسال می‌شه — بهتر از
// شکستِ کاملِ ارسالِ پیام، ولی این حالت باید نادر باشه
// موقتاً غیرفعال شده به‌درخواستِ کاربر — با این‌که زیرساختِ بازیابیِ خودکار (رمزِ
// per-account، keys.ts: tryAutoRecoverIdentity) طبق طراحی دقیقاً همون رفتارِ
// «بدونِ رمزِ دستی مثلِ واتساپ» رو داشت، هنوز موردهایی بود که طرفِ مقابل همچنان
// «پیام رمزنگاری‌شده» می‌دید (هم توی وب هم اندروید). تا زمانی که با لاگِ واقعی
// ریشه‌یابی و رفع بشه، پیام‌های جدید ساده (بدونِ envelope) ارسال می‌شن —
// decryptDmContent برای پیام‌های قدیمیِ رمزنگاری‌شده دست‌نخورده مونده. کلِ زیرساختِ
// کلید/بک‌آپ دست‌نخورده مونده تا بعداً دوباره همینجا فعال بشه (فقط همین return رو بردار)
export async function encryptDmContent(
myUserId: string,
peerUserId: string,
plaintext: string
): Promise<string | null> {
const key = await getDmConversationKey(myUserId, peerUserId);
if (!key) return plaintext;
try {
return encryptText(key, plaintext);
} catch {
return plaintext;
}
return plaintext;
}
export async function decryptDmContent(

View File

@@ -2,7 +2,6 @@ import Cookies from "js-cookie";
import { getApiBaseUrl } from "@/components/main/BaseUrl";
import {
decryptText,
encryptText,
generateRoomKeyRaw,
importPublicKeySpkiB64,
importRoomAesKey,
@@ -224,16 +223,15 @@ export async function ensureRoomAesKey(
return task;
}
// موقتاً غیرفعال شده به‌درخواستِ کاربر — عینِ همون دلیلِ encryptDmContent (dm.ts).
// پیام‌های جدیدِ روم ساده ارسال می‌شن؛ decryptRoomContent برای پیام‌های قدیمیِ
// رمزنگاری‌شده دست‌نخورده می‌مونه. برای فعال‌سازیِ دوباره، همین return رو بردار.
export async function encryptRoomContent(
myUserId: string,
roomId: string,
plaintext: string,
): Promise<string | null> {
if (!plaintext) return plaintext;
if (isE2eeEnvelope(plaintext)) return plaintext;
const key = await ensureRoomAesKey(myUserId, roomId);
if (!key) return null;
return encryptText(key, plaintext);
return plaintext;
}
export async function decryptRoomContent(

View File

@@ -13,7 +13,6 @@ export const RELATIONSHIP_GOAL_VALUES = [
"not_sure",
] as const;
export type RelationshipGoal = (typeof RELATIONSHIP_GOAL_VALUES)[number];
export const RELATIONSHIP_GOAL_MAX = 2;
export const MARITAL_STATUS_NEW_VALUES = [
"widowed",
@@ -69,7 +68,6 @@ export const PERSONALITY_TRAIT_VALUES = [
"extrovert",
] as const;
export type PersonalityTrait = (typeof PERSONALITY_TRAIT_VALUES)[number];
export const PERSONALITY_TRAITS_MAX = 8;
export const LIFESTYLE_VALUES = [
"active_healthy",
@@ -83,7 +81,6 @@ export const LIFESTYLE_VALUES = [
"luxury_lover",
] as const;
export type Lifestyle = (typeof LIFESTYLE_VALUES)[number];
export const LIFESTYLE_MAX = 5;
export const INTEREST_VALUES = [
"movies_series",
@@ -102,7 +99,6 @@ export const INTEREST_VALUES = [
"language_learning",
] as const;
export type Interest = (typeof INTEREST_VALUES)[number];
export const INTERESTS_MAX = 8;
export const VALUE_VALUES = [
"family",
@@ -115,7 +111,6 @@ export const VALUE_VALUES = [
"excitement_new_experiences",
] as const;
export type ValueItem = (typeof VALUE_VALUES)[number];
export const VALUES_MAX = 5;
export const RELATIONSHIP_VIEW_VALUES = [
"traditional",
@@ -146,6 +141,16 @@ export const CHILDREN_STATUS_VALUES = [
] as const;
export type ChildrenStatus = (typeof CHILDREN_STATUS_VALUES)[number];
export const PERSONAL_STYLE_VALUES = [
"casual",
"classic",
"minimal",
"formal",
"street",
"romantic",
] as const;
export type PersonalStyle = (typeof PERSONAL_STYLE_VALUES)[number];
export const SMOKING_STATUS_VALUES = [
"none",
"sometimes",
@@ -213,12 +218,109 @@ export const buildGeoPreferenceOptions = (t: TranslateFn) =>
export const buildChildrenStatusOptions = (t: TranslateFn) =>
buildOptions(t, "childrenStatusOptions", CHILDREN_STATUS_VALUES);
export const buildPersonalStyleOptions = (t: TranslateFn) =>
buildOptions(t, "personalStyleOptions", PERSONAL_STYLE_VALUES);
export const buildSmokingStatusOptions = (t: TranslateFn) =>
buildOptions(t, "smokingStatusOptions", SMOKING_STATUS_VALUES);
export const buildIncomeRangeOptions = (t: TranslateFn) =>
buildOptions(t, "incomeRangeOptions", INCOME_RANGE_VALUES);
/** یک دسته‌ی بازه‌ی عددی برای دراپ‌داون‌های قد/وزن/سن در فیلتر «یافتن پارتنر»:
* value منحصربه‌فرد برای <select>، min/max برای اِعمال روی فیلتر واقعی (خالی
* یعنی نامحدود از آن سمت). گزینه‌ی ویژه‌ی "custom" (بازه‌ی انتخابی) در پایان هر
* لیست باعث می‌شود دو ورودی حداقل/حداکثر دستی نمایش داده شوند. */
export type RangeBucket = { value: string; min?: number; max?: number };
function buildRangeBucketLabel(t: TranslateFn, bucket: RangeBucket): string {
if (bucket.value === "not_important") {
return t("filters.notImportant");
}
if (bucket.value === "custom") {
return t("filters.customRange");
}
if (bucket.min !== undefined && bucket.max !== undefined) {
return t("filters.rangeValue", { min: bucket.min, max: bucket.max });
}
if (bucket.max !== undefined) {
return t("filters.underValue", { value: bucket.max + 1 });
}
if (bucket.min !== undefined) {
return t("filters.overValue", { value: bucket.min - 1 });
}
return bucket.value;
}
export function buildRangeBucketOptions(t: TranslateFn, buckets: RangeBucket[]): LabeledOption[] {
return buckets.map((bucket) => ({ value: bucket.value, label: buildRangeBucketLabel(t, bucket) }));
}
export const HEIGHT_BUCKETS: RangeBucket[] = [
{ value: "under_160", max: 159 },
{ value: "160_165", min: 160, max: 165 },
{ value: "165_170", min: 165, max: 170 },
{ value: "170_175", min: 170, max: 175 },
{ value: "175_180", min: 175, max: 180 },
{ value: "180_185", min: 180, max: 185 },
{ value: "185_190", min: 185, max: 190 },
{ value: "190_195", min: 190, max: 195 },
{ value: "over_195", min: 196 },
{ value: "not_important" },
{ value: "custom" },
];
export const WEIGHT_BUCKETS: RangeBucket[] = [
{ value: "40_45", min: 40, max: 45 },
{ value: "45_50", min: 45, max: 50 },
{ value: "50_55", min: 50, max: 55 },
{ value: "55_60", min: 55, max: 60 },
{ value: "60_65", min: 60, max: 65 },
{ value: "65_70", min: 65, max: 70 },
{ value: "70_75", min: 70, max: 75 },
{ value: "75_80", min: 75, max: 80 },
{ value: "80_85", min: 80, max: 85 },
{ value: "85_90", min: 85, max: 90 },
{ value: "90_95", min: 90, max: 95 },
{ value: "over_95", min: 96 },
{ value: "not_important" },
{ value: "custom" },
];
export const AGE_BUCKETS: RangeBucket[] = [
{ value: "18_24", min: 18, max: 24 },
{ value: "25_30", min: 25, max: 30 },
{ value: "31_35", min: 31, max: 35 },
{ value: "36_40", min: 36, max: 40 },
{ value: "41_50", min: 41, max: 50 },
{ value: "over_50", min: 51 },
{ value: "not_important" },
{ value: "custom" },
];
/** سایز، برخلاف قد/وزن/سن، بازه نیست — هر گزینه یک مقدار دقیق است (min=max)،
* عیناً سایزبندی لباس/کفشِ استفاده‌شده در این اپ. */
export const SIZE_BUCKETS: RangeBucket[] = [
34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70,
].map((n) => ({ value: String(n), min: n, max: n }));
export const SIZE_BUCKETS_WITH_EXTRAS: RangeBucket[] = [
...SIZE_BUCKETS,
{ value: "not_important" },
{ value: "custom" },
];
export function buildSizeBucketOptions(t: TranslateFn): LabeledOption[] {
return SIZE_BUCKETS_WITH_EXTRAS.map((bucket) => ({
value: bucket.value,
label:
bucket.value === "not_important"
? t("filters.notImportant")
: bucket.value === "custom"
? t("filters.customRange")
: bucket.value,
}));
}
/** برای درج n آیتم آرایه‌ای در URLSearchParams طبق قرارداد بک‌اند (مقدار جدا شده با کاما). */
export function joinFilterValues(values: string[] | undefined | null): string | undefined {
if (!values || values.length === 0) return undefined;

View File

@@ -8,10 +8,6 @@ export const PWA_THEME_COLOR = "#FF107D";
export const PWA_BACKGROUND_COLOR = "#FF107D";
export const PWA_ID = "modstagram-pwa";
export const PWA_VERSION = "12";
// نسخه‌ی فعلیِ APK اندروید — با هر انتشار نسخه‌ی جدید (و آپلود APK جدید توی
// storage/downloads/modstagram.apk) این مقدار هم دستی به‌روز بشه؛ همون versionName
// توی android/app/build.gradle
export const ANDROID_APP_VERSION = "2.1.2";
export { resolvePwaLang, toPwaLang } from "@/lib/i18n/pwaLang";