add create sharable url logic
ui for opening shared collections
This commit is contained in:
parent
c26db1184d
commit
429211f9a8
|
@ -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) => (
|
||||
<tr>
|
||||
<td>{sharee.email}</td>
|
||||
|
@ -154,6 +164,11 @@ function CollectionShare(props: Props) {
|
|||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<Button
|
||||
variant="outline-success"
|
||||
onClick={createSharableUrlHelper}>
|
||||
Create New Shareable URL
|
||||
</Button>
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
|
@ -162,6 +177,36 @@ function CollectionShare(props: Props) {
|
|||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
{props.collection?.publicAccessUrls.length > 0 && (
|
||||
<div style={{ width: '100%', wordBreak: 'break-all' }}>
|
||||
<p>{constants.PUBLIC_URL}</p>
|
||||
|
||||
<Table striped bordered hover variant="dark" size="sm">
|
||||
<tbody>
|
||||
{props.collection?.publicAccessUrls.map(
|
||||
(publicAccessUrl) => (
|
||||
<tr key={publicAccessUrl.url}>
|
||||
{
|
||||
<a
|
||||
href={transformShareURLForHost(
|
||||
publicAccessUrl.url,
|
||||
props.collection.key
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
{transformShareURLForHost(
|
||||
publicAccessUrl.url,
|
||||
props.collection.key
|
||||
)}
|
||||
</a>
|
||||
}
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
{props.collection?.sharees.length > 0 ? (
|
||||
<>
|
||||
<p>{constants.SHAREES}</p>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -661,7 +661,6 @@ export default function Gallery() {
|
|||
search={search}
|
||||
setSearchStats={setSearchStats}
|
||||
deleted={deleted}
|
||||
setDialogMessage={setDialogMessage}
|
||||
activeCollection={activeCollection}
|
||||
isSharedCollection={isSharedCollection(
|
||||
activeCollection,
|
||||
|
|
48
src/pages/shared-album/index.tsx
Normal file
48
src/pages/shared-album/index.tsx
Normal file
|
@ -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 (
|
||||
<PhotoFrame
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
syncWithRemote={syncWithRemote}
|
||||
favItemIds={null}
|
||||
setSelected={() => null}
|
||||
selected={{ count: 0, collectionID: null }}
|
||||
isFirstLoad={false}
|
||||
openFileUploader={() => null}
|
||||
loadingBar={null}
|
||||
isInSearchMode={false}
|
||||
search={{}}
|
||||
setSearchStats={() => null}
|
||||
deleted={null}
|
||||
activeCollection={ALL_SECTION}
|
||||
isSharedCollection={true}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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<EnteFile>[]
|
||||
|
|
58
src/services/sharedCollectionService.ts
Normal file
58
src/services/sharedCollectionService.ts
Normal file
|
@ -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<EnteFile>[]
|
||||
))
|
||||
);
|
||||
|
||||
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');
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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`
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -377,6 +377,7 @@ const englishConstants = {
|
|||
</div>
|
||||
</>
|
||||
),
|
||||
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}`,
|
||||
|
|
Loading…
Reference in a new issue