e2ee
This commit is contained in:
@@ -23,6 +23,7 @@ function FailedProject() {
|
||||
const [project, setProject] = useState<Project>();
|
||||
const [typeList, setTypeList] = useState<IProjectType[] | null>(null);
|
||||
const [price, setPrice] = useState("");
|
||||
const [paying, setPaying] = useState(false);
|
||||
|
||||
const getDisplayTypeLabel = (projectType?: string) => {
|
||||
if (projectType === "normal") {
|
||||
@@ -68,6 +69,9 @@ function FailedProject() {
|
||||
}, [project, typeList]);
|
||||
|
||||
const payHandler = async () => {
|
||||
// قبلاً هیچ گاردی نداشت — دابلکلیک دو درخواستِ initiate-payment-web همزمان میفرستاد
|
||||
if (paying) return;
|
||||
setPaying(true);
|
||||
try {
|
||||
const response = await request<{ authority?: string; type?: string }>(
|
||||
"POST",
|
||||
@@ -88,6 +92,7 @@ function FailedProject() {
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
setPaying(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -122,6 +127,7 @@ function FailedProject() {
|
||||
onClick={() => {
|
||||
payHandler();
|
||||
}}
|
||||
disabled={paying}
|
||||
variant="primary"
|
||||
className="w-32 h-9"
|
||||
>
|
||||
|
||||
@@ -23,6 +23,7 @@ function FailedProject() {
|
||||
const [project, setProject] = useState<Project>();
|
||||
const [typeList, setTypeList] = useState<IProjectType[] | null>(null);
|
||||
const [price, setPrice] = useState("");
|
||||
const [paying, setPaying] = useState(false);
|
||||
|
||||
const getDisplayTypeLabel = (projectType?: string) => {
|
||||
if (projectType === "normal" || projectType === "free") {
|
||||
@@ -68,6 +69,9 @@ function FailedProject() {
|
||||
}, [project, typeList]);
|
||||
|
||||
const payHandler = async () => {
|
||||
// قبلاً هیچ گاردی نداشت — دابلکلیک دو درخواستِ initiate-payment-web همزمان میفرستاد
|
||||
if (paying) return;
|
||||
setPaying(true);
|
||||
try {
|
||||
const response = await request<{
|
||||
authority?: string;
|
||||
@@ -88,12 +92,14 @@ function FailedProject() {
|
||||
window.location.href = `https://www.zarinpal.com/pg/StartPay/${response.authority}`;
|
||||
} else {
|
||||
toast.error(response?.message || t("projects.payment.initFailed"));
|
||||
setPaying(false);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const axiosErr = error as { response?: { data?: { message?: string } } };
|
||||
toast.error(
|
||||
axiosErr?.response?.data?.message || t("projects.payment.initFailed")
|
||||
);
|
||||
setPaying(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,6 +132,7 @@ function FailedProject() {
|
||||
</RoundedDiv>
|
||||
<RoundedButton
|
||||
onClick={payHandler}
|
||||
disabled={paying}
|
||||
variant="primary"
|
||||
className="h-9 w-32"
|
||||
>
|
||||
|
||||
@@ -26,6 +26,7 @@ function BillboardPayment({ params }: IBillboardProps) {
|
||||
const [advertising, setAdvertising] = useState<IAdvertising>();
|
||||
const [typeList, setTypeList] = useState<IAdvertisingType[] | null>(null);
|
||||
const [price, setPrice] = useState("");
|
||||
const [paying, setPaying] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAd = async () => {
|
||||
@@ -50,6 +51,9 @@ function BillboardPayment({ params }: IBillboardProps) {
|
||||
}, [id, request]);
|
||||
|
||||
const payHandler = async () => {
|
||||
// قبلاً هیچ گاردی نداشت — دابلکلیک دو درخواستِ initiate-payment-web همزمان میفرستاد
|
||||
if (paying) return;
|
||||
setPaying(true);
|
||||
try {
|
||||
const response = await request<{ authority?: string }>(
|
||||
"POST",
|
||||
@@ -72,6 +76,7 @@ function BillboardPayment({ params }: IBillboardProps) {
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
setPaying(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,6 +108,7 @@ function BillboardPayment({ params }: IBillboardProps) {
|
||||
<RoundedButton
|
||||
variant="primary"
|
||||
onClick={payHandler}
|
||||
disabled={paying}
|
||||
className="h-9 w-32"
|
||||
>
|
||||
{t("billboards.payment.title")}
|
||||
|
||||
@@ -27,6 +27,7 @@ function FailedBillboard() {
|
||||
const price = searchParams.get("price") || "0";
|
||||
|
||||
const [advertising, setAdvertising] = useState<IAdvertising>();
|
||||
const [paying, setPaying] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAd = async () => {
|
||||
@@ -39,7 +40,11 @@ function FailedBillboard() {
|
||||
fetchAd();
|
||||
}, [projectId, request]);
|
||||
|
||||
// قبلاً هیچکدومِ این دو تابع گاردِ submitting نداشتن — دابلکلیکِ «تلاشِ دوباره»
|
||||
// دو درخواستِ پرداختِ همزمان میفرستاد
|
||||
const payHandler = async () => {
|
||||
if (paying) return;
|
||||
setPaying(true);
|
||||
try {
|
||||
const response = await request<{ authority?: string }>(
|
||||
"POST",
|
||||
@@ -62,10 +67,13 @@ function FailedBillboard() {
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
setPaying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const republishHandler = async () => {
|
||||
if (paying) return;
|
||||
setPaying(true);
|
||||
try {
|
||||
const response = await request<{ authority?: string }>(
|
||||
"POST",
|
||||
@@ -88,6 +96,7 @@ function FailedBillboard() {
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
setPaying(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,6 +135,7 @@ function FailedBillboard() {
|
||||
payHandler();
|
||||
}
|
||||
}}
|
||||
disabled={paying}
|
||||
variant="primary"
|
||||
className="w-32 h-9"
|
||||
>
|
||||
|
||||
@@ -641,11 +641,17 @@ select {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* env(safe-area-inset-bottom) روی اندروید (برخلافِ iOS) تقریباً همیشه 0px
|
||||
برمیگرده، حتی وقتی نوار جستوجوی سیستم/نشانگرِ ژستِ اندروید همون چند پیکسلِ
|
||||
پایینِ صفحه رو اشغال کرده — یعنی قبلاً تکیهی تنها به env() باعث میشد دکمهی
|
||||
ارسال/ویس روی خیلی از گوشیهای اندروید دقیقاً روی لبهی پایینیِ صفحه بشینه و
|
||||
زیرِ نوارِ سیستم بره. max() یه حداقلِ ثابت (که این نوارها رو پوشش میده) رو
|
||||
تضمین میکنه، مستقل از اینکه env() چی برگردونه */
|
||||
.chat-input-area {
|
||||
padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
|
||||
padding-bottom: max(1.5rem, calc(0.75rem + env(safe-area-inset-bottom, 0px)));
|
||||
}
|
||||
.chat-input-area--thread {
|
||||
padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
|
||||
padding-bottom: max(1.5rem, calc(1rem + env(safe-area-inset-bottom, 0px)));
|
||||
}
|
||||
|
||||
.ig-dm-send-btn {
|
||||
|
||||
@@ -363,14 +363,23 @@ function TicketChat({ params }: ITicketChatProps) {
|
||||
const isFileUpload = fileForUpload && fileType !== "location";
|
||||
|
||||
try {
|
||||
await ensureIdentityKeys(currentUserId);
|
||||
const encrypted = await encryptDmContent(
|
||||
currentUserId,
|
||||
targetReceiverId,
|
||||
textContent
|
||||
);
|
||||
if (!encrypted) throw new Error("E2EE encryption failed");
|
||||
const sendContent = encrypted;
|
||||
// ارسالِ فایل/عکس/ویس/ویدیو فعلاً بدونِ e2ee میره (طبقِ تصمیمِ فعلی، رمزنگاریِ
|
||||
// مدیا بعداً اضافه میشه) — قبلاً حتی برای همین آپلودها هم این مرحله اجباری بود
|
||||
// و اگه encryptDmContent به هر دلیلی (مثلاً هنوز کلیدِ عمومیِ طرفِ مقابل واکشی
|
||||
// نشده) null برمیگردوند، کلِ ارسالِ فایل هم با «E2EE encryption failed» شکست
|
||||
// میخورد — دقیقاً مثلِ اندروید که از اول اصلاً این مرحله رو برای فایل صدا نمیزد
|
||||
const sendContent = isFileUpload
|
||||
? textContent
|
||||
: await (async () => {
|
||||
await ensureIdentityKeys(currentUserId);
|
||||
const encrypted = await encryptDmContent(
|
||||
currentUserId,
|
||||
targetReceiverId,
|
||||
textContent
|
||||
);
|
||||
if (!encrypted) throw new Error("E2EE encryption failed");
|
||||
return encrypted;
|
||||
})();
|
||||
|
||||
let messagePayload: Record<string, unknown> = {
|
||||
content: sendContent,
|
||||
|
||||
@@ -622,10 +622,17 @@ export default function ChatRoomThreadPage({
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureIdentityKeys(senderId);
|
||||
const encrypted = await encryptRoomContent(senderId, roomId, text);
|
||||
if (!encrypted) throw new Error("E2EE encryption failed");
|
||||
const sendContent = encrypted;
|
||||
// ارسالِ فایل/عکس/ویس/ویدیو توی روم هم فعلاً بدونِ e2ee میره — عینِ دلیلِ همین
|
||||
// تغییر توی چتِ مستقیم (chats/[username]/[id]/page.tsx): اگه encryptRoomContent
|
||||
// به هر دلیلی null برگردونه، قبلاً کلِ آپلودِ فایل هم با همین خطا شکست میخورد
|
||||
const sendContent = fileForUpload
|
||||
? text
|
||||
: await (async () => {
|
||||
await ensureIdentityKeys(senderId);
|
||||
const encrypted = await encryptRoomContent(senderId, roomId, text);
|
||||
if (!encrypted) throw new Error("E2EE encryption failed");
|
||||
return encrypted;
|
||||
})();
|
||||
|
||||
let res: { message?: RoomMessage } | null = null;
|
||||
if (fileForUpload) {
|
||||
|
||||
@@ -29,8 +29,12 @@ export default function Financial() {
|
||||
limit: 15,
|
||||
});
|
||||
|
||||
// قبلاً flatMap روی خودِ page.financial (یه آبجکت {docs, totalPages}، نه آرایه) اجرا
|
||||
// میشد، پس هر صفحه فقط به یه آیتم تبدیل میشد و allTransactions[0]?.docs همیشه فقط
|
||||
// صفحهی اول رو میداد — اسکرول به پایین صفحههای بعدی رو دانلود میکرد ولی هیچوقت
|
||||
// نمایش نمیداد
|
||||
const allTransactions =
|
||||
data?.pages.flatMap((page) => page.financial || []) ?? [];
|
||||
data?.pages.flatMap((page) => page.financial?.docs ?? []) ?? [];
|
||||
|
||||
const getTransactionType = (type: string) => {
|
||||
const known = [
|
||||
@@ -98,7 +102,7 @@ export default function Financial() {
|
||||
ref={containerRef}
|
||||
className="space-y-4 max-h-[65vh] overflow-y-auto scrollbar-thin scrollbar-thumb-neutral-300 dark:scrollbar-thumb-neutral-600"
|
||||
>
|
||||
{allTransactions[0]?.docs?.map((item: IFinancial) => (
|
||||
{allTransactions.map((item: IFinancial) => (
|
||||
<div
|
||||
key={item._id}
|
||||
className="p-5 border rounded-3xl border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 shadow-sm hover:shadow-md transition-all duration-200"
|
||||
|
||||
@@ -200,11 +200,11 @@ const Step5 = () => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && formik.values.selectedType) {
|
||||
formik.submitForm();
|
||||
}
|
||||
}, [isEditing, formik.values.selectedType]);
|
||||
// این افکت قبلاً با هر تغییرِ selectedType (یعنی هر کلیکِ کاربر روی یه کارتِ نوعِ
|
||||
// دیگه، توی حالتِ ویرایش) دوباره اجرا میشد و formik.submitForm() رو صدا میزد —
|
||||
// یعنی هر کلیک یه PATCH واقعی به /advertising/edit میفرستاد، نه فقط کلیکِ دکمهی
|
||||
// ثبتِ واقعی. حذف شد — کاربر توی حالتِ ویرایش هم باید مثلِ حالتِ عادی، دکمهی
|
||||
// ارسال/ثبتِ پایینِ همین فرم رو بزنه
|
||||
|
||||
return (
|
||||
<form
|
||||
|
||||
@@ -223,7 +223,7 @@ const MessageInput = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`pointer-events-none fixed left-0 right-0 z-50 flex flex-col items-center bg-transparent px-3 sm:px-4 ${
|
||||
className={`pointer-events-none fixed left-0 right-0 z-50 flex flex-col items-center bg-transparent px-2 sm:px-3 ${
|
||||
isChatThread ? "bottom-0 chat-input-area--thread" : "bottom-[5.5rem] chat-input-area"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -183,7 +183,15 @@ const Step4 = () => {
|
||||
{formik.touched.selectedType && formik.errors.selectedType && (
|
||||
<p className="text-red-500">{formik.errors.selectedType}</p>
|
||||
)}
|
||||
<RoundedButton type="submit" variant="primary" className="mt-4 px-8 py-2">
|
||||
{/* قبلاً هیچ گاردی نبود — فرمیک isSubmitting رو خودش درست مدیریت میکنه (چون
|
||||
onSubmit یه تابعِ async هست)، فقط به دکمه وصل نشده بود؛ دابلکلیک دو
|
||||
POST /projects/create همزمان میفرستاد و دو پروژهی تکراری میساخت */}
|
||||
<RoundedButton
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
className="mt-4 px-8 py-2"
|
||||
>
|
||||
{t("projects.newProject.submitRequest")}
|
||||
</RoundedButton>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user