diff --git a/package-lock.json b/package-lock.json
index 8b884c9..12fba59 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"axios": "^1.6.7",
"buffer": "^6.0.3",
"clsx": "^2.1.0",
+ "jssha": "^3.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.0",
@@ -1437,6 +1438,26 @@
"jssha": "3.2.0"
}
},
+ "node_modules/@ton/crypto-primitives/node_modules/jssha": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz",
+ "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==",
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@ton/crypto/node_modules/jssha": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz",
+ "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==",
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/@tonconnect/isomorphic-eventsource": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@tonconnect/isomorphic-eventsource/-/isomorphic-eventsource-0.0.2.tgz",
@@ -4202,11 +4223,10 @@
}
},
"node_modules/jssha": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz",
- "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz",
+ "integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==",
"license": "BSD-3-Clause",
- "peer": true,
"engines": {
"node": "*"
}
diff --git a/src/env.d.ts b/src/env.d.ts
index 6c210c7..b252d65 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -3,6 +3,7 @@
interface ImportMetaEnv {
readonly VITE_SENTRY_DSN: string
readonly VITE_API_BASE_URL: string
+ readonly VITE_API_BASE_STORAGE_URL: string
readonly MODE: 'development' | 'production'
readonly PROD: boolean
readonly DEV: boolean
diff --git a/src/pages/root/steps/data-step/components/disclaimer-modal/index.tsx b/src/pages/root/steps/data-step/components/disclaimer-modal/index.tsx
index 9b20d66..8c7cce5 100644
--- a/src/pages/root/steps/data-step/components/disclaimer-modal/index.tsx
+++ b/src/pages/root/steps/data-step/components/disclaimer-modal/index.tsx
@@ -1,4 +1,5 @@
-import { useHapticFeedback } from "@vkruglikov/react-telegram-web-app";
+import { useHapticFeedback, useWebApp } from "@vkruglikov/react-telegram-web-app";
+import { useEffect } from "react";
import { Button } from "~/shared/ui/button";
type DisclaimerModalProps = {
@@ -9,6 +10,20 @@ export const DisclaimerModal = ({
onConfirm,
}: DisclaimerModalProps) => {
const [impactOccurred] = useHapticFeedback();
+ const WebApp = useWebApp();
+ useEffect(() => {
+ // Отключаем вертикальные свайпы при монтировании компонента
+ if (WebApp && WebApp.disableVerticalSwipes) {
+ WebApp.disableVerticalSwipes();
+ }
+
+ // Включаем вертикальные свайпы обратно при размонтировании
+ return () => {
+ if (WebApp && WebApp.enableVerticalSwipes) {
+ WebApp.enableVerticalSwipes();
+ }
+ };
+ }, []);
const handleClick = (fn: () => void) => {
impactOccurred("light");
@@ -21,40 +36,40 @@ export const DisclaimerModal = ({
"fixed left-0 top-0 z-30 flex h-full w-full items-center justify-center bg-black/80 px-[15px]"
}
>
-
+
-
- «Внимание!
-
-
- MY снимает с себя ответственность за правомерность загрузки контента пользователем.
-
-
-
- Сервис исходит из личной
-
- ответственности пользователя перед законом и третьими лицами. MY категорически не приемлет любые виды пиратства, но признает за Пользователем право принятия самостоятельных решений.
-
-
-
- Перед загрузкой контента
-
- необходимо убедиться, что первые 30 секунд контента, которые будут использоваться для превью, не содержат материалов, нарушающих возрастное ограничение 18+»
-
-
handleClick(onConfirm)}
- />
+
+
+ Внимание!
+
+
+ MY снимает с себя ответственность за правомерность загрузки контента пользователем.
+
+
+
+ Сервис исходит из личной
+
+ ответственности пользователя перед законом и третьими лицами. MY категорически не приемлет любые виды пиратства, но признает за Пользователем право принятия самостоятельных решений.
+
+
+
+ Перед загрузкой контента
+
+ необходимо убедиться, что первые 30 секунд контента, которые будут использоваться для превью, не содержат материалов, нарушающих возрастное ограничение 18+
+
+
-
-
+
handleClick(onConfirm)}
+ />
);
-};
+};
\ No newline at end of file
diff --git a/src/pages/root/steps/presubmit-step/components/error-upload-modal/index.tsx b/src/pages/root/steps/presubmit-step/components/error-upload-modal/index.tsx
new file mode 100644
index 0000000..2da2732
--- /dev/null
+++ b/src/pages/root/steps/presubmit-step/components/error-upload-modal/index.tsx
@@ -0,0 +1,75 @@
+import { useHapticFeedback, useWebApp } from "@vkruglikov/react-telegram-web-app";
+import { useEffect } from "react";
+import { Button } from "~/shared/ui/button";
+
+type ErrorUploadProps = {
+ onConfirm(): void;
+};
+
+export const ErrorUploadModal = ({
+ onConfirm,
+}: ErrorUploadProps) => {
+ const [impactOccurred] = useHapticFeedback();
+ const WebApp = useWebApp();
+ useEffect(() => {
+ // Отключаем вертикальные свайпы при монтировании компонента
+ if (WebApp && WebApp.disableVerticalSwipes) {
+ WebApp.disableVerticalSwipes();
+ }
+
+ // Включаем вертикальные свайпы обратно при размонтировании
+ return () => {
+ if (WebApp && WebApp.enableVerticalSwipes) {
+ WebApp.enableVerticalSwipes();
+ }
+ };
+ }, []);
+
+ const handleClick = (fn: () => void) => {
+ impactOccurred("light");
+ fn();
+ };
+
+ return (
+
+
+
+
+
+ Внимание!
+
+
+ Произошла ошибка при загрузке видео.
+
+
+
+ Загрузка не завершена
+
+ из-за технических проблем или превышения допустимых ограничений. Вы можете попробовать загрузить файл ещё раз или выбрать другой файл.
+
+
+
+ Если проблема повторяется
+
+ обратитесь в техническую поддержку сервиса MY, предоставив подробную информацию о загружаемом файле и возникшей ошибке.
+
+
+
+
handleClick(onConfirm)}
+ />
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/root/steps/presubmit-step/index.tsx b/src/pages/root/steps/presubmit-step/index.tsx
index c9d986c..3c43b0b 100644
--- a/src/pages/root/steps/presubmit-step/index.tsx
+++ b/src/pages/root/steps/presubmit-step/index.tsx
@@ -2,7 +2,7 @@ import {
useHapticFeedback,
useWebApp,
} from "@vkruglikov/react-telegram-web-app";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import ReactPlayer from "react-player/lazy";
import { Button } from "~/shared/ui/button";
@@ -13,6 +13,7 @@ import { Progress } from "~/shared/ui/progress";
import { useCreateNewContent } from "~/shared/services/content";
import { BackButton } from "~/shared/ui/back-button";
import { useTonConnectUI } from "@tonconnect/ui-react";
+import { ErrorUploadModal } from "./components/error-upload-modal";
@@ -35,6 +36,20 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
const createContent = useCreateNewContent();
+ const [isErrorUploadModal, setIsErrorUploadModal] = useState(false);
+
+ const handleErrorUploadModal = () => {
+ setIsErrorUploadModal(false);
+ uploadFile.resetUploadError();
+ uploadCover.resetUploadError();
+ };
+
+ useEffect(() => {
+ if (uploadFile.uploadError || uploadCover.uploadError) {
+ setIsErrorUploadModal(true);
+ }
+ }, [uploadFile.uploadError, uploadCover.uploadError]);
+
const handleSubmit = async () => {
try {
let coverUploadResult = { content_id_v1: "" };
@@ -119,6 +134,11 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
return (
+ {isErrorUploadModal && (
+ {
+ handleErrorUploadModal()}}
+ />)}
diff --git a/src/pages/view-content/components/congrats-modal/index.tsx b/src/pages/view-content/components/congrats-modal/index.tsx
index ee428a5..2f10901 100644
--- a/src/pages/view-content/components/congrats-modal/index.tsx
+++ b/src/pages/view-content/components/congrats-modal/index.tsx
@@ -1,4 +1,5 @@
-import { useHapticFeedback } from "@vkruglikov/react-telegram-web-app";
+import { useHapticFeedback, useWebApp } from "@vkruglikov/react-telegram-web-app";
+import { useEffect } from "react";
import { Button } from "~/shared/ui/button";
type CongratsModalProps = {
@@ -9,25 +10,38 @@ export const CongratsModal = ({
onConfirm,
}: CongratsModalProps) => {
const [impactOccurred] = useHapticFeedback();
+ const WebApp = useWebApp();
const handleClick = (fn: () => void) => {
impactOccurred("light");
fn();
};
-
+ useEffect(() => {
+ // Отключаем вертикальные свайпы при монтировании компонента
+ if (WebApp && WebApp.disableVerticalSwipes) {
+ WebApp.disableVerticalSwipes();
+ }
+
+ // Включаем вертикальные свайпы обратно при размонтировании
+ return () => {
+ if (WebApp && WebApp.enableVerticalSwipes) {
+ WebApp.enableVerticalSwipes();
+ }
+ };
+ }, []);
return (
-
-
-
+
+
+
🎉
Поздравляем с покупкой!
🎉
@@ -44,13 +58,13 @@ export const CongratsModal = ({
Спасибо, что выбираете MY!
-
handleClick(onConfirm)}
- />
+
handleClick(onConfirm)}
+ />
);
diff --git a/src/pages/view-content/components/error-modal/index.tsx b/src/pages/view-content/components/error-modal/index.tsx
index 0837717..85f300d 100644
--- a/src/pages/view-content/components/error-modal/index.tsx
+++ b/src/pages/view-content/components/error-modal/index.tsx
@@ -1,4 +1,5 @@
-import { useHapticFeedback } from "@vkruglikov/react-telegram-web-app";
+import { useHapticFeedback, useWebApp } from "@vkruglikov/react-telegram-web-app";
+import { useEffect } from "react";
import { Button } from "~/shared/ui/button";
type ErrorModalProps = {
@@ -9,25 +10,39 @@ export const ErrorModal = ({
onConfirm,
}: ErrorModalProps) => {
const [impactOccurred] = useHapticFeedback();
+ const WebApp = useWebApp();
const handleClick = (fn: () => void) => {
impactOccurred("light");
fn();
};
+ useEffect(() => {
+ // Отключаем вертикальные свайпы при монтировании компонента
+ if (WebApp && WebApp.disableVerticalSwipes) {
+ WebApp.disableVerticalSwipes();
+ }
+
+ // Включаем вертикальные свайпы обратно при размонтировании
+ return () => {
+ if (WebApp && WebApp.enableVerticalSwipes) {
+ WebApp.enableVerticalSwipes();
+ }
+ };
+ }, []);
return (
+
+
-
-
-
+
Ошибка запроса транзакции
@@ -41,13 +56,13 @@ export const ErrorModal = ({
Если проблема не исчезает, убедитесь, что ваш кошелек работает корректно, или свяжитесь с поддержкой.
-
handleClick(onConfirm)}
- />
+
handleClick(onConfirm)}
+ />
);
diff --git a/src/shared/services/file/index.ts b/src/shared/services/file/index.ts
index 03a830d..60bd57e 100644
--- a/src/shared/services/file/index.ts
+++ b/src/shared/services/file/index.ts
@@ -1,46 +1,201 @@
import { useMutation } from "react-query";
import { useState } from "react";
-
import { request } from "~/shared/libs";
+const STORAGE_API_URL = import.meta.env.VITE_API_BASE_STORAGE_URL;
+const MAX_CHUNK_SIZE = 80 * 1024 * 1024; // 80 MB
+
export const useUploadFile = () => {
const [uploadProgress, setUploadProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);
+ const [uploadError, setUploadError] = useState
(null);
- const mutation = useMutation(["upload-file"], (file: File) => {
+ const mutation = useMutation(["upload-file"], async (file: File) => {
+ console.log(`Начинаем загрузку файла: ${file.name} (${file.size} байт)`);
setIsUploading(true);
+ setUploadProgress(0);
+ setUploadError(null); // Сбрасываем ошибку перед началом новой загрузки
- const formData = new FormData();
- formData.append("file", file);
-
- return request
- .post<{
- content_sha256: string;
- content_id_v1: string;
- content_url: string;
- }>("/storage", formData, {
- headers: {
- "Content-Type": "multipart/form-data",
- Authorization: localStorage.getItem('auth_v1_token') || ""
- },
-
- onUploadProgress: (progressEvent) => {
- const percentCompleted = Math.round(
- (progressEvent.loaded * 100) / (progressEvent?.total as number) ||
- 0,
- );
- setUploadProgress(percentCompleted);
- },
- })
- .then((response) => {
- setIsUploading(false);
- return response.data;
- })
- .catch((error) => {
- setIsUploading(false);
- throw error;
- });
+ try {
+ // Для маленьких файлов используем обычную загрузку, но с теми же заголовками
+ if (file.size <= MAX_CHUNK_SIZE) {
+ console.log("Используем обычную загрузку (файл <= MAX_CHUNK_SIZE)");
+
+ // Подготавливаем заголовки - такие же, как для чанковой загрузки
+ const headers: Record = {
+ "X-File-Name": btoa(unescape(encodeURIComponent(file.name))), // Имя файла в base64
+ "X-Chunk-Start": "0", // Начинаем с позиции 0
+ "Content-Type": file.type || "application/octet-stream",
+ "X-Last-Chunk": "1" // Это единственный и последний чанк
+ };
+
+ // Добавляем заголовок авторизации
+ const authToken = localStorage.getItem('auth_v1_token');
+ if (authToken) {
+ headers["Authorization"] = authToken;
+ }
+
+ console.log("Заголовки запроса:", headers);
+
+ try {
+ const response = await request.post<{
+ upload_id?: string;
+ content_sha256?: string;
+ content_id?: string;
+ content_id_v1?: string;
+ content_url?: string;
+ }>("", file, { // Отправляем файл напрямую вместо FormData
+ baseURL: STORAGE_API_URL,
+ headers,
+ onUploadProgress: (progressEvent) => {
+ const percentCompleted = Math.round(
+ (progressEvent.loaded * 100) / (progressEvent?.total as number || file.size)
+ );
+ setUploadProgress(Math.min(99, percentCompleted));
+ },
+ });
+
+ console.log("Ответ на обычную загрузку:", response.data);
+ setUploadProgress(100);
+
+ return {
+ content_sha256: response.data.content_sha256 || "",
+ content_id_v1: response.data.content_id_v1 || response.data.content_id || "",
+ content_url: response.data.content_url || ""
+ };
+ } catch (error) {
+ console.error("Ошибка при обычной загрузке:", error);
+ setUploadError(error instanceof Error ? error : new Error('Ошибка при загрузке файла'));
+ throw error;
+ }
+ }
+
+ // Для больших файлов используем чанковую загрузку
+ console.log("Используем чанкированную загрузку (файл > MAX_CHUNK_SIZE)");
+
+ let offset = 0;
+ let uploadId: string | null = null;
+ let chunkNumber = 0;
+
+ // Загружаем файл по чанкам
+ while (offset < file.size) {
+ chunkNumber++;
+ const chunkEnd = Math.min(offset + MAX_CHUNK_SIZE, file.size);
+ const chunk = file.slice(offset, chunkEnd);
+ console.log(`Загрузка чанка #${chunkNumber} начиная с байта ${offset}`);
+
+ // Определяем, является ли текущий чанк последним
+ const isLastChunk = chunkEnd === file.size;
+
+ // Подготавливаем заголовки
+ const headers: Record = {
+ "X-File-Name": btoa(unescape(encodeURIComponent(file.name))), // Имя файла в base64
+ "X-Chunk-Start": offset.toString(),
+ "Content-Type": file.type || "application/octet-stream"
+ };
+
+ // Добавляем маркер последнего чанка, если это последний чанк
+ if (isLastChunk) {
+ headers["X-Last-Chunk"] = "1";
+ }
+
+ // Добавляем заголовок авторизации
+ const authToken = localStorage.getItem('auth_v1_token');
+ if (authToken) {
+ headers["Authorization"] = authToken;
+ }
+
+ // Если есть uploadId, добавляем его в заголовки
+ if (uploadId) {
+ headers["X-Upload-ID"] = uploadId;
+ }
+
+ console.log("Заголовки запроса:", headers);
+
+ try {
+ const response = await request.post<{
+ upload_id?: string;
+ current_size?: number;
+ content_id?: string;
+ content_sha256?: string;
+ content_id_v1?: string;
+ content_url?: string;
+ }>("", chunk, {
+ baseURL: STORAGE_API_URL,
+ headers,
+ onUploadProgress: (progressEvent) => {
+ // Прогресс загрузки текущего чанка
+ const overallProgress = offset + progressEvent.loaded;
+ const percentCompleted = Math.round((overallProgress / file.size) * 100);
+ setUploadProgress(Math.min(99, percentCompleted));
+ },
+ });
+
+ console.log(`Ответ на чанк #${chunkNumber}:`, response.data);
+
+ // Сохраняем uploadId из первого ответа, если не установлен
+ if (!uploadId && response.data.upload_id) {
+ uploadId = response.data.upload_id;
+ console.log("Получен upload_id:", uploadId);
+ }
+
+ // Проверка на наличие content_id
+ if (response.data.content_id) {
+ console.log("Загрузка завершена. ID файла:", response.data.content_id);
+ setUploadProgress(100);
+
+ return {
+ content_sha256: response.data.content_sha256 || "",
+ content_id_v1: response.data.content_id_v1 || response.data.content_id || "",
+ content_url: response.data.content_url || ""
+ };
+ }
+
+ // Обновляем смещение на основе ответа сервера
+ if (response.data.current_size !== undefined) {
+ offset = response.data.current_size;
+ console.log(`Сервер сообщает current_size: ${offset}`);
+ } else {
+ console.warn("Неожиданный ответ от сервера, отсутствует current_size");
+ const error = new Error("Missing current_size in response");
+ setUploadError(error);
+ throw error;
+ }
+ } catch (error: any) {
+ console.error(`Ошибка при загрузке чанка #${chunkNumber}:`, error);
+ if (error.response) {
+ console.error("Ответ сервера:", error.response.status, error.response.data);
+ }
+ setUploadError(error instanceof Error ? error : new Error(`Ошибка при загрузке чанка #${chunkNumber}`));
+ throw error;
+ }
+ }
+
+ const error = new Error("Ошибка загрузки файла: все чанки загружены, но content_id не получен");
+ console.error(error.message);
+ setUploadError(error);
+ throw error;
+ } catch (error: any) {
+ console.error("Ошибка при загрузке:", error);
+ if (error.response) {
+ console.error("Ответ сервера:", error.response.status, error.response.data);
+ }
+ setUploadError(error instanceof Error ? error : new Error("Неизвестная ошибка при загрузке"));
+ setIsUploading(false);
+ throw error;
+ } finally {
+ setIsUploading(false);
+ }
});
- return { ...mutation, uploadProgress, isUploading };
-};
+ // Сбросить ошибку
+ const resetUploadError = () => setUploadError(null);
+
+ return {
+ ...mutation,
+ uploadProgress,
+ isUploading,
+ uploadError,
+ resetUploadError
+ };
+};
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 9511a7a..ea8fb39 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2076,6 +2076,11 @@ json5@^2.2.3:
resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+jssha@^3.3.1:
+ version "3.3.1"
+ resolved "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz"
+ integrity sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==
+
jssha@3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz"