Merge pull request #585 from ente-io/collection-share-redesign
Collection share redesign
This commit is contained in:
commit
c9558aaa16
|
@ -2,7 +2,7 @@ import { Dialog, Slide, styled } from '@mui/material';
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const FloatingDrawer = styled(Dialog)(({ theme }) => ({
|
||||
export const AllCollectionContainer = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialog-container': {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
|
@ -17,7 +17,7 @@ export const FloatingDrawer = styled(Dialog)(({ theme }) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
FloatingDrawer.propTypes = {
|
||||
AllCollectionContainer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
|
@ -4,8 +4,8 @@ import { CollectionType, COLLECTION_SORT_BY } from 'constants/collection';
|
|||
import { sortCollectionSummaries } from 'services/collectionService';
|
||||
import {
|
||||
Transition,
|
||||
FloatingDrawer,
|
||||
} from 'components/Collections/FloatingDrawer';
|
||||
AllCollectionContainer,
|
||||
} from 'components/Collections/AllCollections/Container';
|
||||
import { useLocalState } from 'hooks/useLocalState';
|
||||
import { LS_KEYS } from 'utils/storage/localStorage';
|
||||
import AllCollectionsHeader from './header';
|
||||
|
@ -48,7 +48,7 @@ export default function AllCollections(props: Iprops) {
|
|||
};
|
||||
|
||||
return (
|
||||
<FloatingDrawer
|
||||
<AllCollectionContainer
|
||||
TransitionComponent={LeftSlideTransition}
|
||||
onClose={close}
|
||||
open={isOpen}>
|
||||
|
@ -64,6 +64,6 @@ export default function AllCollections(props: Iprops) {
|
|||
collectionSummaries={sortedCollectionSummaries}
|
||||
onCollectionClick={onCollectionClick}
|
||||
/>
|
||||
</FloatingDrawer>
|
||||
</AllCollectionContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,607 +0,0 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import Select from 'react-select';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Form from 'react-bootstrap/Form';
|
||||
import FormControl from 'react-bootstrap/FormControl';
|
||||
import { Button, Col, Table } from 'react-bootstrap';
|
||||
import { DeadCenter, GalleryContext } from 'pages/gallery';
|
||||
import { User } from 'types/user';
|
||||
import {
|
||||
shareCollection,
|
||||
unshareCollection,
|
||||
createShareableURL,
|
||||
deleteShareableURL,
|
||||
updateShareableURL,
|
||||
} from 'services/collectionService';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import SubmitButton from '../SubmitButton';
|
||||
import DialogBox from '../DialogBox';
|
||||
import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
|
||||
import {
|
||||
appendCollectionKeyToShareURL,
|
||||
selectIntOptions,
|
||||
shareExpiryOptions,
|
||||
} from 'utils/collection';
|
||||
import { FlexWrapper, Label, Row, Value } from '../Container';
|
||||
import CodeBlock from '../CodeBlock';
|
||||
import { ButtonVariant, getVariantColor } from '../pages/gallery/LinkButton';
|
||||
import { handleSharingErrors } from 'utils/error';
|
||||
import { sleep } from 'utils/common';
|
||||
import { SelectStyles } from '../Search/styles';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import { dateStringWithMMH } from 'utils/time';
|
||||
import styled from 'styled-components';
|
||||
import SingleInputForm from '../SingleInputForm';
|
||||
import { AppContext } from 'pages/_app';
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
collection: Collection;
|
||||
}
|
||||
interface formValues {
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface ShareeProps {
|
||||
sharee: User;
|
||||
collectionUnshare: (sharee: User) => void;
|
||||
}
|
||||
|
||||
const DropdownStyle = {
|
||||
...SelectStyles,
|
||||
dropdownIndicator: (style) => ({
|
||||
...style,
|
||||
margin: '0px',
|
||||
}),
|
||||
singleValue: (style) => ({
|
||||
...style,
|
||||
color: '#d1d1d1',
|
||||
width: '240px',
|
||||
}),
|
||||
control: (style, { isFocused }) => ({
|
||||
...style,
|
||||
...SelectStyles.control(style, { isFocused }),
|
||||
minWidth: '240px',
|
||||
}),
|
||||
};
|
||||
|
||||
const linkExpiryStyle = {
|
||||
...DropdownStyle,
|
||||
placeholder: (style) => ({
|
||||
...style,
|
||||
color: '#d1d1d1',
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
}),
|
||||
};
|
||||
|
||||
const OptionRow = styled(Row)`
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
`;
|
||||
const OptionLabel = styled(Label)`
|
||||
flex: 1 1 103px;
|
||||
@media (min-width: 513px) {
|
||||
text-align: left;
|
||||
}
|
||||
margin: 5px;
|
||||
`;
|
||||
const OptionValue = styled(Value)`
|
||||
flex: 0 0 240px;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
`;
|
||||
|
||||
function CollectionShare(props: Props) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const appContext = useContext(AppContext);
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const [sharableLinkError, setSharableLinkError] = useState(null);
|
||||
const [publicShareUrl, setPublicShareUrl] = useState<string>(null);
|
||||
const [publicShareProp, setPublicShareProp] = useState<PublicURL>(null);
|
||||
const [configurePassword, setConfigurePassword] = useState(false);
|
||||
const deviceLimitOptions = selectIntOptions(50);
|
||||
const expiryOptions = shareExpiryOptions;
|
||||
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
if (props.collection?.publicURLs?.[0]?.url) {
|
||||
const t = await appendCollectionKeyToShareURL(
|
||||
props.collection?.publicURLs?.[0]?.url,
|
||||
props.collection.key
|
||||
);
|
||||
setPublicShareUrl(t);
|
||||
setPublicShareProp(
|
||||
props.collection?.publicURLs?.[0] as PublicURL
|
||||
);
|
||||
} else {
|
||||
setPublicShareUrl(null);
|
||||
setPublicShareProp(null);
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, [props.collection]);
|
||||
|
||||
const collectionShare = async (
|
||||
{ email }: formValues,
|
||||
{ resetForm, setFieldError }: FormikHelpers<formValues>
|
||||
) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
appContext.startLoading();
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
if (email === user.email) {
|
||||
setFieldError('email', constants.SHARE_WITH_SELF);
|
||||
} else if (
|
||||
props.collection?.sharees?.find(
|
||||
(value) => value.email === email
|
||||
)
|
||||
) {
|
||||
setFieldError('email', constants.ALREADY_SHARED(email));
|
||||
} else {
|
||||
await shareCollection(props.collection, email);
|
||||
await sleep(2000);
|
||||
await galleryContext.syncWithRemote(false, true);
|
||||
resetForm();
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMessage = handleSharingErrors(e);
|
||||
setFieldError('email', errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
appContext.finishLoading();
|
||||
}
|
||||
};
|
||||
const collectionUnshare = async (sharee) => {
|
||||
try {
|
||||
appContext.startLoading();
|
||||
await unshareCollection(props.collection, sharee.email);
|
||||
await sleep(2000);
|
||||
await galleryContext.syncWithRemote(false, true);
|
||||
} finally {
|
||||
appContext.finishLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const createSharableURLHelper = async () => {
|
||||
try {
|
||||
appContext.startLoading();
|
||||
const publicURL = await createShareableURL(props.collection);
|
||||
const sharableURL = await appendCollectionKeyToShareURL(
|
||||
publicURL.url,
|
||||
props.collection.key
|
||||
);
|
||||
setPublicShareUrl(sharableURL);
|
||||
galleryContext.syncWithRemote(false, true);
|
||||
} catch (e) {
|
||||
const errorMessage = handleSharingErrors(e);
|
||||
setSharableLinkError(errorMessage);
|
||||
} finally {
|
||||
appContext.finishLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const disablePublicSharingHelper = async () => {
|
||||
try {
|
||||
appContext.startLoading();
|
||||
await deleteShareableURL(props.collection);
|
||||
setPublicShareUrl(null);
|
||||
galleryContext.syncWithRemote(false, true);
|
||||
} catch (e) {
|
||||
const errorMessage = handleSharingErrors(e);
|
||||
setSharableLinkError(errorMessage);
|
||||
} finally {
|
||||
appContext.finishLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const savePassword = async (passphrase, setFieldError) => {
|
||||
if (passphrase && passphrase.trim().length >= 1) {
|
||||
await enablePublicUrlPassword(passphrase);
|
||||
setConfigurePassword(false);
|
||||
publicShareProp.passwordEnabled = true;
|
||||
} else {
|
||||
setFieldError('linkPassword', 'can not be empty');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordChangeSetting = async () => {
|
||||
if (publicShareProp.passwordEnabled) {
|
||||
await disablePublicUrlPassword();
|
||||
} else {
|
||||
setConfigurePassword(true);
|
||||
}
|
||||
};
|
||||
|
||||
const disablePublicUrlPassword = async () => {
|
||||
appContext.setDialogMessage({
|
||||
title: constants.DISABLE_PASSWORD,
|
||||
content: constants.DISABLE_PASSWORD_MESSAGE,
|
||||
close: { text: constants.CANCEL },
|
||||
proceed: {
|
||||
text: constants.DISABLE,
|
||||
action: () =>
|
||||
updatePublicShareURLHelper({
|
||||
collectionID: props.collection.id,
|
||||
disablePassword: true,
|
||||
}),
|
||||
variant: ButtonVariant.danger,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const enablePublicUrlPassword = async (password: string) => {
|
||||
const cryptoWorker = await new CryptoWorker();
|
||||
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
|
||||
const kek = await cryptoWorker.deriveInteractiveKey(password, kekSalt);
|
||||
|
||||
return updatePublicShareURLHelper({
|
||||
collectionID: props.collection.id,
|
||||
passHash: kek.key,
|
||||
nonce: kekSalt,
|
||||
opsLimit: kek.opsLimit,
|
||||
memLimit: kek.memLimit,
|
||||
});
|
||||
};
|
||||
|
||||
const disablePublicSharing = () => {
|
||||
appContext.setDialogMessage({
|
||||
title: constants.DISABLE_PUBLIC_SHARING,
|
||||
content: constants.DISABLE_PUBLIC_SHARING_MESSAGE,
|
||||
close: { text: constants.CANCEL },
|
||||
proceed: {
|
||||
text: constants.DISABLE,
|
||||
action: disablePublicSharingHelper,
|
||||
variant: ButtonVariant.danger,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const disableFileDownload = () => {
|
||||
appContext.setDialogMessage({
|
||||
title: constants.DISABLE_FILE_DOWNLOAD,
|
||||
content: constants.DISABLE_FILE_DOWNLOAD_MESSAGE,
|
||||
close: { text: constants.CANCEL },
|
||||
proceed: {
|
||||
text: constants.DISABLE,
|
||||
action: () =>
|
||||
updatePublicShareURLHelper({
|
||||
collectionID: props.collection.id,
|
||||
enableDownload: false,
|
||||
}),
|
||||
variant: ButtonVariant.danger,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updatePublicShareURLHelper = async (req: UpdatePublicURL) => {
|
||||
try {
|
||||
galleryContext.setBlockingLoad(true);
|
||||
const response = await updateShareableURL(req);
|
||||
setPublicShareProp(response);
|
||||
galleryContext.syncWithRemote(false, true);
|
||||
} catch (e) {
|
||||
const errorMessage = handleSharingErrors(e);
|
||||
setSharableLinkError(errorMessage);
|
||||
} finally {
|
||||
galleryContext.setBlockingLoad(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateDeviceLimit = async (newLimit: number) => {
|
||||
return updatePublicShareURLHelper({
|
||||
collectionID: props.collection.id,
|
||||
deviceLimit: newLimit,
|
||||
});
|
||||
};
|
||||
|
||||
const updateDeviceExpiry = async (optionFn) => {
|
||||
return updatePublicShareURLHelper({
|
||||
collectionID: props.collection.id,
|
||||
validTill: optionFn(),
|
||||
});
|
||||
};
|
||||
|
||||
const handleCollectionPublicSharing = () => {
|
||||
setSharableLinkError(null);
|
||||
if (publicShareUrl) {
|
||||
disablePublicSharing();
|
||||
} else {
|
||||
createSharableURLHelper();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileDownloadSetting = () => {
|
||||
if (publicShareProp.enableDownload) {
|
||||
disableFileDownload();
|
||||
} else {
|
||||
updatePublicShareURLHelper({
|
||||
collectionID: props.collection.id,
|
||||
enableDownload: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const ShareeRow = ({ sharee, collectionUnshare }: ShareeProps) => (
|
||||
<tr>
|
||||
<td>{sharee.email}</td>
|
||||
<td>
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
style={{
|
||||
height: '25px',
|
||||
lineHeight: 0,
|
||||
padding: 0,
|
||||
width: '25px',
|
||||
fontSize: '1.2em',
|
||||
fontWeight: 900,
|
||||
}}
|
||||
onClick={() => collectionUnshare(sharee)}>
|
||||
-
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
if (!props.collection) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogBox
|
||||
open={props.show}
|
||||
onClose={props.onHide}
|
||||
attributes={{
|
||||
title: constants.SHARE_COLLECTION,
|
||||
staticBackdrop: true,
|
||||
}}>
|
||||
<DeadCenter style={{ width: '85%', margin: 'auto' }}>
|
||||
<h6 style={{ marginTop: '8px' }}>
|
||||
{constants.SHARE_WITH_PEOPLE}
|
||||
</h6>
|
||||
<p />
|
||||
<Formik<formValues>
|
||||
initialValues={{ email: '' }}
|
||||
validationSchema={Yup.object().shape({
|
||||
email: Yup.string()
|
||||
.email(constants.EMAIL_ERROR)
|
||||
.required(constants.REQUIRED),
|
||||
})}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
onSubmit={collectionShare}>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
touched,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
}) => (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Row>
|
||||
<Form.Group
|
||||
as={Col}
|
||||
xs={10}
|
||||
controlId="formHorizontalEmail">
|
||||
<Form.Control
|
||||
type="email"
|
||||
placeholder={constants.ENTER_EMAIL}
|
||||
value={values.email}
|
||||
onChange={handleChange('email')}
|
||||
isInvalid={Boolean(
|
||||
touched.email && errors.email
|
||||
)}
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
/>
|
||||
<FormControl.Feedback type="invalid">
|
||||
{errors.email}
|
||||
</FormControl.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group
|
||||
as={Col}
|
||||
xs={2}
|
||||
controlId="formHorizontalEmail">
|
||||
<SubmitButton
|
||||
loading={loading}
|
||||
inline
|
||||
buttonText="+"
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form.Row>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
{props.collection.sharees?.length > 0 && (
|
||||
<>
|
||||
<p>{constants.SHAREES}</p>
|
||||
|
||||
<Table striped bordered hover variant="dark" size="sm">
|
||||
<tbody>
|
||||
{props.collection.sharees?.map((sharee) => (
|
||||
<ShareeRow
|
||||
key={sharee.email}
|
||||
sharee={sharee}
|
||||
collectionUnshare={collectionUnshare}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
marginTop: '10px',
|
||||
marginBottom: '18px',
|
||||
background: '#444',
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<FlexWrapper>
|
||||
<FlexWrapper
|
||||
style={{ paddingTop: '5px', color: '#fff' }}>
|
||||
{constants.PUBLIC_SHARING}
|
||||
</FlexWrapper>
|
||||
<Form.Switch
|
||||
style={{ marginLeft: '20px' }}
|
||||
checked={!!publicShareUrl}
|
||||
id="collection-public-sharing-toggler"
|
||||
className="custom-switch-md"
|
||||
onChange={handleCollectionPublicSharing}
|
||||
/>
|
||||
</FlexWrapper>
|
||||
{sharableLinkError && (
|
||||
<FlexWrapper
|
||||
style={{
|
||||
marginTop: '10px',
|
||||
color: getVariantColor(ButtonVariant.danger),
|
||||
}}>
|
||||
{sharableLinkError}
|
||||
</FlexWrapper>
|
||||
)}
|
||||
</div>
|
||||
{publicShareUrl ? (
|
||||
<>
|
||||
<CodeBlock
|
||||
wordBreak={'break-all'}
|
||||
code={publicShareUrl}
|
||||
/>
|
||||
<details style={{ width: '100%' }}>
|
||||
<summary
|
||||
onClick={(e) => {
|
||||
const lastOptionRow: Element =
|
||||
e.currentTarget.nextElementSibling
|
||||
.lastElementChild;
|
||||
const main = async (
|
||||
lastOptionRow: Element
|
||||
) => {
|
||||
await sleep(0);
|
||||
lastOptionRow.scrollIntoView(true);
|
||||
};
|
||||
main(lastOptionRow);
|
||||
}}
|
||||
className="manageLinkHeader"
|
||||
style={{ marginBottom: '20px' }}>
|
||||
{constants.MANAGE_LINK}
|
||||
</summary>
|
||||
<section>
|
||||
<OptionRow>
|
||||
<OptionLabel>
|
||||
{constants.LINK_DEVICE_LIMIT}
|
||||
</OptionLabel>
|
||||
<OptionValue>
|
||||
<Select
|
||||
menuPosition="fixed"
|
||||
options={deviceLimitOptions}
|
||||
isSearchable={false}
|
||||
value={{
|
||||
label: publicShareProp?.deviceLimit.toString(),
|
||||
value: publicShareProp?.deviceLimit,
|
||||
}}
|
||||
onChange={(e) =>
|
||||
updateDeviceLimit(e.value)
|
||||
}
|
||||
styles={DropdownStyle}
|
||||
/>
|
||||
</OptionValue>
|
||||
</OptionRow>
|
||||
|
||||
<OptionRow>
|
||||
<OptionLabel
|
||||
style={{ alignItems: 'center' }}>
|
||||
{constants.LINK_EXPIRY}
|
||||
</OptionLabel>
|
||||
<OptionValue>
|
||||
<Select
|
||||
menuPosition="fixed"
|
||||
options={expiryOptions}
|
||||
isSearchable={false}
|
||||
value={null}
|
||||
placeholder={
|
||||
publicShareProp?.validTill
|
||||
? dateStringWithMMH(
|
||||
publicShareProp?.validTill
|
||||
)
|
||||
: 'never'
|
||||
}
|
||||
onChange={(e) => {
|
||||
updateDeviceExpiry(e.value);
|
||||
}}
|
||||
styles={linkExpiryStyle}
|
||||
/>
|
||||
</OptionValue>
|
||||
</OptionRow>
|
||||
<OptionRow>
|
||||
<OptionLabel>
|
||||
{constants.FILE_DOWNLOAD}
|
||||
</OptionLabel>
|
||||
<OptionValue>
|
||||
<Form.Switch
|
||||
style={{ marginLeft: '10px' }}
|
||||
checked={
|
||||
publicShareProp?.enableDownload ??
|
||||
false
|
||||
}
|
||||
id="public-sharing-file-download-toggler"
|
||||
className="custom-switch-md"
|
||||
onChange={handleFileDownloadSetting}
|
||||
/>
|
||||
</OptionValue>
|
||||
</OptionRow>
|
||||
|
||||
<OptionRow>
|
||||
<OptionLabel>
|
||||
{constants.LINK_PASSWORD_LOCK}{' '}
|
||||
</OptionLabel>
|
||||
<OptionValue>
|
||||
<Form.Switch
|
||||
style={{ marginLeft: '10px' }}
|
||||
checked={
|
||||
publicShareProp?.passwordEnabled
|
||||
}
|
||||
id="public-sharing-file-password-toggler"
|
||||
className="custom-switch-md"
|
||||
onChange={
|
||||
handlePasswordChangeSetting
|
||||
}
|
||||
/>
|
||||
</OptionValue>
|
||||
</OptionRow>
|
||||
</section>
|
||||
<DialogBox
|
||||
open={configurePassword}
|
||||
onClose={() => setConfigurePassword(false)}
|
||||
size="sm"
|
||||
attributes={{
|
||||
title: constants.PASSWORD_LOCK,
|
||||
}}>
|
||||
<SingleInputForm
|
||||
callback={savePassword}
|
||||
placeholder={
|
||||
constants.RETURN_PASSPHRASE_HINT
|
||||
}
|
||||
buttonText={constants.LOCK}
|
||||
fieldType="password"
|
||||
/>
|
||||
</DialogBox>
|
||||
</details>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
marginTop: '28px',
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DeadCenter>
|
||||
</DialogBox>
|
||||
);
|
||||
}
|
||||
export default CollectionShare;
|
22
src/components/Collections/CollectionShare/container.tsx
Normal file
22
src/components/Collections/CollectionShare/container.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Dialog, styled } from '@mui/material';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const CollectionShareContainer = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialog-container': {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
'& .MuiPaper-root': {
|
||||
maxWidth: '414px',
|
||||
},
|
||||
'& .MuiDialogTitle-root': {
|
||||
padding: theme.spacing(4, 3, 3, 4),
|
||||
},
|
||||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(3, 4),
|
||||
},
|
||||
}));
|
||||
|
||||
CollectionShareContainer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
46
src/components/Collections/CollectionShare/emailShare.tsx
Normal file
46
src/components/Collections/CollectionShare/emailShare.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import SingleInputForm from 'components/SingleInputForm';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import React, { useContext } from 'react';
|
||||
import { shareCollection } from 'services/collectionService';
|
||||
import { User } from 'types/user';
|
||||
import { sleep } from 'utils/common';
|
||||
import { handleSharingErrors } from 'utils/error';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { CollectionShareSharees } from './sharees';
|
||||
import CollectionShareSubmitButton from './submitButton';
|
||||
export default function EmailShare({ collection }) {
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
const collectionShare = async (email, setFieldError) => {
|
||||
try {
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
if (email === user.email) {
|
||||
setFieldError('email', constants.SHARE_WITH_SELF);
|
||||
} else if (
|
||||
collection?.sharees?.find((value) => value.email === email)
|
||||
) {
|
||||
setFieldError('email', constants.ALREADY_SHARED(email));
|
||||
} else {
|
||||
await shareCollection(collection, email);
|
||||
await sleep(2000);
|
||||
await galleryContext.syncWithRemote(false, true);
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMessage = handleSharingErrors(e);
|
||||
setFieldError('email', errorMessage);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<SingleInputForm
|
||||
callback={collectionShare}
|
||||
placeholder={constants.ENTER_EMAIL}
|
||||
fieldType="email"
|
||||
buttonText={constants.SHARE}
|
||||
customSubmitButton={CollectionShareSubmitButton}
|
||||
/>
|
||||
<CollectionShareSharees collection={collection} />
|
||||
</>
|
||||
);
|
||||
}
|
44
src/components/Collections/CollectionShare/index.tsx
Normal file
44
src/components/Collections/CollectionShare/index.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import EmailShare from './emailShare';
|
||||
import React from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Collection } from 'types/collection';
|
||||
import { dialogCloseHandler } from 'components/DialogBox/base';
|
||||
import DialogTitleWithCloseButton from 'components/DialogBox/titleWithCloseButton';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import { Divider } from '@mui/material';
|
||||
|
||||
import { CollectionShareContainer } from './container';
|
||||
import PublicShare from './publicShare';
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
collection: Collection;
|
||||
}
|
||||
|
||||
function CollectionShare(props: Props) {
|
||||
const handleClose = dialogCloseHandler({
|
||||
staticBackdrop: true,
|
||||
onClose: props.onHide,
|
||||
});
|
||||
|
||||
if (!props.collection) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CollectionShareContainer open={props.show} onClose={handleClose}>
|
||||
<DialogTitleWithCloseButton onClose={handleClose}>
|
||||
{constants.SHARE_COLLECTION}
|
||||
</DialogTitleWithCloseButton>
|
||||
<DialogContent>
|
||||
<EmailShare collection={props.collection} />
|
||||
<Divider />
|
||||
<PublicShare collection={props.collection} />
|
||||
</DialogContent>
|
||||
</CollectionShareContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default CollectionShare;
|
|
@ -0,0 +1,105 @@
|
|||
import { Box, Typography } from '@mui/material';
|
||||
import { FlexWrapper } from 'components/Container';
|
||||
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext } from 'react';
|
||||
import {
|
||||
createShareableURL,
|
||||
deleteShareableURL,
|
||||
} from 'services/collectionService';
|
||||
import { appendCollectionKeyToShareURL } from 'utils/collection';
|
||||
import { handleSharingErrors } from 'utils/error';
|
||||
import constants from 'utils/strings/constants';
|
||||
import PublicShareSwitch from './switch';
|
||||
export default function PublicShareControl({
|
||||
publicShareUrl,
|
||||
sharableLinkError,
|
||||
collection,
|
||||
setPublicShareUrl,
|
||||
setSharableLinkError,
|
||||
}) {
|
||||
const appContext = useContext(AppContext);
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
const createSharableURLHelper = async () => {
|
||||
try {
|
||||
appContext.startLoading();
|
||||
const publicURL = await createShareableURL(collection);
|
||||
const sharableURL = await appendCollectionKeyToShareURL(
|
||||
publicURL.url,
|
||||
collection.key
|
||||
);
|
||||
setPublicShareUrl(sharableURL);
|
||||
galleryContext.syncWithRemote(false, true);
|
||||
} catch (e) {
|
||||
const errorMessage = handleSharingErrors(e);
|
||||
setSharableLinkError(errorMessage);
|
||||
} finally {
|
||||
appContext.finishLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const disablePublicSharing = async () => {
|
||||
try {
|
||||
appContext.startLoading();
|
||||
await deleteShareableURL(collection);
|
||||
setPublicShareUrl(null);
|
||||
galleryContext.syncWithRemote(false, true);
|
||||
} catch (e) {
|
||||
const errorMessage = handleSharingErrors(e);
|
||||
setSharableLinkError(errorMessage);
|
||||
} finally {
|
||||
appContext.finishLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDisablePublicSharing = () => {
|
||||
appContext.setDialogMessage({
|
||||
title: constants.DISABLE_PUBLIC_SHARING,
|
||||
content: constants.DISABLE_PUBLIC_SHARING_MESSAGE,
|
||||
close: { text: constants.CANCEL },
|
||||
proceed: {
|
||||
text: constants.DISABLE,
|
||||
action: disablePublicSharing,
|
||||
variant: ButtonVariant.danger,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCollectionPublicSharing = () => {
|
||||
setSharableLinkError(null);
|
||||
|
||||
if (publicShareUrl) {
|
||||
confirmDisablePublicSharing();
|
||||
} else {
|
||||
createSharableURLHelper();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box mt={3}>
|
||||
<FlexWrapper>
|
||||
<FlexWrapper>{constants.PUBLIC_SHARING}</FlexWrapper>
|
||||
|
||||
<PublicShareSwitch
|
||||
color="accent"
|
||||
sx={{
|
||||
ml: 2,
|
||||
}}
|
||||
checked={!!publicShareUrl}
|
||||
onChange={handleCollectionPublicSharing}
|
||||
/>
|
||||
</FlexWrapper>
|
||||
{sharableLinkError && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: (theme) => theme.palette.danger.main,
|
||||
mt: 0.5,
|
||||
}}>
|
||||
{sharableLinkError}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import { Box, Typography, Divider } from '@mui/material';
|
||||
import { components } from 'react-select';
|
||||
|
||||
const { Option } = components;
|
||||
|
||||
export const OptionWithDivider = (props) => (
|
||||
<Option {...props}>
|
||||
<LabelWithDivider data={props.data} />
|
||||
</Option>
|
||||
);
|
||||
export const LabelWithDivider = ({ data }) => (
|
||||
<>
|
||||
<Box className="main" px={3} py={1}>
|
||||
<Typography>{data.label}</Typography>
|
||||
</Box>
|
||||
<Divider />
|
||||
</>
|
||||
);
|
|
@ -0,0 +1,52 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { PublicURL } from 'types/collection';
|
||||
import { appendCollectionKeyToShareURL } from 'utils/collection';
|
||||
import PublicShareControl from './control';
|
||||
import PublicShareLink from './link';
|
||||
import PublicShareManage from './manage';
|
||||
|
||||
export default function PublicShare({ collection }) {
|
||||
const [sharableLinkError, setSharableLinkError] = useState(null);
|
||||
const [publicShareUrl, setPublicShareUrl] = useState<string>(null);
|
||||
const [publicShareProp, setPublicShareProp] = useState<PublicURL>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
if (collection?.publicURLs?.[0]?.url) {
|
||||
const t = await appendCollectionKeyToShareURL(
|
||||
collection?.publicURLs?.[0]?.url,
|
||||
collection.key
|
||||
);
|
||||
setPublicShareUrl(t);
|
||||
setPublicShareProp(collection?.publicURLs?.[0] as PublicURL);
|
||||
} else {
|
||||
setPublicShareUrl(null);
|
||||
setPublicShareProp(null);
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, [collection]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PublicShareControl
|
||||
setPublicShareUrl={setPublicShareUrl}
|
||||
collection={collection}
|
||||
publicShareUrl={publicShareUrl}
|
||||
sharableLinkError={sharableLinkError}
|
||||
setSharableLinkError={setSharableLinkError}
|
||||
/>
|
||||
{publicShareUrl && (
|
||||
<PublicShareLink publicShareUrl={publicShareUrl} />
|
||||
)}
|
||||
{publicShareProp && (
|
||||
<PublicShareManage
|
||||
publicShareProp={publicShareProp}
|
||||
collection={collection}
|
||||
setPublicShareProp={setPublicShareProp}
|
||||
setSharableLinkError={setSharableLinkError}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { Box } from '@mui/material';
|
||||
import CodeBlock from 'components/CodeBlock';
|
||||
import React from 'react';
|
||||
|
||||
export default function PublicShareLink({ publicShareUrl }) {
|
||||
return (
|
||||
<Box mt={2} mb={3}>
|
||||
<CodeBlock wordBreak={'break-all'} code={publicShareUrl} />
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { Box, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { getDeviceLimitOptions } from 'utils/collection';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { DropdownStyle } from '../../styles';
|
||||
import { OptionWithDivider } from '../customSelectComponents';
|
||||
export function ManageDeviceLimit({
|
||||
publicShareProp,
|
||||
collection,
|
||||
updatePublicShareURLHelper,
|
||||
}) {
|
||||
const updateDeviceLimit = async (newLimit: number) => {
|
||||
return updatePublicShareURLHelper({
|
||||
collectionID: collection.id,
|
||||
deviceLimit: newLimit,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography>{constants.LINK_DEVICE_LIMIT}</Typography>
|
||||
<Select
|
||||
menuPosition="fixed"
|
||||
options={getDeviceLimitOptions()}
|
||||
components={{
|
||||
Option: OptionWithDivider,
|
||||
}}
|
||||
isSearchable={false}
|
||||
value={{
|
||||
label: publicShareProp?.deviceLimit.toString(),
|
||||
value: publicShareProp?.deviceLimit,
|
||||
}}
|
||||
onChange={(e) => updateDeviceLimit(e.value)}
|
||||
styles={DropdownStyle}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import { Box, Typography } from '@mui/material';
|
||||
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import PublicShareSwitch from '../switch';
|
||||
export function ManageDownloadAccess({
|
||||
publicShareProp,
|
||||
updatePublicShareURLHelper,
|
||||
collection,
|
||||
}) {
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
const handleFileDownloadSetting = () => {
|
||||
if (publicShareProp.enableDownload) {
|
||||
disableFileDownload();
|
||||
} else {
|
||||
updatePublicShareURLHelper({
|
||||
collectionID: collection.id,
|
||||
enableDownload: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const disableFileDownload = () => {
|
||||
appContext.setDialogMessage({
|
||||
title: constants.DISABLE_FILE_DOWNLOAD,
|
||||
content: constants.DISABLE_FILE_DOWNLOAD_MESSAGE,
|
||||
close: { text: constants.CANCEL },
|
||||
proceed: {
|
||||
text: constants.DISABLE,
|
||||
action: () =>
|
||||
updatePublicShareURLHelper({
|
||||
collectionID: collection.id,
|
||||
enableDownload: false,
|
||||
}),
|
||||
variant: ButtonVariant.danger,
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Box>
|
||||
<Typography>{constants.FILE_DOWNLOAD}</Typography>
|
||||
<PublicShareSwitch
|
||||
checked={publicShareProp?.enableDownload ?? false}
|
||||
onChange={handleFileDownloadSetting}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import { ManageLinkPassword } from './linkPassword';
|
||||
import { ManageDeviceLimit } from './deviceLimit';
|
||||
import { ManageLinkExpiry } from './linkExpiry';
|
||||
import { PublicLinkSetPassword } from '../setPassword';
|
||||
import { Stack } from '@mui/material';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { updateShareableURL } from 'services/collectionService';
|
||||
import { UpdatePublicURL } from 'types/collection';
|
||||
import { sleep } from 'utils/common';
|
||||
import { handleSharingErrors } from 'utils/error';
|
||||
import constants from 'utils/strings/constants';
|
||||
import {
|
||||
ManageSectionLabel,
|
||||
ManageSectionOptions,
|
||||
} from '../../styledComponents';
|
||||
import { ManageDownloadAccess } from './downloadAcess';
|
||||
|
||||
export default function PublicShareManage({
|
||||
publicShareProp,
|
||||
collection,
|
||||
setPublicShareProp,
|
||||
setSharableLinkError,
|
||||
}) {
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
const [changePasswordView, setChangePasswordView] = useState(false);
|
||||
|
||||
const closeConfigurePassword = () => setChangePasswordView(false);
|
||||
|
||||
const updatePublicShareURLHelper = async (req: UpdatePublicURL) => {
|
||||
try {
|
||||
galleryContext.setBlockingLoad(true);
|
||||
const response = await updateShareableURL(req);
|
||||
setPublicShareProp(response);
|
||||
galleryContext.syncWithRemote(false, true);
|
||||
} catch (e) {
|
||||
const errorMessage = handleSharingErrors(e);
|
||||
setSharableLinkError(errorMessage);
|
||||
} finally {
|
||||
galleryContext.setBlockingLoad(false);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToEnd = (e) => {
|
||||
const lastOptionRow: Element =
|
||||
e.currentTarget.nextElementSibling.lastElementChild;
|
||||
const main = async (lastOptionRow: Element) => {
|
||||
await sleep(0);
|
||||
lastOptionRow.scrollIntoView(true);
|
||||
};
|
||||
main(lastOptionRow);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<details>
|
||||
<ManageSectionLabel onClick={scrollToEnd}>
|
||||
{constants.MANAGE_LINK}
|
||||
</ManageSectionLabel>
|
||||
<ManageSectionOptions>
|
||||
<Stack spacing={1}>
|
||||
<ManageLinkExpiry
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
/>
|
||||
<ManageDeviceLimit
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
/>
|
||||
<ManageDownloadAccess
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
/>
|
||||
<ManageLinkPassword
|
||||
setChangePasswordView={setChangePasswordView}
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</ManageSectionOptions>
|
||||
</details>
|
||||
<PublicLinkSetPassword
|
||||
open={changePasswordView}
|
||||
onClose={closeConfigurePassword}
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={updatePublicShareURLHelper}
|
||||
setChangePasswordView={setChangePasswordView}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import { Box, Typography } from '@mui/material';
|
||||
import { DropdownStyle } from 'components/Collections/CollectionShare/styles';
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { shareExpiryOptions } from 'utils/collection';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { dateStringWithMMH } from 'utils/time';
|
||||
import { OptionWithDivider } from '../customSelectComponents';
|
||||
|
||||
const linkExpiryStyle = {
|
||||
...DropdownStyle,
|
||||
placeholder: (style) => ({
|
||||
...style,
|
||||
color: '#d1d1d1',
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
}),
|
||||
};
|
||||
|
||||
export function ManageLinkExpiry({
|
||||
publicShareProp,
|
||||
collection,
|
||||
updatePublicShareURLHelper,
|
||||
}) {
|
||||
const updateDeviceExpiry = async (optionFn) => {
|
||||
return updatePublicShareURLHelper({
|
||||
collectionID: collection.id,
|
||||
validTill: optionFn(),
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Box>
|
||||
<Typography>{constants.LINK_EXPIRY}</Typography>
|
||||
<Select
|
||||
menuPosition="fixed"
|
||||
options={shareExpiryOptions}
|
||||
isSearchable={false}
|
||||
value={null}
|
||||
components={{
|
||||
Option: OptionWithDivider,
|
||||
}}
|
||||
placeholder={
|
||||
publicShareProp?.validTill
|
||||
? dateStringWithMMH(publicShareProp?.validTill)
|
||||
: 'never'
|
||||
}
|
||||
onChange={(e) => {
|
||||
updateDeviceExpiry(e.value);
|
||||
}}
|
||||
styles={linkExpiryStyle}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { Box, Typography } from '@mui/material';
|
||||
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import PublicShareSwitch from '../switch';
|
||||
export function ManageLinkPassword({
|
||||
collection,
|
||||
publicShareProp,
|
||||
updatePublicShareURLHelper,
|
||||
setChangePasswordView,
|
||||
}) {
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
const handlePasswordChangeSetting = async () => {
|
||||
if (publicShareProp.passwordEnabled) {
|
||||
await confirmDisablePublicUrlPassword();
|
||||
} else {
|
||||
setChangePasswordView(true);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDisablePublicUrlPassword = async () => {
|
||||
appContext.setDialogMessage({
|
||||
title: constants.DISABLE_PASSWORD,
|
||||
content: constants.DISABLE_PASSWORD_MESSAGE,
|
||||
close: { text: constants.CANCEL },
|
||||
proceed: {
|
||||
text: constants.DISABLE,
|
||||
action: () =>
|
||||
updatePublicShareURLHelper({
|
||||
collectionID: collection.id,
|
||||
disablePassword: true,
|
||||
}),
|
||||
variant: ButtonVariant.danger,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography> {constants.LINK_PASSWORD_LOCK}</Typography>
|
||||
<PublicShareSwitch
|
||||
checked={!!publicShareProp?.passwordEnabled}
|
||||
onChange={handlePasswordChangeSetting}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import DialogBox from 'components/DialogBox';
|
||||
import SingleInputForm from 'components/SingleInputForm';
|
||||
import React from 'react';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import constants from 'utils/strings/constants';
|
||||
export function PublicLinkSetPassword({
|
||||
open,
|
||||
onClose,
|
||||
collection,
|
||||
publicShareProp,
|
||||
updatePublicShareURLHelper,
|
||||
setChangePasswordView,
|
||||
}) {
|
||||
const savePassword = async (passphrase, setFieldError) => {
|
||||
if (passphrase && passphrase.trim().length >= 1) {
|
||||
await enablePublicUrlPassword(passphrase);
|
||||
setChangePasswordView(false);
|
||||
publicShareProp.passwordEnabled = true;
|
||||
} else {
|
||||
setFieldError('linkPassword', 'can not be empty');
|
||||
}
|
||||
};
|
||||
|
||||
const enablePublicUrlPassword = async (password: string) => {
|
||||
const cryptoWorker = await new CryptoWorker();
|
||||
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
|
||||
const kek = await cryptoWorker.deriveInteractiveKey(password, kekSalt);
|
||||
|
||||
return updatePublicShareURLHelper({
|
||||
collectionID: collection.id,
|
||||
passHash: kek.key,
|
||||
nonce: kekSalt,
|
||||
opsLimit: kek.opsLimit,
|
||||
memLimit: kek.memLimit,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<DialogBox
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
PaperProps={{ sx: { maxWidth: '350px' } }}
|
||||
titleCloseButton
|
||||
attributes={{
|
||||
title: constants.PASSWORD_LOCK,
|
||||
}}>
|
||||
<SingleInputForm
|
||||
callback={savePassword}
|
||||
placeholder={constants.RETURN_PASSPHRASE_HINT}
|
||||
buttonText={constants.LOCK}
|
||||
fieldType="password"
|
||||
/>
|
||||
</DialogBox>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
import { SwitchProps, Switch } from '@mui/material';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const PublicShareSwitch = styled((props: SwitchProps) => (
|
||||
<Switch
|
||||
focusVisibleClassName=".Mui-focusVisible"
|
||||
disableRipple
|
||||
{...props}
|
||||
/>
|
||||
))(({ theme }) => ({
|
||||
width: 40,
|
||||
height: 24,
|
||||
padding: 0,
|
||||
'& .MuiSwitch-switchBase': {
|
||||
padding: 0,
|
||||
margin: 2,
|
||||
transitionDuration: '300ms',
|
||||
'&.Mui-checked': {
|
||||
transform: 'translateX(16px)',
|
||||
color: '#fff',
|
||||
'& + .MuiSwitch-track': {
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark' ? '#2ECA45' : '#65C466',
|
||||
opacity: 1,
|
||||
border: 0,
|
||||
},
|
||||
'&.Mui-disabled + .MuiSwitch-track': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
'&.Mui-focusVisible .MuiSwitch-thumb': {
|
||||
color: '#33cf4d',
|
||||
border: '6px solid #fff',
|
||||
},
|
||||
'&.Mui-disabled .MuiSwitch-thumb': {
|
||||
color:
|
||||
theme.palette.mode === 'light'
|
||||
? theme.palette.grey[100]
|
||||
: theme.palette.grey[600],
|
||||
},
|
||||
'&.Mui-disabled + .MuiSwitch-track': {
|
||||
opacity: theme.palette.mode === 'light' ? 0.7 : 0.3,
|
||||
},
|
||||
},
|
||||
'& .MuiSwitch-thumb': {
|
||||
boxSizing: 'border-box',
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
'& .MuiSwitch-track': {
|
||||
borderRadius: 22 / 2,
|
||||
backgroundColor: theme.palette.mode === 'light' ? '#E9E9EA' : '#39393D',
|
||||
opacity: 1,
|
||||
transition: theme.transitions.create(['background-color'], {
|
||||
duration: 500,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
export default PublicShareSwitch;
|
48
src/components/Collections/CollectionShare/sharees/index.tsx
Normal file
48
src/components/Collections/CollectionShare/sharees/index.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Box, Typography } from '@mui/material';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext } from 'react';
|
||||
import { unshareCollection } from 'services/collectionService';
|
||||
import { Collection } from 'types/collection';
|
||||
import { sleep } from 'utils/common';
|
||||
import constants from 'utils/strings/constants';
|
||||
import ShareeRow from './row';
|
||||
|
||||
interface Iprops {
|
||||
collection: Collection;
|
||||
}
|
||||
|
||||
export function CollectionShareSharees({ collection }: Iprops) {
|
||||
const appContext = useContext(AppContext);
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
const collectionUnshare = async (sharee) => {
|
||||
try {
|
||||
appContext.startLoading();
|
||||
await unshareCollection(collection, sharee.email);
|
||||
await sleep(2000);
|
||||
await galleryContext.syncWithRemote(false, true);
|
||||
} finally {
|
||||
appContext.finishLoading();
|
||||
}
|
||||
};
|
||||
|
||||
if (!collection.sharees?.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box mb={3}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{constants.SHAREES}
|
||||
</Typography>
|
||||
{collection.sharees?.map((sharee) => (
|
||||
<ShareeRow
|
||||
key={sharee.email}
|
||||
sharee={sharee}
|
||||
collectionUnshare={collectionUnshare}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
22
src/components/Collections/CollectionShare/sharees/row.tsx
Normal file
22
src/components/Collections/CollectionShare/sharees/row.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { SpaceBetweenFlex } from 'components/Container';
|
||||
import { User } from 'types/user';
|
||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||
|
||||
interface IProps {
|
||||
sharee: User;
|
||||
collectionUnshare: (sharee: User) => void;
|
||||
}
|
||||
const ShareeRow = ({ sharee, collectionUnshare }: IProps) => (
|
||||
<SpaceBetweenFlex>
|
||||
{sharee.email}
|
||||
<IconButton
|
||||
sx={{ ml: 2, color: 'text.secondary' }}
|
||||
onClick={() => collectionUnshare(sharee)}>
|
||||
<MoreHorizIcon />
|
||||
</IconButton>
|
||||
</SpaceBetweenFlex>
|
||||
);
|
||||
|
||||
export default ShareeRow;
|
|
@ -0,0 +1,14 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const ManageSectionLabel = styled.summary(
|
||||
({ theme }) => `
|
||||
text-align: center;
|
||||
margin-bottom:${theme.spacing(1)};
|
||||
`
|
||||
);
|
||||
|
||||
export const ManageSectionOptions = styled.section(
|
||||
({ theme }) => `
|
||||
margin-bottom:${theme.spacing(4)};
|
||||
`
|
||||
);
|
20
src/components/Collections/CollectionShare/styles.tsx
Normal file
20
src/components/Collections/CollectionShare/styles.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { SelectStyles } from 'components/Search/styles';
|
||||
|
||||
export const DropdownStyle = {
|
||||
...SelectStyles,
|
||||
dropdownIndicator: (style) => ({
|
||||
...style,
|
||||
margin: '0px',
|
||||
}),
|
||||
singleValue: (style) => ({
|
||||
...style,
|
||||
color: '#d1d1d1',
|
||||
width: '240px',
|
||||
}),
|
||||
control: (style, { isFocused }) => ({
|
||||
...style,
|
||||
...SelectStyles.control(style, { isFocused }),
|
||||
minWidth: '240px',
|
||||
paddingLeft: '8px',
|
||||
}),
|
||||
};
|
10
src/components/Collections/CollectionShare/submitButton.tsx
Normal file
10
src/components/Collections/CollectionShare/submitButton.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { FlexWrapper } from 'components/Container';
|
||||
import SubmitButton, { SubmitButtonProps } from 'components/SubmitButton';
|
||||
import React from 'react';
|
||||
export default function CollectionShareSubmitButton(props: SubmitButtonProps) {
|
||||
return (
|
||||
<FlexWrapper style={{ justifyContent: 'flex-end' }}>
|
||||
<SubmitButton {...props} size="medium" inline sx={{ my: 2 }} />
|
||||
</FlexWrapper>
|
||||
);
|
||||
}
|
|
@ -27,7 +27,7 @@ export default function Collections(props: Iprops) {
|
|||
|
||||
const [allCollectionView, setAllCollectionView] = useState(false);
|
||||
const [collectionShareModalView, setCollectionShareModalView] =
|
||||
useState(false);
|
||||
useState(true);
|
||||
const collectionsMap = useRef<Map<number, Collection>>(new Map());
|
||||
const activeCollection = useRef<Collection>(null);
|
||||
|
||||
|
|
|
@ -22,7 +22,12 @@ type IProps = React.PropsWithChildren<
|
|||
}
|
||||
>;
|
||||
|
||||
export default function DialogBox({ attributes, children, ...props }: IProps) {
|
||||
export default function DialogBox({
|
||||
attributes,
|
||||
children,
|
||||
titleCloseButton,
|
||||
...props
|
||||
}: IProps) {
|
||||
if (!attributes) {
|
||||
return <Dialog open={false} />;
|
||||
}
|
||||
|
@ -41,7 +46,7 @@ export default function DialogBox({ attributes, children, ...props }: IProps) {
|
|||
{...props}>
|
||||
{attributes.title && (
|
||||
<DialogTitleWithCloseButton
|
||||
onClose={props.titleCloseButton && handleClose}>
|
||||
onClose={titleCloseButton && handleClose}>
|
||||
{attributes.title}
|
||||
</DialogTitleWithCloseButton>
|
||||
)}
|
||||
|
|
|
@ -32,7 +32,7 @@ export const SelectStyles = {
|
|||
cursor: 'pointer',
|
||||
},
|
||||
'& .main': {
|
||||
backgroundColor: isFocused && '#343434',
|
||||
backgroundColor: isFocused && '#202020',
|
||||
},
|
||||
'&:last-child .MuiDivider-root': {
|
||||
display: 'none',
|
||||
|
|
|
@ -5,6 +5,7 @@ import * as Yup from 'yup';
|
|||
import SubmitButton from './SubmitButton';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import ShowHidePassword from './Form/ShowHidePassword';
|
||||
import { FlexWrapper } from './Container';
|
||||
|
||||
interface formValues {
|
||||
inputValue: string;
|
||||
|
@ -17,6 +18,7 @@ export interface SingleInputFormProps {
|
|||
fieldType: 'text' | 'email' | 'password';
|
||||
placeholder: string;
|
||||
buttonText: string;
|
||||
customSubmitButton?: any;
|
||||
}
|
||||
|
||||
export default function SingleInputForm(props: SingleInputFormProps) {
|
||||
|
@ -97,12 +99,19 @@ export default function SingleInputForm(props: SingleInputFormProps) {
|
|||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<SubmitButton
|
||||
sx={{ mt: 2 }}
|
||||
buttonText={props.buttonText}
|
||||
loading={loading}
|
||||
/>
|
||||
<FlexWrapper></FlexWrapper>
|
||||
{props.customSubmitButton ? (
|
||||
<props.customSubmitButton
|
||||
buttonText={props.buttonText}
|
||||
loading={loading}
|
||||
/>
|
||||
) : (
|
||||
<SubmitButton
|
||||
sx={{ mt: 2 }}
|
||||
buttonText={props.buttonText}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Button, ButtonProps, CircularProgress } from '@mui/material';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
interface Props {
|
||||
export interface SubmitButtonProps {
|
||||
loading: boolean;
|
||||
buttonText: string;
|
||||
inline?: any;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const SubmitButton: FC<ButtonProps<'button', Props>> = ({
|
||||
const SubmitButton: FC<ButtonProps<'button', SubmitButtonProps>> = ({
|
||||
loading,
|
||||
buttonText,
|
||||
inline,
|
||||
|
|
|
@ -488,6 +488,3 @@ div.otp-input input:not(:placeholder-shown) , div.otp-input input:focus{
|
|||
}
|
||||
}
|
||||
|
||||
.manageLinkHeader:hover{
|
||||
color:#bbb;
|
||||
}
|
|
@ -44,6 +44,12 @@ declare module '@mui/material/Typography' {
|
|||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Switch' {
|
||||
interface SwitchPropsColorOverrides {
|
||||
accent: true;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a theme instance.
|
||||
const darkThemeOptions = createTheme({
|
||||
components: {
|
||||
|
@ -123,6 +129,13 @@ const darkThemeOptions = createTheme({
|
|||
},
|
||||
},
|
||||
},
|
||||
MuiTypography: {
|
||||
styleOverrides: {
|
||||
body1: {
|
||||
paddingBottom: '4px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
palette: {
|
||||
|
|
|
@ -129,10 +129,8 @@ const _intSelectOption = (i: number) => {
|
|||
return { label: i.toString(), value: i };
|
||||
};
|
||||
|
||||
export function selectIntOptions(upperLimit: number) {
|
||||
return [...Array(upperLimit).reverse().keys()].map((i) =>
|
||||
_intSelectOption(i + 1)
|
||||
);
|
||||
export function getDeviceLimitOptions() {
|
||||
return [2, 5, 10, 25, 50].map((i) => _intSelectOption(i));
|
||||
}
|
||||
|
||||
export const shareExpiryOptions = [
|
||||
|
|
|
@ -50,7 +50,7 @@ const englishConstants = {
|
|||
NAME: 'name',
|
||||
ENTER_NAME: 'your name',
|
||||
EMAIL: 'email',
|
||||
ENTER_EMAIL: 'email',
|
||||
ENTER_EMAIL: 'Email',
|
||||
DATA_DISCLAIMER: "we'll never share your data with anyone else.",
|
||||
SUBMIT: 'submit',
|
||||
EMAIL_ERROR: 'enter a valid email',
|
||||
|
@ -672,7 +672,7 @@ const englishConstants = {
|
|||
REPORT_SUBMIT_FAILED: 'failed to sent report, try again',
|
||||
INSTALL: 'install',
|
||||
ALBUM_URL: 'album url',
|
||||
PUBLIC_SHARING: 'link sharing',
|
||||
PUBLIC_SHARING: 'Public link',
|
||||
NOT_FOUND: '404 - not found',
|
||||
LINK_EXPIRED: 'this link has either expired or been disabled!',
|
||||
MANAGE_LINK: 'manage link',
|
||||
|
@ -680,8 +680,8 @@ const englishConstants = {
|
|||
DISABLE_PUBLIC_SHARING: "'disable public sharing",
|
||||
DISABLE_PUBLIC_SHARING_MESSAGE:
|
||||
'are you sure you want to disable public sharing?',
|
||||
FILE_DOWNLOAD: 'file download',
|
||||
LINK_PASSWORD_LOCK: 'password lock',
|
||||
FILE_DOWNLOAD: 'Allow downloads',
|
||||
LINK_PASSWORD_LOCK: 'Password lock',
|
||||
LINK_DEVICE_LIMIT: 'device limit',
|
||||
LINK_EXPIRY: 'link expiry',
|
||||
LINK_EXPIRY_NEVER: 'never',
|
||||
|
|
Loading…
Reference in a new issue