diff --git a/.github/workflows/build-and-publish-preview.yaml b/.github/workflows/build-and-publish-preview.yaml new file mode 100644 index 000000000..701f00a0e --- /dev/null +++ b/.github/workflows/build-and-publish-preview.yaml @@ -0,0 +1,48 @@ +name: Build & Publish Stable Preview +on: + push: + branches: ["master"] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: echo "registry=https://registry.yarnpkg.com/" > .npmrc + - run: echo "@internxt:registry=https://npm.pkg.github.com" >> .npmrc + # You cannot read packages from other private repos with GITHUB_TOKEN + # You have to use a PAT instead https://github.com/actions/setup-node/issues/49 + - run: echo //npm.pkg.github.com/:_authToken=${{ secrets.PERSONAL_ACCESS_TOKEN }} >> .npmrc + - run: echo "always-auth=true" >> .npmrc + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Build and push to ${{ github.event.repository.name }}-dev + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./infrastructure/preview.Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}-dev:${{ github.sha }} + dispatch_update_preview_image: + needs: build + runs-on: ubuntu-latest + steps: + - name: Dispatch Update Preview Image Command + uses: myrotvorets/trigger-repository-dispatch-action@1.0.0 + with: + token: ${{ secrets.PAT }} + repo: internxt/environments + type: update-preview-image-command + payload: | + { + "image": { + "name": "${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}", + "newName": "${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}-dev", + "newTag": "${{ github.sha }}" + } + } \ No newline at end of file diff --git a/.github/workflows/clean-up-pr-preview.yaml b/.github/workflows/clean-up-pr-preview.yaml new file mode 100644 index 000000000..815cbf1ba --- /dev/null +++ b/.github/workflows/clean-up-pr-preview.yaml @@ -0,0 +1,27 @@ +name: Clean Up PR Preview +on: + pull_request: + types: [closed] +jobs: + dispatch_cleanup_deployment: + runs-on: ubuntu-latest + steps: + - name: Dispatch Cleanup Preview Repository Command + uses: myrotvorets/trigger-repository-dispatch-action@1.0.0 + with: + token: ${{ secrets.PAT }} + repo: internxt/environments + type: cleanup-preview-command + payload: | + { + "github": { + "payload": { + "repository": { + "name": "${{ github.event.repository.name }}" + }, + "issue": { + "number": ${{ github.event.number }} + } + } + } + } \ No newline at end of file diff --git a/.github/workflows/deploy-pr-preview.yaml b/.github/workflows/deploy-pr-preview.yaml new file mode 100644 index 000000000..778a6854b --- /dev/null +++ b/.github/workflows/deploy-pr-preview.yaml @@ -0,0 +1,111 @@ +name: Deploy PR Preview +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check Out Repo + uses: actions/checkout@v2 + - run: echo "registry=https://registry.yarnpkg.com/" > .npmrc + - run: echo "@internxt:registry=https://npm.pkg.github.com" >> .npmrc + # You cannot read packages from other private repos with GITHUB_TOKEN + # You have to use a PAT instead https://github.com/actions/setup-node/issues/49 + - run: echo //npm.pkg.github.com/:_authToken=${{ secrets.PERSONAL_ACCESS_TOKEN }} >> .npmrc + - run: echo "always-auth=true" >> .npmrc + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Build and push to ${{ github.event.repository.name }}-dev + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./infrastructure/preview.Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}-dev:preview-${{ github.event.number }}-${{ github.event.pull_request.head.sha }} + add_ready_for_preview_label: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions-ecosystem/action-add-labels@v1 + with: + labels: | + ready-for-preview + dispatch_update_deployment: + needs: add_ready_for_preview_label + runs-on: ubuntu-latest + if: ${{ contains(github.event.pull_request.labels.*.name, 'deployed') }} + steps: + - name: Dispatch Update Preview Repository Command + uses: myrotvorets/trigger-repository-dispatch-action@1.0.0 + with: + token: ${{ secrets.PAT }} + repo: internxt/environments + type: update-preview-command + payload: | + { + "github": { + "payload": { + "repository": { + "name": "${{ github.event.repository.name }}", + "full_name": "${{ github.event.repository.full_name }}" + }, + "issue": { + "number": ${{ github.event.number }}, + "labels": ${{ toJSON(github.event.pull_request.labels) }} + } + } + }, + "slash_command": { + "args": { + "named": { + "deployment": "${{ github.event.repository.name }}", + "tag": "preview-${{ github.event.number }}-${{ github.event.pull_request.head.sha }}", + "imageSuffix": "-dev" + } + } + } + } + dispatch_check_deployment: + needs: add_ready_for_preview_label + runs-on: ubuntu-latest + if: ${{ contains(github.event.pull_request.labels.*.name, 'preview') }} + steps: + - name: Dispatch Check Preview Repository Command + uses: myrotvorets/trigger-repository-dispatch-action@1.0.0 + with: + token: ${{ secrets.PAT }} + repo: internxt/environments + type: check-preview-command + payload: | + { + "github": { + "payload": { + "repository": { + "name": "${{ github.event.repository.name }}", + "full_name": "${{ github.event.repository.full_name }}", + "html_url": "${{ github.event.repository.html_url }}" + }, + "issue": { + "number": ${{ github.event.number }}, + "labels": ${{ toJSON(github.event.pull_request.labels) }}, + "pull_request": { + "html_url": "${{ github.event.pull_request.html_url }}" + } + } + } + }, + "slash_command": { + "args": { + "named": { + "notify": "true" + } + } + } + } \ No newline at end of file diff --git a/.github/workflows/slash-command-dispatcher.yaml b/.github/workflows/slash-command-dispatcher.yaml new file mode 100644 index 000000000..50120cde5 --- /dev/null +++ b/.github/workflows/slash-command-dispatcher.yaml @@ -0,0 +1,30 @@ +name: Slash Command Dispatch +on: + issue_comment: + types: [created] +jobs: + slash_command_dispatch: + runs-on: ubuntu-latest + if: ${{ contains(github.event.issue.labels.*.name, 'deployed') || contains(github.event.issue.labels.*.name, 'preview') }} + steps: + - name: Slash Command Dispatch + id: scd + uses: peter-evans/slash-command-dispatch@v4 + with: + token: ${{ secrets.PAT }} + commands: update-preview,check-preview + permission: write + repository: internxt/environments + issue-type: pull-request + allow-edits: false + reactions: false + - name: Edit comment with error message + if: steps.scd.outputs.error-message + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ github.event.comment.id }} + body: | + + > [!CAUTION] + > Couldn't dispatch your command due to error: + > **${{ steps.scd.outputs.error-message }}** \ No newline at end of file diff --git a/infrastructure/preview.Dockerfile b/infrastructure/preview.Dockerfile new file mode 100644 index 000000000..2595e8e84 --- /dev/null +++ b/infrastructure/preview.Dockerfile @@ -0,0 +1,16 @@ +FROM node:18-alpine3.19 + +RUN apk update +RUN apk add nginx git yarn + +WORKDIR /app +COPY package.json yarn.lock ./ +COPY .npmrc ./.npmrc +COPY ./scripts ./scripts/ + +RUN yarn +COPY . /app + +EXPOSE 3000 + +CMD ["yarn", "dev"] \ No newline at end of file diff --git a/package.json b/package.json index bf9d1412e..c47da9e00 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "drive-web", - "version": "v1.0.248", + "version": "v1.0.255", "private": true, "dependencies": { "@headlessui/react": "1.7.5", diff --git a/src/app/auth/views/RecoveryLinkView/RecoveryLinkView.tsx b/src/app/auth/views/RecoveryLinkView/RecoveryLinkView.tsx index a3216bfb8..0ddfc71fe 100644 --- a/src/app/auth/views/RecoveryLinkView/RecoveryLinkView.tsx +++ b/src/app/auth/views/RecoveryLinkView/RecoveryLinkView.tsx @@ -14,7 +14,7 @@ function RecoveryLinkView(): JSX.Element { -
+
{translate('general.terms')} diff --git a/src/app/auth/views/SignInView/SignInView.tsx b/src/app/auth/views/SignInView/SignInView.tsx index 04a7b5c39..115e639cb 100644 --- a/src/app/auth/views/SignInView/SignInView.tsx +++ b/src/app/auth/views/SignInView/SignInView.tsx @@ -23,7 +23,7 @@ export default function SignInView(props: SignInProps): JSX.Element {
{!props.displayIframe && ( -
+
{isRegularSignup && ( -
+
{ const [showBanner, setShowBanner] = useState(false); @@ -41,7 +41,7 @@ const BannerWrapper = (): JSX.Element => { } } - return ; + return ; }; export default BannerWrapper; diff --git a/src/app/banners/FeaturesBanner.tsx b/src/app/banners/FeaturesBanner.tsx index 782e64e40..4586f2f82 100644 --- a/src/app/banners/FeaturesBanner.tsx +++ b/src/app/banners/FeaturesBanner.tsx @@ -1,74 +1,68 @@ -import { CheckCircle, CircleWavyCheck, X } from '@phosphor-icons/react'; +import { CheckCircle, ShieldCheck, X } from '@phosphor-icons/react'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; const FeaturesBanner = ({ showBanner, onClose }: { showBanner: boolean; onClose: () => void }): JSX.Element => { - const { translate } = useTranslationContext(); + const { translate, translateList } = useTranslationContext(); - const features = [ - { - title: translate('featuresBanner.features.discount'), - }, - { - title: translate('featuresBanner.features.safeCloud'), - }, - { - title: translate('featuresBanner.features.openSource'), - }, - { - title: translate('featuresBanner.features.endToEnd'), - }, - { - title: translate('featuresBanner.features.unauthorized'), - }, - { - title: translate('featuresBanner.features.offerEnds'), - }, - ]; + const features = translateList('featuresBanner.features'); + + const handleOnClick = () => { + window.open('https://internxt.com/lifetime', '_blank', 'noopener noreferrer'); + }; return ( + //Background
+ {/* Banner */}
- -
-
-
-

{translate('featuresBanner.header')}

-

{translate('featuresBanner.title')}

+
+
+
+

{translate('featuresBanner.label')}

-
+

{translate('featuresBanner.title')}

+ +
-
- -

{translate('lifetimeBanner.guarantee')}

+
+ +

{translate('featuresBanner.guarantee')}

+ +

{translate('featuresBanner.lastCta')}

-
- {features.map((item) => ( -
- -

{item.title}

+
+
+
+ {features.map((card) => ( +
+ +

{card}

+
+ ))}
- ))} +
); }; + export default FeaturesBanner; diff --git a/src/app/drive/components/CreateFolderDialog/CreateFolderDialog.tsx b/src/app/drive/components/CreateFolderDialog/CreateFolderDialog.tsx index ba2c17cc3..a5fd7cf21 100644 --- a/src/app/drive/components/CreateFolderDialog/CreateFolderDialog.tsx +++ b/src/app/drive/components/CreateFolderDialog/CreateFolderDialog.tsx @@ -45,17 +45,18 @@ const CreateFolderDialog = ({ onFolderCreated, currentFolderId, neededFolderId } const createFolder = async () => { if (folderName && folderName.trim().length > 0) { setIsLoading(true); + const parentFolderId = currentFolderId ?? neededFolderId; await dispatch( storageThunks.createFolderThunk({ folderName, - parentFolderId: currentFolderId ? currentFolderId : neededFolderId, + parentFolderId, }), ) .unwrap() .then(() => { onFolderCreated && onFolderCreated(); - dispatch(storageActions.setHasMoreDriveFolders(true)); - dispatch(storageActions.setHasMoreDriveFiles(true)); + dispatch(storageActions.setHasMoreDriveFolders({ folderId: parentFolderId, status: true })); + dispatch(storageActions.setHasMoreDriveFiles({ folderId: parentFolderId, status: true })); setTimeout(() => { dispatch(fetchSortedFolderContentThunk(currentFolderId)); setIsLoading(false); @@ -72,7 +73,7 @@ const CreateFolderDialog = ({ onFolderCreated, currentFolderId, neededFolderId } return e; }); } else { - setError(translate('error.folderCannotBeEmpty') as string); + setError(translate('error.folderCannotBeEmpty')); } }; @@ -92,9 +93,9 @@ const CreateFolderDialog = ({ onFolderCreated, currentFolderId, neededFolderId } { setFolderName(name); setError(''); diff --git a/src/app/drive/components/DriveExplorer/DriveExplorer.tsx b/src/app/drive/components/DriveExplorer/DriveExplorer.tsx index 35ea14a9f..c175ccf90 100644 --- a/src/app/drive/components/DriveExplorer/DriveExplorer.tsx +++ b/src/app/drive/components/DriveExplorer/DriveExplorer.tsx @@ -19,6 +19,7 @@ import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { useHotkeys } from 'react-hotkeys-hook'; import moveItemsToTrash from 'use_cases/trash/move-items-to-trash'; import { getTrashPaginated } from '../../../../use_cases/trash/get_trash'; +import BannerWrapper from '../../../banners/BannerWrapper'; import deviceService from '../../../core/services/device.service'; import errorService from '../../../core/services/error.service'; import localStorageService, { STORAGE_KEYS } from '../../../core/services/local-storage.service'; @@ -67,7 +68,6 @@ import WarningMessageWrapper from '../WarningMessage/WarningMessageWrapper'; import './DriveExplorer.scss'; import { DriveTopBarItems } from './DriveTopBarItems'; import DriveTopBarActions from './components/DriveTopBarActions'; -import BannerWrapper from '../../../banners/BannerWrapper'; const TRASH_PAGINATION_OFFSET = 50; const UPLOAD_ITEMS_LIMIT = 1000; @@ -155,6 +155,7 @@ const DriveExplorer = (props: DriveExplorerProps): JSX.Element => { const [hasMoreItems, setHasMoreItems] = useState(true); const [hasMoreTrashFolders, setHasMoreTrashFolders] = useState(true); const [isLoadingTrashItems, setIsLoadingTrashItems] = useState(false); + const hasMoreItemsToLoad = isTrash ? hasMoreItems : hasMoreFiles || hasMoreFolders; // RIGHT CLICK MENU STATES const [isListElementsHovered, setIsListElementsHovered] = useState(false); @@ -168,11 +169,12 @@ const DriveExplorer = (props: DriveExplorerProps): JSX.Element => { // ONBOARDING TUTORIAL STATES const [currentTutorialStep, setCurrentTutorialStep] = useState(0); const [showSecondTutorialStep, setShowSecondTutorialStep] = useState(false); - const stepOneTutorialRef = useRef(null); + const uploadFileButtonRef = useRef(null); const isSignUpTutorialCompleted = localStorageService.hasCompletedTutorial(user?.userId); const successNotifications = useTaskManagerGetNotifications({ status: [TaskStatus.Success], }); + const divRef = useRef(null); const showTutorial = useAppSelector(userSelectors.hasSignedToday) && @@ -186,7 +188,8 @@ const DriveExplorer = (props: DriveExplorerProps): JSX.Element => { }, 0); passToNextStep(); }, - stepOneTutorialRef, + stepOneTutorialRef: uploadFileButtonRef, + offset: hasAnyItemSelected ? { x: divRef?.current?.offsetWidth ?? 0, y: 0 } : { x: 0, y: 0 }, }, { onNextStepClicked: () => { @@ -739,7 +742,7 @@ const DriveExplorer = (props: DriveExplorerProps): JSX.Element => { }} { currentFolderId={currentFolderId} setEditNameItem={setEditNameItem} hasItems={hasItems} + driveActionsRef={divRef} />
{isTrash && ( @@ -818,7 +822,7 @@ const DriveExplorer = (props: DriveExplorerProps): JSX.Element => { items={items} isLoading={isTrash ? isLoadingTrashItems : isLoading} onEndOfScroll={fetchItems} - hasMoreItems={hasMoreItems} + hasMoreItems={hasMoreItemsToLoad} isTrash={isTrash} onHoverListItems={(areHovered) => { setIsListElementsHovered(areHovered); @@ -1042,6 +1046,8 @@ const dropTargetCollect: DropTargetCollector< export default connect((state: RootState) => { const currentFolderId: number = storageSelectors.currentFolderId(state); + const hasMoreFolders = state.storage.hasMoreDriveFolders[currentFolderId] ?? true; + const hasMoreFiles = state.storage.hasMoreDriveFiles[currentFolderId] ?? true; return { isAuthenticated: state.user.isAuthenticated, @@ -1060,7 +1066,7 @@ export default connect((state: RootState) => { planUsage: state.plan.planUsage, folderOnTrashLength: state.storage.folderOnTrashLength, filesOnTrashLength: state.storage.filesOnTrashLength, - hasMoreFolders: state.storage.hasMoreDriveFolders, - hasMoreFiles: state.storage.hasMoreDriveFiles, + hasMoreFolders, + hasMoreFiles, }; })(DropTarget([NativeTypes.FILE], dropTargetSpec, dropTargetCollect)(DriveExplorer)); diff --git a/src/app/drive/components/DriveExplorer/DriveExplorerList/DriveExplorerList.tsx b/src/app/drive/components/DriveExplorer/DriveExplorerList/DriveExplorerList.tsx index 65eb32a81..3057364d8 100644 --- a/src/app/drive/components/DriveExplorer/DriveExplorerList/DriveExplorerList.tsx +++ b/src/app/drive/components/DriveExplorer/DriveExplorerList/DriveExplorerList.tsx @@ -77,6 +77,25 @@ const createDriveListItem = (item: DriveItemData, isTrash?: boolean) => ( ); +const resetDriveOrder = ({ + dispatch, + orderType, + direction, + currentFolderId, +}: { + dispatch; + orderType: string; + direction: string; + currentFolderId: number; +}) => { + dispatch(storageActions.setDriveItemsSort(orderType)); + dispatch(storageActions.setDriveItemsOrder(direction)); + + dispatch(storageActions.setHasMoreDriveFolders({ folderId: currentFolderId, status: true })); + dispatch(storageActions.setHasMoreDriveFiles({ folderId: currentFolderId, status: true })); + dispatch(fetchSortedFolderContentThunk(currentFolderId)); +}; + const DriveExplorerList: React.FC = memo((props) => { const [isAllSelectedEnabled, setIsAllSelectedEnabled] = useState(false); const [editNameItem, setEditNameItem] = useState(null); @@ -143,21 +162,11 @@ const DriveExplorerList: React.FC = memo((props) => { dispatch(storageActions.setOrder({ by: value.field, direction })); if (value.field === 'name') { - dispatch(storageActions.setDriveItemsSort('plainName')); - dispatch(storageActions.setDriveItemsOrder(direction)); - - dispatch(storageActions.setHasMoreDriveFolders(true)); - dispatch(storageActions.setHasMoreDriveFiles(true)); - dispatch(fetchSortedFolderContentThunk(currentFolderId)); + resetDriveOrder({ dispatch, orderType: 'plainName', direction, currentFolderId }); } if (value.field === 'updatedAt') { - dispatch(storageActions.setDriveItemsSort('updatedAt')); - dispatch(storageActions.setDriveItemsOrder(direction)); - - dispatch(storageActions.setHasMoreDriveFolders(true)); - dispatch(storageActions.setHasMoreDriveFiles(true)); - dispatch(fetchSortedFolderContentThunk(currentFolderId)); + resetDriveOrder({ dispatch, orderType: 'updatedAt', direction, currentFolderId }); } }; diff --git a/src/app/drive/components/DriveExplorer/components/DriveTopBarActions.tsx b/src/app/drive/components/DriveExplorer/components/DriveTopBarActions.tsx index 4ed6c54eb..8866431b7 100644 --- a/src/app/drive/components/DriveExplorer/components/DriveTopBarActions.tsx +++ b/src/app/drive/components/DriveExplorer/components/DriveTopBarActions.tsx @@ -9,26 +9,26 @@ import { Users, } from '@phosphor-icons/react'; import { ReactComponent as MoveActionIcon } from 'assets/icons/move.svg'; +import moveItemsToTrash from 'use_cases/trash/move-items-to-trash'; +import errorService from '../../../../core/services/error.service'; +import navigationService from '../../../../core/services/navigation.service'; +import { DriveItemData, DriveItemDetails, FileViewMode } from '../../../../drive/types'; +import { useTranslationContext } from '../../../../i18n/provider/TranslationProvider'; +import shareService from '../../../../share/services/share.service'; import Button from '../../../../shared/components/Button/Button'; import Dropdown from '../../../../shared/components/Dropdown'; import TooltipElement, { DELAY_SHOW_MS } from '../../../../shared/components/Tooltip/Tooltip'; -import { useTranslationContext } from '../../../../i18n/provider/TranslationProvider'; import { useAppDispatch, useAppSelector } from '../../../../store/hooks'; import { storageActions } from '../../../../store/slices/storage'; +import storageThunks from '../../../../store/slices/storage/storage.thunks'; import { uiActions } from '../../../../store/slices/ui'; +import useDriveItemStoreProps from '../DriveExplorerItem/hooks/useDriveStoreProps'; import { contextMenuDriveFolderNotSharedLink, contextMenuDriveFolderShared, contextMenuDriveItemShared, contextMenuDriveNotSharedLink, } from '../DriveExplorerList/DriveItemContextMenu'; -import moveItemsToTrash from 'use_cases/trash/move-items-to-trash'; -import errorService from '../../../../core/services/error.service'; -import storageThunks from '../../../../store/slices/storage/storage.thunks'; -import shareService from '../../../../share/services/share.service'; -import { DriveItemData, DriveItemDetails, FileViewMode } from '../../../../drive/types'; -import useDriveItemStoreProps from '../DriveExplorerItem/hooks/useDriveStoreProps'; -import navigationService from '../../../../core/services/navigation.service'; const DriveTopBarActions = ({ selectedItems, @@ -37,6 +37,7 @@ const DriveTopBarActions = ({ hasAnyItemSelected, isTrash, hasItems, + driveActionsRef, }: { selectedItems: DriveItemData[]; currentFolderId: number; @@ -44,6 +45,7 @@ const DriveTopBarActions = ({ hasAnyItemSelected: boolean; isTrash: boolean; hasItems: boolean; + driveActionsRef?: React.MutableRefObject; }) => { const dispatch = useAppDispatch(); @@ -211,7 +213,7 @@ const DriveTopBarActions = ({ {hasItemsAndIsNotTrash && ( <> {separatorV} -
+
{selectedItems.length === 1 && ( <>
{ return { namePath: state.storage.namePath, - isLoading: state.storage.loadingFolders[currentFolderId], + isLoading: state.storage.loadingFolders[currentFolderId] ?? true, + currentFolderId, items: sortedItems, }; })(DriveView); diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index 199d16982..9378e71e9 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -4,18 +4,19 @@ "logOut": "Log-out" }, "featuresBanner": { - "header": "Spare 70%!", - "title": "Bereiten Sie sich auf die Datenschutzwoche vor", - "cta": "Plan auswählen", - "guarantee": "30-tägige Geld-zurück-Garantie", - "features": { - "discount": "Sparen Sie bei monatlichen und jährlichen Plänen", - "safeCloud": "Sicherer und geschützter Cloud-Speicher", - "openSource": "Open Source und transparent", - "endToEnd": "Ende-zu-Ende-verschlüsselte Übertragungen", - "unauthorized": "Kein unberechtigter Zugriff", - "offerEnds": "Angebot endet am 29. Februar" - } + "label": "Angebot zum Welttag der Cloud-Sicherheit", + "title": "Ein Leben lang Privatsphäre für 75% weniger", + "cta": "Wähle einen Plan", + "guarantee": "30 Tage Geld-zurück-Garantie", + "lastCta": "*Angebot gilt für kostenlose Konten oder neue Kunden", + "features": [ + "75% RABATT auf alle lebenslangen Pläne", + "Sicherer Cloud-Speicher", + "Backup privater Dateien und Fotos", + "End-to-end verschlüsselte Übertragungen", + "Kein unbefugter Zugriff", + "Angebot endet am 8. April" + ] }, "valentinesBanner": { "title": "Liebe ohne grenzen!", diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index 415a6a223..a5b684c08 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -4,19 +4,19 @@ "logOut": "Log Out" }, "featuresBanner": { - "header": "Save 70%!", - "title": "Get ready for Data Privacy Week", + "label": "World Cloud Security Day Offer", + "title": "A lifetime of privacy for 75% less", "cta": "Choose plan", "guarantee": "30-day money-back guarantee", "lastCta": "*Offer is for free accounts or new customers", - "features": { - "discount": "Save on monthly & annual plans", - "safeCloud": "Safe and secure cloud storage", - "openSource": "Open source and transparent", - "endToEnd": "End-to-end encrypted transfers", - "unauthorized": "No unauthorized access", - "offerEnds": "Offer ends February 29th" - } + "features": [ + "75% OFF all lifetime plans", + "Safe and secure cloud storage", + "Open source and transparent", + "End-to-end encrypted transfers", + "No unauthorized access", + "Offer ends April 8th" + ] }, "valentinesBanner": { "title": "Love without limits!", diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index d2fc32040..49f4f713c 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -4,19 +4,19 @@ "logOut": "Cerrar sesión" }, "featuresBanner": { - "header": "¡Ahorra un 70%!", - "title": "Prepárate para la Semana de la Privacidad de Datos", - "cta": "Elige un plan", + "label": "Oferta del Día Mundial de la Seguridad en la Nube", + "title": "Una vida de privacidad por mucho menos", + "cta": "Consíguela", "guarantee": "Garantía de devolución de 30 días", "lastCta": "*La oferta es para cuentas gratuitas o nuevos clientes", - "features": { - "discount": "Ahorra en suscripciones mensuales y anuales", - "safeCloud": "Almacenamiento seguro en la nube", - "openSource": "Código abierto y transparente", - "endToEnd": "Transferencias cifradas de extremo a extremo", - "unauthorized": "Sin acceso no autorizado", - "offerEnds": "La oferta finaliza el 29 de febrero" - } + "features": [ + "Oferta 75% en planes lifetime", + "Almacenamiento seguro en la nube", + "Código abierto y auditado", + "Transferencias cifradas de extremo a extremo", + "Sin accesos no autorizados", + "La promoción finaliza el 8 de abril" + ] }, "valentinesBanner": { "title": "¡Amor sin limites!", diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index f520eb3de..71b9978ad 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -4,19 +4,19 @@ "logOut": "Déconnexion" }, "featuresBanner": { - "header": "Économisez 70% !", - "title": "Préparez-vous pour la Semaine de la Protection des Données", + "label": "Offre pour la Journée mondiale de la sécurité cloud", + "title": "Une vie privée à 75% de moins", "cta": "Choisissez un plan", "guarantee": "Garantie de remboursement de 30 jours", "lastCta": "*L'offre est valable pour les comptes gratuits ou les nouveaux clients", - "features": { - "discount": "Économisez sur les plans mensuels et annuels", - "safeCloud": "Stockage cloud sûr et sécurisé", - "openSource": "Open source et transparent", - "endToEnd": "Transferts chiffrés de bout en bout", - "unauthorized": "Aucun accès non autorisé", - "offerEnds": "L'offre se termine le 29 février" - } + "features": [ + "75% de réduction sur tous les plans à vie", + "Stockage cloud sûr et sécurisé", + "Open source et transparent", + "Transferts chiffrés de bout en bout", + "Aucun accès non autorisé", + "L'offre se termine le 8 avril" + ] }, "valentinesBanner": { "title": "L'amour sans limites !", diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index 31d287ded..bec2a4169 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -9,19 +9,19 @@ "cta": "Get the deal" }, "featuresBanner": { - "header": "Risparmia il 70%!", - "title": "Preparati per la Settimana della Privacy dei Dati", + "label": "Offerta per la Giornata mondiale della sicurezza cloud", + "title": "Una vita di privacy per il 75% in meno", "cta": "Scegli un piano", "guarantee": "Garanzia di rimborso entro 30 giorni", "lastCta": "*L'offerta è valida solo per account gratuiti o nuovi clienti", - "features": { - "discount": "Risparmia su piani mensili e annuali", - "safeCloud": "Archiviazione cloud sicura e protetta", - "openSource": "Open source e trasparente", - "endToEnd": "Trasferimenti crittografati end-to-end", - "unauthorized": "Nessun accesso non autorizzato", - "offerEnds": "L'offerta termina il 29 febbraio" - } + "features": [ + "75% DI SCONTO su tutti i piani a vita", + "Archiviazione cloud sicura e protetta", + "Open source e trasparente", + "Trasferimenti crittografati end-to-end", + "Nessun accesso non autorizzato", + "L'offerta termina l'8 aprile" + ] }, "lifetimeBanner": { "label": "Cercate uno spazio sicuro per i vostri dati per tutta la vita?", diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index ed04b362d..c7425709b 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -9,19 +9,19 @@ "cta": "Получить скидку" }, "featuresBanner": { - "label": "Сэкономьте 70%!", - "title": "Приготовьтесь к Неделе конфиденциальности данных", + "label": "Предложение на День мировой безопасности в облаке", + "title": "Приватность на всю жизнь за 75% дешевле", "cta": "Выбрать план", - "guarantee": "30-дневная гарантия возврата денег", + "guarantee": "Гарантия возврата денег в течение 30 дней", "lastCta": "*Предложение действует только для бесплатных аккаунтов или новых клиентов", - "features": { - "discount": "Скидка на годовые и месячные планы", - "safeCloud": "Надежное и безопасное облачное хранилище", - "openSource": "Открытый исходный код и прозрачность", - "endToEnd": "Передача данных в зашифрованном виде", - "unauthorized": "Отсутствие несанкционированного доступа", - "offerEnds": "Предложение действует до 29 февраля" - } + "features": [ + "СКИДКА 75% на все пожизненные планы", + "Надежное и безопасное облачное хранилище", + "Открытый исходный код и прозрачность", + "Передача данных в зашифрованном виде", + "Отсутствие несанкционированного доступа", + "Предложение заканчивается 8 апреля" + ] }, "lifetimeBanner": { "label": "Ищите надежное хранилище данных на всю жизнь?", diff --git a/src/app/i18n/locales/zh.json b/src/app/i18n/locales/zh.json index 66d8e96a3..04e682001 100644 --- a/src/app/i18n/locales/zh.json +++ b/src/app/i18n/locales/zh.json @@ -4,19 +4,19 @@ "logOut": "登出" }, "featuresBanner": { - "header": "省 70%!", - "title": "为数据隐私周做好准备", + "label": "世界云安全日优惠", + "title": "75%折扣享终身隐私", "cta": "选择计划", - "guarantee": "30 天内退款保证", - "lastCta": "*此优惠仅适用于免费账户或新客户", - "features": { - "discount": "在月度和年度计划上节省", - "safeCloud": "安全可靠的云存储", - "openSource": "开源和透明", - "endToEnd": "端到端加密传输", - "unauthorized": "无未经授权访问", - "offerEnds": "优惠截止日期为2月29日" - } + "guarantee": "30天退款保证", + "lastCta": "*优惠仅适用于免费帐户或新客户", + "features": [ + "75%折扣所有终身计划", + "安全可靠的云存储", + "开源和透明", + "端到端加密传输", + "没有未经授权的访问", + "优惠截止日期为4月8日" + ] }, "valentinesBanner": { "title": "爱无极限!", diff --git a/src/app/i18n/provider/TranslationProvider.tsx b/src/app/i18n/provider/TranslationProvider.tsx index 573534e8c..1a91f3e1c 100644 --- a/src/app/i18n/provider/TranslationProvider.tsx +++ b/src/app/i18n/provider/TranslationProvider.tsx @@ -3,16 +3,20 @@ import { useTranslation } from 'react-i18next'; interface TranslationContextProps { translate: (key: string, props?: Record) => string; + translateList: (key: string) => string[]; } -const TranslationContext = createContext({ translate: () => '' }); +const TranslationContext = createContext({ translate: () => '', translateList: () => [] }); interface TranslationProviderProps { children: React.ReactNode; } export const TranslationProvider: React.FC = ({ children }) => { const { t } = useTranslation(); - const value = useMemo(() => ({ translate: t }), [t]); + + const translateList = (key: string) => t(key, { returnObjects: true }) as string[]; + + const value = useMemo(() => ({ translate: t, translateList }), [t]); return {children}; }; diff --git a/src/app/network/UploadManager.ts b/src/app/network/UploadManager.ts index e6770ed8f..52cd5c5a7 100644 --- a/src/app/network/UploadManager.ts +++ b/src/app/network/UploadManager.ts @@ -70,6 +70,7 @@ export const uploadFileWithManager = ( }; class UploadManager { + private currentGroupBeingUploaded: FileSizeType = FileSizeType.Small; private filesGroups: Record< FileSizeType, { @@ -101,6 +102,33 @@ class UploadManager { (fileData, next: (err: Error | null, res?: DriveFileData) => void) => { if (this.abortController?.signal.aborted ?? fileData.abortController?.signal.aborted) return; + if (window.performance && (window.performance as any).memory) { + const memory = window.performance.memory; + + if (memory && memory.jsHeapSizeLimit !== null && memory.usedJSHeapSize !== null) { + const memoryUsagePercentage = memory.usedJSHeapSize / memory.jsHeapSizeLimit; + const shouldIncreaseConcurrency = memoryUsagePercentage < 0.7 && this.currentGroupBeingUploaded !== FileSizeType.Big; + + if (shouldIncreaseConcurrency) { + const newConcurrency = Math.min( + this.uploadQueue.concurrency + 1, + this.filesGroups[FileSizeType.Small].concurrency + ); + console.warn(`Memory usage under 70%. Increasing upload concurrency to ${newConcurrency}`); + this.uploadQueue.concurrency = newConcurrency; + } + + const shouldReduceConcurrency = memoryUsagePercentage >= 0.8 && this.uploadQueue.concurrency > 1; + + if (shouldReduceConcurrency) { + console.warn('Memory usage reached 80%. Reducing upload concurrency.'); + this.uploadQueue.concurrency = 1; + } + } + } else { + console.warn('Memory usage control is not available'); + } + let uploadAttempts = 0; const uploadId = randomBytes(10).toString('hex'); const taskId = fileData.taskId; @@ -454,8 +482,12 @@ class UploadManager { if (smallSizedFiles.length > 0) await uploadFiles(smallSizedFiles, this.filesGroups.small.concurrency); + this.currentGroupBeingUploaded = FileSizeType.Medium; + if (mediumSizedFiles.length > 0) await uploadFiles(mediumSizedFiles, this.filesGroups.medium.concurrency); + this.currentGroupBeingUploaded = FileSizeType.Big; + if (bigSizedFiles.length > 0) await uploadFiles(bigSizedFiles, this.filesGroups.big.concurrency); return uploadedFilesData; diff --git a/src/app/shared/components/Tutorial/signUpSteps.tsx b/src/app/shared/components/Tutorial/signUpSteps.tsx index c5748bc18..0a651e193 100644 --- a/src/app/shared/components/Tutorial/signUpSteps.tsx +++ b/src/app/shared/components/Tutorial/signUpSteps.tsx @@ -1,5 +1,5 @@ -import { t } from 'i18next'; import { UploadSimple } from '@phosphor-icons/react'; +import { t } from 'i18next'; import { MutableRefObject } from 'react'; import Button from '../Button/Button'; import { OnboardingModal } from './OnBoardingModal'; @@ -9,6 +9,7 @@ export const getSignUpSteps = ( stepOneOptions: { onNextStepClicked: () => void; stepOneTutorialRef: MutableRefObject; + offset?: { x: number; y: number }; }, stepTwoOptions: { onNextStepClicked: () => void; @@ -37,7 +38,7 @@ export const getSignUpSteps = ( ), placement: 'bottom-end' as const, ref: stepOneOptions.stepOneTutorialRef, - offset: { x: 0, y: -40 }, + offset: { x: 0 + (stepOneOptions.offset?.x ?? 0), y: -40 + (stepOneOptions.offset?.x ?? 0) }, disableClickNextStepOutOfContent: true, }, { diff --git a/src/app/store/slices/storage/index.ts b/src/app/store/slices/storage/index.ts index c7ef505db..94ad64d78 100644 --- a/src/app/store/slices/storage/index.ts +++ b/src/app/store/slices/storage/index.ts @@ -17,8 +17,8 @@ const initialState: StorageState = { moveDialogLevels: {}, levelsFoldersLength: {}, levelsFilesLength: {}, - hasMoreDriveFolders: true, - hasMoreDriveFiles: true, + hasMoreDriveFolders: {}, + hasMoreDriveFiles: {}, recents: [], isLoadingRecents: false, isLoadingDeleted: false, @@ -108,15 +108,15 @@ export const storageSlice = createSlice({ state.levelsFilesLength[action.payload.folderId] = 0; state.levels[action.payload.folderId] = []; }, - setHasMoreDriveFolders: (state: StorageState, action: PayloadAction) => { - state.hasMoreDriveFolders = action.payload; + setHasMoreDriveFolders: (state: StorageState, action: PayloadAction<{ folderId: number; status: boolean }>) => { + state.hasMoreDriveFolders[action.payload.folderId] = action.payload.status; }, - setHasMoreDriveFiles: (state: StorageState, action: PayloadAction) => { - state.hasMoreDriveFiles = action.payload; + setHasMoreDriveFiles: (state: StorageState, action: PayloadAction<{ folderId: number; status: boolean }>) => { + state.hasMoreDriveFiles[action.payload.folderId] = action.payload.status; }, resetDrivePagination: (state: StorageState) => { - state.hasMoreDriveFiles = true; - state.hasMoreDriveFolders = true; + state.hasMoreDriveFolders[state.currentPath.id] = true; + state.hasMoreDriveFiles[state.currentPath.id] = true; }, setRecents: (state: StorageState, action: PayloadAction) => { state.recents = action.payload; diff --git a/src/app/store/slices/storage/storage.model.ts b/src/app/store/slices/storage/storage.model.ts index 173f76e57..b1c1dcba2 100644 --- a/src/app/store/slices/storage/storage.model.ts +++ b/src/app/store/slices/storage/storage.model.ts @@ -15,8 +15,8 @@ export interface StorageState { moveDialogLevels: Record; levelsFoldersLength: Record; levelsFilesLength: Record; - hasMoreDriveFolders: boolean; - hasMoreDriveFiles: boolean; + hasMoreDriveFolders: Record; + hasMoreDriveFiles: Record; recents: DriveItemData[]; isLoadingRecents: boolean; isLoadingDeleted: boolean; diff --git a/src/app/store/slices/storage/storage.selectors.ts b/src/app/store/slices/storage/storage.selectors.ts index 01029a3e4..be0a2cbc4 100644 --- a/src/app/store/slices/storage/storage.selectors.ts +++ b/src/app/store/slices/storage/storage.selectors.ts @@ -37,10 +37,10 @@ const storageSelectors = { return this.levelItems(state)(currentFolderId); }, hasMoreFiles(state: RootState): boolean { - return state.storage.hasMoreDriveFiles; + return state.storage.hasMoreDriveFiles[state.storage.currentPath.id]; }, hasMoreFolders(state: RootState): boolean { - return state.storage.hasMoreDriveFolders; + return state.storage.hasMoreDriveFolders[state.storage.currentPath.id]; }, levelItems(state: RootState): (folderId: number) => DriveItemData[] { return (folderId) => state.storage.levels[folderId] || []; diff --git a/src/app/store/slices/storage/storage.thunks/fetchFolderContentThunk.ts b/src/app/store/slices/storage/storage.thunks/fetchFolderContentThunk.ts index 7583c0382..5dbaf7575 100644 --- a/src/app/store/slices/storage/storage.thunks/fetchFolderContentThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/fetchFolderContentThunk.ts @@ -20,8 +20,9 @@ export const fetchPaginatedFolderContentThunk = createAsyncThunk { const storageState = getState().storage; - const hasMoreDriveFolders = storageState.hasMoreDriveFolders; - const hasMoreDriveFiles = storageState.hasMoreDriveFiles; + const hasMoreDriveFolders = storageState.hasMoreDriveFolders[folderId] ?? true; + const hasMoreDriveFiles = storageState.hasMoreDriveFiles[folderId] ?? true; + const foldersOffset = (storageState.levels[folderId] ?? []).filter(filterFolderItems).length; const filesOffset = (storageState.levels[folderId] ?? []).filter(filterFilesItems).length; const driveItemsSort = storageState.driveItemsSort; @@ -63,10 +64,10 @@ export const fetchPaginatedFolderContentThunk = createAsyncThunk( 'storage/fetchSortedFolderContentThunk', async (folderId, { getState, dispatch }) => { - dispatch(storageActions.setHasMoreDriveFolders(true)); - dispatch(storageActions.setHasMoreDriveFiles(true)); + dispatch(storageActions.setHasMoreDriveFolders({ folderId, status: true })); + dispatch(storageActions.setHasMoreDriveFiles({ folderId, status: true })); const storageState = getState().storage; const hasMoreDriveFolders = storageState.hasMoreDriveFolders; diff --git a/src/app/store/slices/storage/storage.thunks/goToFolderThunk.ts b/src/app/store/slices/storage/storage.thunks/goToFolderThunk.ts index 6df34b3aa..07a29849c 100644 --- a/src/app/store/slices/storage/storage.thunks/goToFolderThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/goToFolderThunk.ts @@ -28,6 +28,9 @@ export const goToFolderThunk = createAsyncThunk void) => void; execute: (siteKey: string, { action: string }) => Promise; }; + performance: { + memory?: { + jsHeapSizeLimit: number; + totalJSHeapSize: number; + usedJSHeapSize: number; + }; + } } interface Navigator {