add create sharable url logic

ui for opening shared collections
This commit is contained in:
Abhinav 2022-01-17 16:48:09 +05:30
parent c26db1184d
commit 429211f9a8
14 changed files with 216 additions and 11 deletions

View file

@ -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>

View file

@ -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;
}

View file

@ -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;

View file

@ -661,7 +661,6 @@ export default function Gallery() {
search={search}
setSearchStats={setSearchStats}
deleted={deleted}
setDialogMessage={setDialogMessage}
activeCollection={activeCollection}
isSharedCollection={isSharedCollection(
activeCollection,

View 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}
/>
);
}

View file

@ -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) {

View file

@ -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>[]

View 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');
}
};

View file

@ -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);

View file

@ -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();

View file

@ -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 {

View file

@ -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`
);
}

View file

@ -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(

View file

@ -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}`,