diff --git a/src/components/CollectionShare.tsx b/src/components/CollectionShare.tsx index 3aabd3b0b..fe36939ea 100644 --- a/src/components/CollectionShare.tsx +++ b/src/components/CollectionShare.tsx @@ -7,11 +7,16 @@ import FormControl from 'react-bootstrap/FormControl'; import { Button, Col, Table } from 'react-bootstrap'; import { DeadCenter } from 'pages/gallery'; import { User } from 'types/user'; -import { shareCollection, unshareCollection } from 'services/collectionService'; +import { + shareCollection, + unshareCollection, + createShareableUrl, +} from 'services/collectionService'; import { getData, LS_KEYS } from 'utils/storage/localStorage'; import SubmitButton from './SubmitButton'; import MessageDialog from './MessageDialog'; import { Collection } from 'types/collection'; +import { transformShareURLForHost } from 'utils/collection'; interface Props { show: boolean; @@ -72,6 +77,11 @@ function CollectionShare(props: Props) { await props.syncWithRemote(); }; + const createSharableUrlHelper = async () => { + await createShareableUrl(props.collection); + await props.syncWithRemote(); + }; + const ShareeRow = ({ sharee, collectionUnshare }: ShareeProps) => ( {sharee.email} @@ -154,6 +164,11 @@ function CollectionShare(props: Props) { )} +
+ {props.collection?.publicAccessUrls.length > 0 && ( +
+

{constants.PUBLIC_URL}

+ + + + {props.collection?.publicAccessUrls.map( + (publicAccessUrl) => ( + + { + + {transformShareURLForHost( + publicAccessUrl.url, + props.collection.key + )} + + } + + ) + )} + +
+
+ )} {props.collection?.sharees.length > 0 ? ( <>

{constants.SHAREES}

diff --git a/src/components/PhotoFrame.tsx b/src/components/PhotoFrame.tsx index 2532c6aeb..61ae240b4 100644 --- a/src/components/PhotoFrame.tsx +++ b/src/components/PhotoFrame.tsx @@ -9,7 +9,6 @@ import constants from 'utils/strings/constants'; import AutoSizer from 'react-virtualized-auto-sizer'; import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe'; import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search'; -import { SetDialogMessage } from './MessageDialog'; import { fileIsArchived, formatDateRelative } from 'utils/file'; import { ALL_SECTION, @@ -64,7 +63,6 @@ interface Props { search: Search; setSearchStats: setSearchStats; deleted?: number[]; - setDialogMessage: SetDialogMessage; activeCollection: number; isSharedCollection: boolean; } diff --git a/src/constants/collection/index.ts b/src/constants/collection/index.ts index afb10f0e4..0e68e9a47 100644 --- a/src/constants/collection/index.ts +++ b/src/constants/collection/index.ts @@ -13,3 +13,7 @@ export enum COLLECTION_SORT_BY { MODIFICATION_TIME, NAME, } + +export const COLLECTION_SHARE_DEFAULT_VALID_DURATION = + 10 * 24 * 60 * 60 * 1000 * 1000; +export const COLLECTION_SHARE_DEFAULT_DEVICE_LIMIT = 4; diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index 9a7d6136d..71152bcbc 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -661,7 +661,6 @@ export default function Gallery() { search={search} setSearchStats={setSearchStats} deleted={deleted} - setDialogMessage={setDialogMessage} activeCollection={activeCollection} isSharedCollection={isSharedCollection( activeCollection, diff --git a/src/pages/shared-album/index.tsx b/src/pages/shared-album/index.tsx new file mode 100644 index 000000000..95358e847 --- /dev/null +++ b/src/pages/shared-album/index.tsx @@ -0,0 +1,48 @@ +import { ALL_SECTION } from 'constants/collection'; +import PhotoFrame from 'components/PhotoFrame'; +import React, { useEffect, useState } from 'react'; +import { getSharedCollectionFiles } from 'services/sharedCollectionService'; + +export default function sharedAlbum() { + const [token, setToken] = useState(null); + const [collectionKey, setCollectionKey] = useState(null); + const [files, setFiles] = useState([]); + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const token = urlParams.get('accessToken'); + const collectionKey = urlParams.get('collectionKey'); + setToken(token); + setCollectionKey(collectionKey); + syncWithRemote(token, collectionKey); + }, []); + + const syncWithRemote = async (t?: string, c?: string) => { + const files = await getSharedCollectionFiles( + t ?? token, + c ?? collectionKey, + setFiles + ); + console.log(files); + setFiles(files); + }; + + return ( + null} + selected={{ count: 0, collectionID: null }} + isFirstLoad={false} + openFileUploader={() => null} + loadingBar={null} + isInSearchMode={false} + search={{}} + setSearchStats={() => null} + deleted={null} + activeCollection={ALL_SECTION} + isSharedCollection={true} + /> + ); +} diff --git a/src/services/collectionService.ts b/src/services/collectionService.ts index cd63c2e56..3099eb017 100644 --- a/src/services/collectionService.ts +++ b/src/services/collectionService.ts @@ -20,8 +20,15 @@ import { MoveToCollectionRequest, EncryptedFileKey, RemoveFromCollectionRequest, + CreatePublicAccessTokenRequest, + PublicAccessUrl, } from 'types/collection'; -import { COLLECTION_SORT_BY, CollectionType } from 'constants/collection'; +import { + COLLECTION_SORT_BY, + CollectionType, + COLLECTION_SHARE_DEFAULT_DEVICE_LIMIT, + COLLECTION_SHARE_DEFAULT_VALID_DURATION, +} from 'constants/collection'; const ENDPOINT = getEndpoint(); const COLLECTION_TABLE = 'collections'; @@ -574,6 +581,31 @@ export const unshareCollection = async ( } }; +export const createShareableUrl = async (collection: Collection) => { + try { + const token = getToken(); + const createPublicAccessTokenRequest: CreatePublicAccessTokenRequest = { + collectionID: collection.id, + validTill: + Date.now() * 1000 + COLLECTION_SHARE_DEFAULT_VALID_DURATION, + deviceLimit: COLLECTION_SHARE_DEFAULT_DEVICE_LIMIT, + }; + const resp = await HTTPService.post( + `${ENDPOINT}/collections/share-url`, + createPublicAccessTokenRequest, + null, + { + 'X-Auth-Token': token, + } + ); + console.log(resp); + return resp.data as PublicAccessUrl; + } catch (e) { + logError(e, 'createShareableUrl failed '); + throw e; + } +}; + export const getFavCollection = async () => { const collections = await getLocalCollections(); for (const collection of collections) { diff --git a/src/services/fileService.ts b/src/services/fileService.ts index 8c9f0ece3..8d1ad4944 100644 --- a/src/services/fileService.ts +++ b/src/services/fileService.ts @@ -105,7 +105,7 @@ export const getFiles = async ( ...(await Promise.all( resp.data.diff.map(async (file: EnteFile) => { if (!file.isDeleted) { - file = await decryptFile(file, collection); + file = await decryptFile(file, collection.key); } return file; }) as Promise[] diff --git a/src/services/sharedCollectionService.ts b/src/services/sharedCollectionService.ts new file mode 100644 index 000000000..281ba9623 --- /dev/null +++ b/src/services/sharedCollectionService.ts @@ -0,0 +1,58 @@ +import { EnteFile } from 'types/file'; +import { getEndpoint } from 'utils/common/apiUtil'; +import { decryptFile, sortFiles, mergeMetadata } from 'utils/file'; +import { logError } from 'utils/sentry'; +import HTTPService from './HTTPService'; + +const ENDPOINT = getEndpoint(); + +export const getSharedCollectionFiles = async ( + token: string, + collectionKey: string, + setFiles: (files: EnteFile[]) => void +) => { + try { + if (!token || !collectionKey) { + throw Error('token or collectionKey missing'); + } + const decryptedFiles: EnteFile[] = []; + let time = 0; + let resp; + do { + resp = await HTTPService.get( + `${ENDPOINT}/public-collection/diff`, + { + sinceTime: time, + }, + { + 'X-Auth-Access-Token': token, + } + ); + + decryptedFiles.push( + ...(await Promise.all( + resp.data.diff.map(async (file: EnteFile) => { + if (!file.isDeleted) { + file = await decryptFile(file, collectionKey); + } + return file; + }) as Promise[] + )) + ); + + if (resp.data.diff.length) { + time = resp.data.diff.slice(-1)[0].updationTime; + } + setFiles( + sortFiles( + mergeMetadata( + decryptedFiles.filter((item) => !item.isDeleted) + ) + ) + ); + } while (resp.data.hasMore); + return decryptedFiles; + } catch (e) { + logError(e, 'Get files failed'); + } +}; diff --git a/src/services/trashService.ts b/src/services/trashService.ts index 36ea04756..1dee6537c 100644 --- a/src/services/trashService.ts +++ b/src/services/trashService.ts @@ -107,7 +107,7 @@ export const updateTrash = async ( if (!trashItem.isDeleted && !trashItem.isRestored) { trashItem.file = await decryptFile( trashItem.file, - collection + collection.key ); } updatedTrash.push(trashItem); diff --git a/src/services/upload/uploader.ts b/src/services/upload/uploader.ts index aee78c2b9..ce6662079 100644 --- a/src/services/upload/uploader.ts +++ b/src/services/upload/uploader.ts @@ -103,7 +103,7 @@ export default async function uploader( ); const uploadedFile = await UploadHttpClient.uploadFile(uploadFile); - const decryptedFile = await decryptFile(uploadedFile, collection); + const decryptedFile = await decryptFile(uploadedFile, collection.key); UIService.setFileProgress(rawFile.name, FileUploadResults.UPLOADED); UIService.increaseFileUploaded(); diff --git a/src/types/collection/index.ts b/src/types/collection/index.ts index b6e35693a..b734740c4 100644 --- a/src/types/collection/index.ts +++ b/src/types/collection/index.ts @@ -17,6 +17,19 @@ export interface Collection { keyDecryptionNonce: string; isDeleted: boolean; isSharedCollection?: boolean; + publicAccessUrls?: PublicAccessUrl[]; +} + +export interface PublicAccessUrl { + url: string; + deviceLimit: number; + validTill: number; +} + +export interface CreatePublicAccessTokenRequest { + collectionID: number; + validTill: number; + deviceLimit: number; } export interface EncryptedFileKey { diff --git a/src/utils/collection/index.ts b/src/utils/collection/index.ts index e63500b60..dbe476dcb 100644 --- a/src/utils/collection/index.ts +++ b/src/utils/collection/index.ts @@ -107,3 +107,11 @@ export async function downloadCollection( }); } } + +export function transformShareURLForHost(url: string, collectionKey: string) { + const host = window.location.host; + return `${url}&collectionKey=${collectionKey}`.replace( + 'https://albums.ente.io', + `http://${host}/shared-album` + ); +} diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts index 929c319ea..88afd9d96 100644 --- a/src/utils/file/index.ts +++ b/src/utils/file/index.ts @@ -1,5 +1,4 @@ import { SelectedState } from 'types/gallery'; -import { Collection } from 'types/collection'; import { EnteFile, fileAttribute, @@ -202,13 +201,13 @@ export function sortFiles(files: EnteFile[]) { return files; } -export async function decryptFile(file: EnteFile, collection: Collection) { +export async function decryptFile(file: EnteFile, collectionKey: string) { try { const worker = await new CryptoWorker(); file.key = await worker.decryptB64( file.encryptedKey, file.keyDecryptionNonce, - collection.key + collectionKey ); const encryptedMetadata = file.metadata as unknown as fileAttribute; file.metadata = await worker.decryptMetadata( diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx index 0125d4fcf..074431a16 100644 --- a/src/utils/strings/englishConstants.tsx +++ b/src/utils/strings/englishConstants.tsx @@ -377,6 +377,7 @@ const englishConstants = {
), + PUBLIC_URL: 'public share url', SHARE_WITH_SELF: 'oops, you cannot share with yourself', ALREADY_SHARED: (email) => `oops, you're already sharing this with ${email}`,