Share album redesign (#1210)
This commit is contained in:
commit
2307e8b093
|
@ -355,6 +355,33 @@
|
||||||
"INSTALL": "Install",
|
"INSTALL": "Install",
|
||||||
"SHARING_DETAILS": "Sharing details",
|
"SHARING_DETAILS": "Sharing details",
|
||||||
"MODIFY_SHARING": "Modify sharing",
|
"MODIFY_SHARING": "Modify sharing",
|
||||||
|
"ADD_COLLABORATORS": "Add collaborators",
|
||||||
|
"ADD_NEW_EMAIL": "Add a new email",
|
||||||
|
"shared_with_people_zero" :"Share with specific people",
|
||||||
|
"shared_with_people_one": "Shared with 1 person",
|
||||||
|
"shared_with_people_other": "Shared with {{count, number}} people",
|
||||||
|
"participants_zero": "No participants",
|
||||||
|
"participants_one": "1 participant",
|
||||||
|
"participants_other": "{{count, number}} participants",
|
||||||
|
"ADD_VIEWERS":"Add viewers",
|
||||||
|
"PARTICIPANTS": "Participants",
|
||||||
|
"CHANGE_PERMISSIONS_TO_VIEWER": "<p>{{selectedEmail}} will not be able to add more photos to the album</p> <p>They will still be able to remove photos added by them</p>",
|
||||||
|
"CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} will be able to add photos to the album",
|
||||||
|
"CONVERT_TO_VIEWER": "Yes, convert to viewer",
|
||||||
|
"CONVERT_TO_COLLABORATOR": "Yes, convert to collaborator",
|
||||||
|
"CHANGE_PERMISSION":"Change permission?",
|
||||||
|
"REMOVE_PARTICIPANT": "Remove?",
|
||||||
|
"CONFIRM_REMOVE":"Yes, remove",
|
||||||
|
"MANAGE":"Manage",
|
||||||
|
"ADDED_AS":"Added as",
|
||||||
|
"COLLABORATOR_RIGHTS":"Collaborators can add photos and videos to the shared album",
|
||||||
|
"REMOVE_PARTICIPANT_HEAD": "Remove participant",
|
||||||
|
"OWNER": "Owner",
|
||||||
|
"COLLABORATORS": "Collaborators",
|
||||||
|
"ADD_MORE": "Add more",
|
||||||
|
"VIEWERS": "Viewers",
|
||||||
|
"OR_ADD_EXISTING": "Or pick an existing one",
|
||||||
|
"REMOVE_PARTICIPANT_MESSAGE": "<p>{{selectedEmail}} will be removed from the album</p> <p>Any photos added by them will also be removed from the album</p>",
|
||||||
"NOT_FOUND": "404 - not found",
|
"NOT_FOUND": "404 - not found",
|
||||||
"LINK_EXPIRED": "Link expired",
|
"LINK_EXPIRED": "Link expired",
|
||||||
"LINK_EXPIRED_MESSAGE": "This link has either expired or been disabled!",
|
"LINK_EXPIRED_MESSAGE": "This link has either expired or been disabled!",
|
||||||
|
@ -490,7 +517,6 @@
|
||||||
},
|
},
|
||||||
"COPY_LINK": "Copy link",
|
"COPY_LINK": "Copy link",
|
||||||
"DONE": "Done",
|
"DONE": "Done",
|
||||||
"ADD_EMAIL_TITLE": "Share with specific people",
|
|
||||||
"LINK_SHARE_TITLE": "Or share a link",
|
"LINK_SHARE_TITLE": "Or share a link",
|
||||||
"REMOVE_LINK": "Remove link",
|
"REMOVE_LINK": "Remove link",
|
||||||
"CREATE_PUBLIC_SHARING": "Create public link",
|
"CREATE_PUBLIC_SHARING": "Create public link",
|
||||||
|
|
|
@ -20,12 +20,15 @@ export function ShareQuickOption({
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
// collectionSummaryType === CollectionSummaryType.incomingShare
|
|
||||||
// ? t('SHARING_DETAILS')
|
|
||||||
// :
|
|
||||||
collectionSummaryType === CollectionSummaryType.outgoingShare ||
|
|
||||||
collectionSummaryType ===
|
collectionSummaryType ===
|
||||||
CollectionSummaryType.sharedOnlyViaLink
|
CollectionSummaryType.incomingShareViewer ||
|
||||||
|
collectionSummaryType ===
|
||||||
|
CollectionSummaryType.incomingShareCollaborator
|
||||||
|
? t('SHARING_DETAILS')
|
||||||
|
: collectionSummaryType ===
|
||||||
|
CollectionSummaryType.outgoingShare ||
|
||||||
|
collectionSummaryType ===
|
||||||
|
CollectionSummaryType.sharedOnlyViaLink
|
||||||
? t('MODIFY_SHARING')
|
? t('MODIFY_SHARING')
|
||||||
: t('SHARE_COLLECTION')
|
: t('SHARE_COLLECTION')
|
||||||
}>
|
}>
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
import SingleInputAutocomplete, {
|
|
||||||
SingleInputAutocompleteProps,
|
|
||||||
} from 'components/SingleInputAutocomplete';
|
|
||||||
import { GalleryContext } from 'pages/gallery';
|
|
||||||
import React, { useContext, useState, useEffect } from 'react';
|
|
||||||
import { t } from 'i18next';
|
|
||||||
import { shareCollection } from 'services/collectionService';
|
|
||||||
import { User } from 'types/user';
|
|
||||||
import { handleSharingErrors } from 'utils/error/ui';
|
|
||||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
|
||||||
import { CollectionShareSharees } from './sharees';
|
|
||||||
import { getLocalCollections } from 'services/collectionService';
|
|
||||||
import { getLocalFamilyData } from 'utils/user/family';
|
|
||||||
|
|
||||||
export default function EmailShare({ collection }) {
|
|
||||||
const galleryContext = useContext(GalleryContext);
|
|
||||||
|
|
||||||
const [updatedOptionsList, setUpdatedOptionsList] = useState(['']);
|
|
||||||
let updatedList = [];
|
|
||||||
useEffect(() => {
|
|
||||||
const getUpdatedOptionsList = async () => {
|
|
||||||
const ownerUser: User = getData(LS_KEYS.USER);
|
|
||||||
const collectionList = getLocalCollections();
|
|
||||||
const familyList = getLocalFamilyData();
|
|
||||||
const result = await collectionList;
|
|
||||||
const emails = result.flatMap((item) => {
|
|
||||||
if (item.owner.email && item.owner.id !== ownerUser.id) {
|
|
||||||
return [item.owner.email];
|
|
||||||
} else {
|
|
||||||
const shareeEmails = item.sharees.map(
|
|
||||||
(sharee) => sharee.email
|
|
||||||
);
|
|
||||||
return shareeEmails;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// adding family members
|
|
||||||
if (familyList) {
|
|
||||||
const family = familyList.members.map((member) => member.email);
|
|
||||||
emails.push(...family);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedList = Array.from(new Set(emails));
|
|
||||||
|
|
||||||
const shareeEmailsCollection = collection.sharees.map(
|
|
||||||
(sharees) => sharees.email
|
|
||||||
);
|
|
||||||
const filteredList = updatedList.filter(
|
|
||||||
(email) =>
|
|
||||||
!shareeEmailsCollection.includes(email) &&
|
|
||||||
email !== ownerUser.email
|
|
||||||
);
|
|
||||||
|
|
||||||
setUpdatedOptionsList(filteredList);
|
|
||||||
};
|
|
||||||
|
|
||||||
getUpdatedOptionsList();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const collectionShare: SingleInputAutocompleteProps['callback'] = async (
|
|
||||||
email,
|
|
||||||
setFieldError,
|
|
||||||
resetForm
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const user: User = getData(LS_KEYS.USER);
|
|
||||||
if (email === user.email) {
|
|
||||||
setFieldError(t('SHARE_WITH_SELF'));
|
|
||||||
} else if (
|
|
||||||
collection?.sharees?.find((value) => value.email === email)
|
|
||||||
) {
|
|
||||||
setFieldError(t('ALREADY_SHARED', { email }));
|
|
||||||
} else {
|
|
||||||
await shareCollection(collection, email);
|
|
||||||
await galleryContext.syncWithRemote(false, true);
|
|
||||||
resetForm();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
const errorMessage = handleSharingErrors(e);
|
|
||||||
setFieldError(errorMessage);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SingleInputAutocomplete
|
|
||||||
callback={collectionShare}
|
|
||||||
optionsList={updatedOptionsList}
|
|
||||||
placeholder={t('ENTER_EMAIL')}
|
|
||||||
fieldType="email"
|
|
||||||
buttonText={t('SHARE')}
|
|
||||||
submitButtonProps={{
|
|
||||||
size: 'medium',
|
|
||||||
sx: { mt: 1, mb: 2 },
|
|
||||||
}}
|
|
||||||
disableAutoFocus
|
|
||||||
/>
|
|
||||||
<CollectionShareSharees collection={collection} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { Stack } from '@mui/material';
|
||||||
|
import { COLLECTION_ROLE, Collection } from 'types/collection';
|
||||||
|
import { EnteDrawer } from 'components/EnteDrawer';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { DialogProps } from '@mui/material';
|
||||||
|
import Titlebar from 'components/Titlebar';
|
||||||
|
|
||||||
|
import { GalleryContext } from 'pages/gallery';
|
||||||
|
import { useContext, useMemo } from 'react';
|
||||||
|
import { shareCollection } from 'services/collectionService';
|
||||||
|
import { handleSharingErrors } from 'utils/error/ui';
|
||||||
|
import AddParticipantForm, {
|
||||||
|
AddParticipantFormProps,
|
||||||
|
} from './AddParticipantForm';
|
||||||
|
|
||||||
|
interface Iprops {
|
||||||
|
collection: Collection;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onRootClose: () => void;
|
||||||
|
type: COLLECTION_ROLE.VIEWER | COLLECTION_ROLE.COLLABORATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddParticipant({
|
||||||
|
open,
|
||||||
|
collection,
|
||||||
|
onClose,
|
||||||
|
onRootClose,
|
||||||
|
type,
|
||||||
|
}: Iprops) {
|
||||||
|
const { user, syncWithRemote, emailList } = useContext(GalleryContext);
|
||||||
|
|
||||||
|
const nonSharedEmails = useMemo(
|
||||||
|
() =>
|
||||||
|
emailList.filter(
|
||||||
|
(email) =>
|
||||||
|
!collection.sharees?.find((value) => value.email === email)
|
||||||
|
),
|
||||||
|
[emailList, collection.sharees]
|
||||||
|
);
|
||||||
|
|
||||||
|
const collectionShare: AddParticipantFormProps['callback'] = async (
|
||||||
|
emails,
|
||||||
|
setFieldError,
|
||||||
|
resetForm
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
for (const email of emails) {
|
||||||
|
if (email === user.email) {
|
||||||
|
setFieldError(t('SHARE_WITH_SELF'));
|
||||||
|
break;
|
||||||
|
} else if (
|
||||||
|
collection?.sharees?.find((value) => value.email === email)
|
||||||
|
) {
|
||||||
|
setFieldError(t('ALREADY_SHARED', { email }));
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
await shareCollection(collection, email, type);
|
||||||
|
await syncWithRemote(false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetForm();
|
||||||
|
} catch (e) {
|
||||||
|
const errorMessage = handleSharingErrors(e);
|
||||||
|
setFieldError(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRootClose = () => {
|
||||||
|
onClose();
|
||||||
|
onRootClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrawerClose: DialogProps['onClose'] = (_, reason) => {
|
||||||
|
if (reason === 'backdropClick') {
|
||||||
|
handleRootClose();
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EnteDrawer anchor="right" open={open} onClose={handleDrawerClose}>
|
||||||
|
<Stack spacing={'4px'} py={'12px'}>
|
||||||
|
<Titlebar
|
||||||
|
onClose={onClose}
|
||||||
|
title={
|
||||||
|
type === COLLECTION_ROLE.VIEWER
|
||||||
|
? t('ADD_VIEWERS')
|
||||||
|
: t('ADD_COLLABORATORS')
|
||||||
|
}
|
||||||
|
onRootClose={handleRootClose}
|
||||||
|
caption={collection.name}
|
||||||
|
/>
|
||||||
|
<AddParticipantForm
|
||||||
|
onClose={onClose}
|
||||||
|
callback={collectionShare}
|
||||||
|
optionsList={nonSharedEmails}
|
||||||
|
placeholder={t('ENTER_EMAIL')}
|
||||||
|
fieldType="email"
|
||||||
|
buttonText={
|
||||||
|
type === COLLECTION_ROLE.VIEWER
|
||||||
|
? t('ADD_VIEWERS')
|
||||||
|
: t('ADD_COLLABORATORS')
|
||||||
|
}
|
||||||
|
submitButtonProps={{
|
||||||
|
size: 'large',
|
||||||
|
sx: { mt: 1, mb: 2 },
|
||||||
|
}}
|
||||||
|
disableAutoFocus
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</EnteDrawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,249 @@
|
||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { Formik, FormikHelpers, FormikState } from 'formik';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import SubmitButton from 'components/SubmitButton';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import { FlexWrapper } from 'components/Container';
|
||||||
|
import { Button, FormHelperText, Stack } from '@mui/material';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { MenuItemGroup } from 'components/Menu/MenuItemGroup';
|
||||||
|
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
|
||||||
|
import MenuItemDivider from 'components/Menu/MenuItemDivider';
|
||||||
|
import MenuSectionTitle from 'components/Menu/MenuSectionTitle';
|
||||||
|
import Avatar from 'components/pages/gallery/Avatar';
|
||||||
|
import DoneIcon from '@mui/icons-material/Done';
|
||||||
|
|
||||||
|
interface formValues {
|
||||||
|
inputValue: string;
|
||||||
|
selectedOptions: string[];
|
||||||
|
}
|
||||||
|
export interface AddParticipantFormProps {
|
||||||
|
callback: (
|
||||||
|
emails: string[],
|
||||||
|
setFieldError: (errorMessage: string) => void,
|
||||||
|
resetForm: (nextState?: Partial<FormikState<formValues>>) => void
|
||||||
|
) => Promise<void>;
|
||||||
|
fieldType: 'text' | 'email' | 'password';
|
||||||
|
placeholder: string;
|
||||||
|
buttonText: string;
|
||||||
|
submitButtonProps?: any;
|
||||||
|
initialValue?: string;
|
||||||
|
secondaryButtonAction?: () => void;
|
||||||
|
disableAutoFocus?: boolean;
|
||||||
|
hiddenPreInput?: any;
|
||||||
|
caption?: any;
|
||||||
|
hiddenPostInput?: any;
|
||||||
|
autoComplete?: string;
|
||||||
|
blockButton?: boolean;
|
||||||
|
hiddenLabel?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
optionsList?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddParticipantForm(props: AddParticipantFormProps) {
|
||||||
|
const { submitButtonProps } = props;
|
||||||
|
const { sx: buttonSx, ...restSubmitButtonProps } = submitButtonProps ?? {};
|
||||||
|
const [disableInput, setDisableInput] = useState(false);
|
||||||
|
|
||||||
|
const [loading, SetLoading] = useState(false);
|
||||||
|
|
||||||
|
const submitForm = async (
|
||||||
|
values: formValues,
|
||||||
|
{ setFieldError, resetForm }: FormikHelpers<formValues>
|
||||||
|
) => {
|
||||||
|
SetLoading(true);
|
||||||
|
|
||||||
|
if (values.inputValue !== '') {
|
||||||
|
await props.callback(
|
||||||
|
[values.inputValue],
|
||||||
|
(message) => setFieldError('inputValue', message),
|
||||||
|
resetForm
|
||||||
|
);
|
||||||
|
} else if (values.selectedOptions.length !== 0) {
|
||||||
|
await props.callback(
|
||||||
|
values.selectedOptions,
|
||||||
|
(message) => setFieldError('inputValue', message),
|
||||||
|
resetForm
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisableInput(false);
|
||||||
|
SetLoading(false);
|
||||||
|
|
||||||
|
props.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const validationSchema = useMemo(() => {
|
||||||
|
switch (props.fieldType) {
|
||||||
|
case 'text':
|
||||||
|
return Yup.object().shape({
|
||||||
|
inputValue: Yup.string().required(t('REQUIRED')),
|
||||||
|
});
|
||||||
|
case 'email':
|
||||||
|
return Yup.object().shape({
|
||||||
|
inputValue: Yup.string().email(t('EMAIL_ERROR')),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [props.fieldType]);
|
||||||
|
|
||||||
|
const handleInputFieldClick = (setFieldValue) => {
|
||||||
|
setFieldValue('selectedOptions', []);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik<formValues>
|
||||||
|
initialValues={{
|
||||||
|
inputValue: props.initialValue ?? '',
|
||||||
|
selectedOptions: [],
|
||||||
|
}}
|
||||||
|
onSubmit={submitForm}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
validateOnChange={false}
|
||||||
|
validateOnBlur={false}>
|
||||||
|
{({
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
handleChange,
|
||||||
|
handleSubmit,
|
||||||
|
setFieldValue,
|
||||||
|
}) => (
|
||||||
|
<form noValidate onSubmit={handleSubmit}>
|
||||||
|
<Stack spacing={'24px'} py={'20px'} px={'12px'}>
|
||||||
|
{props.hiddenPreInput}
|
||||||
|
<Stack>
|
||||||
|
<MenuSectionTitle title={t('ADD_NEW_EMAIL')} />
|
||||||
|
<TextField
|
||||||
|
sx={{ marginTop: 0 }}
|
||||||
|
hiddenLabel={props.hiddenLabel}
|
||||||
|
fullWidth
|
||||||
|
type={props.fieldType}
|
||||||
|
id={props.fieldType}
|
||||||
|
onChange={handleChange('inputValue')}
|
||||||
|
onClick={() =>
|
||||||
|
handleInputFieldClick(setFieldValue)
|
||||||
|
}
|
||||||
|
name={props.fieldType}
|
||||||
|
{...(props.hiddenLabel
|
||||||
|
? { placeholder: props.placeholder }
|
||||||
|
: { label: props.placeholder })}
|
||||||
|
error={Boolean(errors.inputValue)}
|
||||||
|
helperText={errors.inputValue}
|
||||||
|
value={values.inputValue}
|
||||||
|
disabled={loading || disableInput}
|
||||||
|
autoFocus={!props.disableAutoFocus}
|
||||||
|
autoComplete={props.autoComplete}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{props.optionsList.length > 0 && (
|
||||||
|
<Stack>
|
||||||
|
<MenuSectionTitle
|
||||||
|
title={t('OR_ADD_EXISTING')}
|
||||||
|
/>
|
||||||
|
<MenuItemGroup>
|
||||||
|
{props.optionsList.map((item, index) => (
|
||||||
|
<>
|
||||||
|
<EnteMenuItem
|
||||||
|
fontWeight="normal"
|
||||||
|
key={item}
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
values.selectedOptions.includes(
|
||||||
|
item
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setFieldValue(
|
||||||
|
'selectedOptions',
|
||||||
|
values.selectedOptions.filter(
|
||||||
|
(
|
||||||
|
selectedOption
|
||||||
|
) =>
|
||||||
|
selectedOption !==
|
||||||
|
item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setFieldValue(
|
||||||
|
'selectedOptions',
|
||||||
|
[
|
||||||
|
...values.selectedOptions,
|
||||||
|
item,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
label={item}
|
||||||
|
startIcon={
|
||||||
|
<Avatar email={item} />
|
||||||
|
}
|
||||||
|
endIcon={
|
||||||
|
values.selectedOptions.includes(
|
||||||
|
item
|
||||||
|
) ? (
|
||||||
|
<DoneIcon />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{index !==
|
||||||
|
props.optionsList.length -
|
||||||
|
1 && <MenuItemDivider />}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</MenuItemGroup>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormHelperText
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
top: errors.inputValue ? '-22px' : '0',
|
||||||
|
float: 'right',
|
||||||
|
padding: '0 8px',
|
||||||
|
}}>
|
||||||
|
{props.caption}
|
||||||
|
</FormHelperText>
|
||||||
|
{props.hiddenPostInput}
|
||||||
|
</Stack>
|
||||||
|
<FlexWrapper
|
||||||
|
px={'8px'}
|
||||||
|
justifyContent={'center'}
|
||||||
|
flexWrap={
|
||||||
|
props.blockButton ? 'wrap-reverse' : 'nowrap'
|
||||||
|
}>
|
||||||
|
<Stack direction={'column'} px={'8px'} width={'100%'}>
|
||||||
|
{props.secondaryButtonAction && (
|
||||||
|
<Button
|
||||||
|
onClick={props.secondaryButtonAction}
|
||||||
|
size="large"
|
||||||
|
color="secondary"
|
||||||
|
sx={{
|
||||||
|
'&&&': {
|
||||||
|
mt: !props.blockButton ? 2 : 0.5,
|
||||||
|
mb: !props.blockButton ? 4 : 0,
|
||||||
|
mr: !props.blockButton ? 1 : 0,
|
||||||
|
...buttonSx,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...restSubmitButtonProps}>
|
||||||
|
{t('CANCEL')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SubmitButton
|
||||||
|
sx={{
|
||||||
|
'&&&': {
|
||||||
|
mt: 2,
|
||||||
|
...buttonSx,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
buttonText={props.buttonText}
|
||||||
|
loading={loading}
|
||||||
|
{...restSubmitButtonProps}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</FlexWrapper>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
import { Stack } from '@mui/material';
|
||||||
|
import { COLLECTION_ROLE, Collection, CollectionUser } from 'types/collection';
|
||||||
|
import { EnteDrawer } from 'components/EnteDrawer';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { DialogProps } from '@mui/material';
|
||||||
|
import Titlebar from 'components/Titlebar';
|
||||||
|
import { useContext, useState } from 'react';
|
||||||
|
import { AppContext } from 'pages/_app';
|
||||||
|
import { GalleryContext } from 'pages/gallery';
|
||||||
|
import { unshareCollection } from 'services/collectionService';
|
||||||
|
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
|
||||||
|
import { MenuItemGroup } from 'components/Menu/MenuItemGroup';
|
||||||
|
import Avatar from 'components/pages/gallery/Avatar';
|
||||||
|
import MenuSectionTitle from 'components/Menu/MenuSectionTitle';
|
||||||
|
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
|
||||||
|
import ModeEditIcon from '@mui/icons-material/ModeEdit';
|
||||||
|
import MenuItemDivider from 'components/Menu/MenuItemDivider';
|
||||||
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||||
|
import ManageParticipant from './ManageParticipant';
|
||||||
|
import AddParticipant from './AddParticipant';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import Add from '@mui/icons-material/Add';
|
||||||
|
import Photo from '@mui/icons-material/Photo';
|
||||||
|
|
||||||
|
interface Iprops {
|
||||||
|
collection: Collection;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onRootClose: () => void;
|
||||||
|
peopleCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ManageEmailShare({
|
||||||
|
open,
|
||||||
|
collection,
|
||||||
|
onClose,
|
||||||
|
onRootClose,
|
||||||
|
peopleCount,
|
||||||
|
}: Iprops) {
|
||||||
|
const appContext = useContext(AppContext);
|
||||||
|
const galleryContext = useContext(GalleryContext);
|
||||||
|
|
||||||
|
const [addParticipantView, setAddParticipantView] = useState(false);
|
||||||
|
const [manageParticipantView, setManageParticipantView] = useState(false);
|
||||||
|
|
||||||
|
const closeAddParticipant = () => setAddParticipantView(false);
|
||||||
|
const openAddParticipant = () => setAddParticipantView(true);
|
||||||
|
|
||||||
|
const participantType = useRef<
|
||||||
|
COLLECTION_ROLE.COLLABORATOR | COLLECTION_ROLE.VIEWER
|
||||||
|
>();
|
||||||
|
|
||||||
|
const selectedParticipant = useRef<CollectionUser>();
|
||||||
|
|
||||||
|
const openAddCollab = () => {
|
||||||
|
participantType.current = COLLECTION_ROLE.COLLABORATOR;
|
||||||
|
openAddParticipant();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAddViewer = () => {
|
||||||
|
participantType.current = COLLECTION_ROLE.VIEWER;
|
||||||
|
openAddParticipant();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRootClose = () => {
|
||||||
|
onClose();
|
||||||
|
onRootClose();
|
||||||
|
};
|
||||||
|
const handleDrawerClose: DialogProps['onClose'] = (_, reason) => {
|
||||||
|
if (reason === 'backdropClick') {
|
||||||
|
handleRootClose();
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectionUnshare = async (email: string) => {
|
||||||
|
try {
|
||||||
|
appContext.startLoading();
|
||||||
|
await unshareCollection(collection, email);
|
||||||
|
await galleryContext.syncWithRemote(false, true);
|
||||||
|
} finally {
|
||||||
|
appContext.finishLoading();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ownerEmail =
|
||||||
|
galleryContext.user.id === collection.owner?.id
|
||||||
|
? galleryContext.user.email
|
||||||
|
: collection.owner?.email;
|
||||||
|
|
||||||
|
const isOwner = galleryContext.user.id === collection.owner?.id;
|
||||||
|
|
||||||
|
const collaborators = collection.sharees
|
||||||
|
?.filter((sharee) => sharee.role === COLLECTION_ROLE.COLLABORATOR)
|
||||||
|
.map((sharee) => sharee.email);
|
||||||
|
|
||||||
|
const viewers =
|
||||||
|
collection.sharees
|
||||||
|
?.filter((sharee) => sharee.role === COLLECTION_ROLE.VIEWER)
|
||||||
|
.map((sharee) => sharee.email) || [];
|
||||||
|
|
||||||
|
const openManageParticipant = (email) => {
|
||||||
|
selectedParticipant.current = collection.sharees.find(
|
||||||
|
(sharee) => sharee.email === email
|
||||||
|
);
|
||||||
|
setManageParticipantView(true);
|
||||||
|
};
|
||||||
|
const closeManageParticipant = () => {
|
||||||
|
setManageParticipantView(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EnteDrawer anchor="right" open={open} onClose={handleDrawerClose}>
|
||||||
|
<Stack spacing={'4px'} py={'12px'}>
|
||||||
|
<Titlebar
|
||||||
|
onClose={onClose}
|
||||||
|
title={collection.name}
|
||||||
|
onRootClose={handleRootClose}
|
||||||
|
caption={t('participants', {
|
||||||
|
count: peopleCount,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack py={'20px'} px={'12px'} spacing={'24px'}>
|
||||||
|
<Stack>
|
||||||
|
<MenuSectionTitle
|
||||||
|
title={t('OWNER')}
|
||||||
|
icon={<AdminPanelSettingsIcon />}
|
||||||
|
/>
|
||||||
|
<MenuItemGroup>
|
||||||
|
<EnteMenuItem
|
||||||
|
fontWeight="normal"
|
||||||
|
onClick={() => {}}
|
||||||
|
label={isOwner ? t('YOU') : ownerEmail}
|
||||||
|
startIcon={<Avatar email={ownerEmail} />}
|
||||||
|
/>
|
||||||
|
</MenuItemGroup>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<MenuSectionTitle
|
||||||
|
title={t('COLLABORATORS')}
|
||||||
|
icon={<ModeEditIcon />}
|
||||||
|
/>
|
||||||
|
<MenuItemGroup>
|
||||||
|
{collaborators.map((item) => (
|
||||||
|
<>
|
||||||
|
<EnteMenuItem
|
||||||
|
fontWeight={'normal'}
|
||||||
|
key={item}
|
||||||
|
onClick={() =>
|
||||||
|
openManageParticipant(item)
|
||||||
|
}
|
||||||
|
label={item}
|
||||||
|
startIcon={<Avatar email={item} />}
|
||||||
|
endIcon={<ChevronRightIcon />}
|
||||||
|
/>
|
||||||
|
<MenuItemDivider hasIcon />
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<EnteMenuItem
|
||||||
|
startIcon={<Add />}
|
||||||
|
onClick={openAddCollab}
|
||||||
|
label={
|
||||||
|
collaborators?.length
|
||||||
|
? t('ADD_MORE')
|
||||||
|
: t('ADD_COLLABORATORS')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</MenuItemGroup>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<MenuSectionTitle
|
||||||
|
title={t('VIEWERS')}
|
||||||
|
icon={<Photo />}
|
||||||
|
/>
|
||||||
|
<MenuItemGroup>
|
||||||
|
{viewers.map((item) => (
|
||||||
|
<>
|
||||||
|
<EnteMenuItem
|
||||||
|
fontWeight={'normal'}
|
||||||
|
key={item}
|
||||||
|
onClick={() =>
|
||||||
|
openManageParticipant(item)
|
||||||
|
}
|
||||||
|
label={item}
|
||||||
|
startIcon={<Avatar email={item} />}
|
||||||
|
endIcon={<ChevronRightIcon />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MenuItemDivider hasIcon />
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
<EnteMenuItem
|
||||||
|
startIcon={<Add />}
|
||||||
|
fontWeight={'bold'}
|
||||||
|
onClick={openAddViewer}
|
||||||
|
label={
|
||||||
|
viewers?.length
|
||||||
|
? t('ADD_MORE')
|
||||||
|
: t('ADD_VIEWERS')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</MenuItemGroup>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</EnteDrawer>
|
||||||
|
<ManageParticipant
|
||||||
|
collectionUnshare={collectionUnshare}
|
||||||
|
open={manageParticipantView}
|
||||||
|
collection={collection}
|
||||||
|
onRootClose={onRootClose}
|
||||||
|
onClose={closeManageParticipant}
|
||||||
|
selectedParticipant={selectedParticipant.current}
|
||||||
|
/>
|
||||||
|
<AddParticipant
|
||||||
|
open={addParticipantView}
|
||||||
|
onClose={closeAddParticipant}
|
||||||
|
onRootClose={onRootClose}
|
||||||
|
collection={collection}
|
||||||
|
type={participantType.current}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,210 @@
|
||||||
|
import { Stack, Typography } from '@mui/material';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { Collection, CollectionUser } from 'types/collection';
|
||||||
|
import { EnteDrawer } from 'components/EnteDrawer';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { DialogProps } from '@mui/material';
|
||||||
|
import Titlebar from 'components/Titlebar';
|
||||||
|
import { MenuItemGroup } from 'components/Menu/MenuItemGroup';
|
||||||
|
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
|
||||||
|
import ModeEditIcon from '@mui/icons-material/ModeEdit';
|
||||||
|
import PhotoIcon from '@mui/icons-material/Photo';
|
||||||
|
import MenuItemDivider from 'components/Menu/MenuItemDivider';
|
||||||
|
import BlockIcon from '@mui/icons-material/Block';
|
||||||
|
import DoneIcon from '@mui/icons-material/Done';
|
||||||
|
import { handleSharingErrors } from 'utils/error/ui';
|
||||||
|
import { logError } from 'utils/sentry';
|
||||||
|
import { shareCollection } from 'services/collectionService';
|
||||||
|
import { GalleryContext } from 'pages/gallery';
|
||||||
|
import { AppContext } from 'pages/_app';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
|
|
||||||
|
interface Iprops {
|
||||||
|
open: boolean;
|
||||||
|
collection: Collection;
|
||||||
|
onClose: () => void;
|
||||||
|
onRootClose: () => void;
|
||||||
|
selectedParticipant: CollectionUser;
|
||||||
|
collectionUnshare: (email: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ManageParticipant({
|
||||||
|
collection,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onRootClose,
|
||||||
|
selectedParticipant,
|
||||||
|
collectionUnshare,
|
||||||
|
}: Iprops) {
|
||||||
|
const galleryContext = useContext(GalleryContext);
|
||||||
|
const appContext = useContext(AppContext);
|
||||||
|
|
||||||
|
const handleDrawerClose: DialogProps['onClose'] = (_, reason) => {
|
||||||
|
if (reason === 'backdropClick') {
|
||||||
|
onRootClose();
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
collectionUnshare(selectedParticipant.email);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoleChange = (role: string) => () => {
|
||||||
|
if (role !== selectedParticipant.role) {
|
||||||
|
changeRolePermission(selectedParticipant.email, role);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCollectionRole = async (selectedEmail, newRole) => {
|
||||||
|
try {
|
||||||
|
await shareCollection(collection, selectedEmail, newRole);
|
||||||
|
selectedParticipant.role = newRole;
|
||||||
|
await galleryContext.syncWithRemote(false, true);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMessage = handleSharingErrors(e);
|
||||||
|
logError(e, errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeRolePermission = (selectedEmail, newRole) => {
|
||||||
|
let contentText;
|
||||||
|
let buttonText;
|
||||||
|
|
||||||
|
if (newRole === 'VIEWER') {
|
||||||
|
contentText = (
|
||||||
|
<Trans
|
||||||
|
i18nKey="CHANGE_PERMISSIONS_TO_VIEWER"
|
||||||
|
values={{
|
||||||
|
selectedEmail: `${selectedEmail}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
buttonText = t('CONVERT_TO_VIEWER');
|
||||||
|
} else if (newRole === 'COLLABORATOR') {
|
||||||
|
contentText = t(`CHANGE_PERMISSIONS_TO_COLLABORATOR`, {
|
||||||
|
selectedEmail: selectedEmail,
|
||||||
|
});
|
||||||
|
buttonText = t('CONVERT_TO_COLLABORATOR');
|
||||||
|
}
|
||||||
|
|
||||||
|
appContext.setDialogMessage({
|
||||||
|
title: t('CHANGE_PERMISSION'),
|
||||||
|
content: contentText,
|
||||||
|
close: { text: t('CANCEL') },
|
||||||
|
proceed: {
|
||||||
|
text: buttonText,
|
||||||
|
action: () => {
|
||||||
|
updateCollectionRole(selectedEmail, newRole);
|
||||||
|
},
|
||||||
|
variant: 'critical',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeParticipant = () => {
|
||||||
|
appContext.setDialogMessage({
|
||||||
|
title: t('REMOVE_PARTICIPANT'),
|
||||||
|
content: (
|
||||||
|
<Trans
|
||||||
|
i18nKey="REMOVE_PARTICIPANT_MESSAGE"
|
||||||
|
values={{
|
||||||
|
selectedEmail: `${selectedParticipant.email}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
close: { text: t('CANCEL') },
|
||||||
|
proceed: {
|
||||||
|
text: t('CONFIRM_REMOVE'),
|
||||||
|
action: () => {
|
||||||
|
handleClick();
|
||||||
|
},
|
||||||
|
variant: 'critical',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!selectedParticipant) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EnteDrawer anchor="right" open={open} onClose={handleDrawerClose}>
|
||||||
|
<Stack spacing={'4px'} py={'12px'}>
|
||||||
|
<Titlebar
|
||||||
|
onClose={onClose}
|
||||||
|
title={t('MANAGE')}
|
||||||
|
onRootClose={onRootClose}
|
||||||
|
caption={selectedParticipant.email}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack py={'20px'} px={'8px'} spacing={'32px'}>
|
||||||
|
<Stack>
|
||||||
|
<Typography
|
||||||
|
color="text.muted"
|
||||||
|
variant="small"
|
||||||
|
padding={1}>
|
||||||
|
{t('ADDED_AS')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<MenuItemGroup>
|
||||||
|
<EnteMenuItem
|
||||||
|
fontWeight="normal"
|
||||||
|
onClick={handleRoleChange('COLLABORATOR')}
|
||||||
|
label={'Collaborator'}
|
||||||
|
startIcon={<ModeEditIcon />}
|
||||||
|
endIcon={
|
||||||
|
selectedParticipant.role ===
|
||||||
|
'COLLABORATOR' && <DoneIcon />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<MenuItemDivider hasIcon />
|
||||||
|
|
||||||
|
<EnteMenuItem
|
||||||
|
fontWeight="normal"
|
||||||
|
onClick={handleRoleChange('VIEWER')}
|
||||||
|
label={'Viewer'}
|
||||||
|
startIcon={<PhotoIcon />}
|
||||||
|
endIcon={
|
||||||
|
selectedParticipant.role ===
|
||||||
|
'VIEWER' && <DoneIcon />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</MenuItemGroup>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
color="text.muted"
|
||||||
|
variant="small"
|
||||||
|
padding={1}>
|
||||||
|
{t('COLLABORATOR_RIGHTS')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack py={'30px'}>
|
||||||
|
<Typography
|
||||||
|
color="text.muted"
|
||||||
|
variant="small"
|
||||||
|
padding={1}>
|
||||||
|
{t('REMOVE_PARTICIPANT_HEAD')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<MenuItemGroup>
|
||||||
|
<EnteMenuItem
|
||||||
|
color="error"
|
||||||
|
fontWeight="normal"
|
||||||
|
onClick={removeParticipant}
|
||||||
|
label={'Remove'}
|
||||||
|
startIcon={<BlockIcon />}
|
||||||
|
/>
|
||||||
|
</MenuItemGroup>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</EnteDrawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { COLLECTION_ROLE, Collection } from 'types/collection';
|
||||||
|
|
||||||
|
import { Stack } from '@mui/material';
|
||||||
|
import MenuSectionTitle from 'components/Menu/MenuSectionTitle';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import Workspaces from '@mui/icons-material/Workspaces';
|
||||||
|
import { MenuItemGroup } from 'components/Menu/MenuItemGroup';
|
||||||
|
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
|
||||||
|
import MenuItemDivider from 'components/Menu/MenuItemDivider';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import AddParticipant from './AddParticipant';
|
||||||
|
import ManageEmailShare from './ManageEmailShare';
|
||||||
|
import AvatarGroup from 'components/pages/gallery/AvatarGroup';
|
||||||
|
import ChevronRight from '@mui/icons-material/ChevronRight';
|
||||||
|
|
||||||
|
export default function EmailShare({
|
||||||
|
collection,
|
||||||
|
onRootClose,
|
||||||
|
}: {
|
||||||
|
collection: Collection;
|
||||||
|
onRootClose: () => void;
|
||||||
|
}) {
|
||||||
|
const [addParticipantView, setAddParticipantView] = useState(false);
|
||||||
|
const [manageEmailShareView, setManageEmailShareView] = useState(false);
|
||||||
|
|
||||||
|
const closeAddParticipant = () => setAddParticipantView(false);
|
||||||
|
const openAddParticipant = () => setAddParticipantView(true);
|
||||||
|
|
||||||
|
const closeManageEmailShare = () => setManageEmailShareView(false);
|
||||||
|
const openManageEmailShare = () => setManageEmailShareView(true);
|
||||||
|
|
||||||
|
const participantType = useRef<
|
||||||
|
COLLECTION_ROLE.COLLABORATOR | COLLECTION_ROLE.VIEWER
|
||||||
|
>();
|
||||||
|
|
||||||
|
const openAddCollab = () => {
|
||||||
|
participantType.current = COLLECTION_ROLE.COLLABORATOR;
|
||||||
|
openAddParticipant();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAddViewer = () => {
|
||||||
|
participantType.current = COLLECTION_ROLE.VIEWER;
|
||||||
|
openAddParticipant();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack>
|
||||||
|
<MenuSectionTitle
|
||||||
|
title={t('shared_with_people', {
|
||||||
|
count: collection.sharees?.length ?? 0,
|
||||||
|
})}
|
||||||
|
icon={<Workspaces />}
|
||||||
|
/>
|
||||||
|
<MenuItemGroup>
|
||||||
|
{collection.sharees.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<EnteMenuItem
|
||||||
|
fontWeight={'normal'}
|
||||||
|
startIcon={
|
||||||
|
<AvatarGroup sharees={collection.sharees} />
|
||||||
|
}
|
||||||
|
onClick={openManageEmailShare}
|
||||||
|
label={
|
||||||
|
collection.sharees.length === 1
|
||||||
|
? t(collection.sharees[0]?.email)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
endIcon={<ChevronRight />}
|
||||||
|
/>
|
||||||
|
<MenuItemDivider hasIcon />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<EnteMenuItem
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={openAddViewer}
|
||||||
|
label={t('ADD_VIEWERS')}
|
||||||
|
/>
|
||||||
|
<MenuItemDivider hasIcon />
|
||||||
|
<EnteMenuItem
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={openAddCollab}
|
||||||
|
label={t('ADD_COLLABORATORS')}
|
||||||
|
/>
|
||||||
|
</MenuItemGroup>
|
||||||
|
</Stack>
|
||||||
|
<AddParticipant
|
||||||
|
open={addParticipantView}
|
||||||
|
onClose={closeAddParticipant}
|
||||||
|
onRootClose={onRootClose}
|
||||||
|
collection={collection}
|
||||||
|
type={participantType.current}
|
||||||
|
/>
|
||||||
|
<ManageEmailShare
|
||||||
|
peopleCount={collection.sharees.length}
|
||||||
|
open={manageEmailShareView}
|
||||||
|
onClose={closeManageEmailShare}
|
||||||
|
onRootClose={onRootClose}
|
||||||
|
collection={collection}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,21 +1,21 @@
|
||||||
import EmailShare from './emailShare';
|
import { Collection, CollectionSummary } from 'types/collection';
|
||||||
import React from 'react';
|
|
||||||
import { Collection } from 'types/collection';
|
|
||||||
import { EnteDrawer } from 'components/EnteDrawer';
|
import { EnteDrawer } from 'components/EnteDrawer';
|
||||||
import PublicShare from './publicShare';
|
import PublicShare from './publicShare';
|
||||||
import WorkspacesIcon from '@mui/icons-material/Workspaces';
|
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import MenuSectionTitle from 'components/Menu/MenuSectionTitle';
|
|
||||||
import { DialogProps, Stack } from '@mui/material';
|
import { DialogProps, Stack } from '@mui/material';
|
||||||
import Titlebar from 'components/Titlebar';
|
import Titlebar from 'components/Titlebar';
|
||||||
|
import EmailShare from './emailShare';
|
||||||
|
import { CollectionSummaryType } from 'constants/collection';
|
||||||
|
import SharingDetails from './sharingDetails';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
|
collectionSummary: CollectionSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CollectionShare(props: Props) {
|
function CollectionShare({ collectionSummary, ...props }: Props) {
|
||||||
const handleRootClose = () => {
|
const handleRootClose = () => {
|
||||||
props.onClose();
|
props.onClose();
|
||||||
};
|
};
|
||||||
|
@ -29,36 +29,53 @@ function CollectionShare(props: Props) {
|
||||||
if (!props.collection) {
|
if (!props.collection) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
const { type } = collectionSummary;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<EnteDrawer
|
||||||
<EnteDrawer
|
anchor="right"
|
||||||
anchor="right"
|
open={props.open}
|
||||||
open={props.open}
|
onClose={handleDrawerClose}
|
||||||
onClose={handleDrawerClose}
|
slotProps={{
|
||||||
BackdropProps={{
|
backdrop: {
|
||||||
sx: { '&&&': { backgroundColor: 'transparent' } },
|
sx: { '&&&': { backgroundColor: 'transparent' } },
|
||||||
}}>
|
},
|
||||||
<Stack spacing={'4px'} py={'12px'}>
|
}}>
|
||||||
<Titlebar
|
<Stack spacing={'4px'} py={'12px'}>
|
||||||
onClose={props.onClose}
|
<Titlebar
|
||||||
title={t('SHARE_COLLECTION')}
|
onClose={props.onClose}
|
||||||
onRootClose={handleRootClose}
|
title={
|
||||||
/>
|
type ===
|
||||||
<Stack py={'20px'} px={'8px'}>
|
CollectionSummaryType.incomingShareCollaborator ||
|
||||||
<MenuSectionTitle
|
type === CollectionSummaryType.incomingShareViewer
|
||||||
title={t('ADD_EMAIL_TITLE')}
|
? t('SHARING_DETAILS')
|
||||||
icon={<WorkspacesIcon />}
|
: t('SHARE_COLLECTION')
|
||||||
/>
|
}
|
||||||
<EmailShare collection={props.collection} />
|
onRootClose={handleRootClose}
|
||||||
<PublicShare
|
caption={props.collection.name}
|
||||||
|
/>
|
||||||
|
<Stack py={'20px'} px={'8px'} gap={'24px'}>
|
||||||
|
{type === CollectionSummaryType.incomingShareCollaborator ||
|
||||||
|
type === CollectionSummaryType.incomingShareViewer ? (
|
||||||
|
<SharingDetails
|
||||||
collection={props.collection}
|
collection={props.collection}
|
||||||
onRootClose={handleRootClose}
|
type={type}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
) : (
|
||||||
|
<>
|
||||||
|
<EmailShare
|
||||||
|
collection={props.collection}
|
||||||
|
onRootClose={handleRootClose}
|
||||||
|
/>
|
||||||
|
<PublicShare
|
||||||
|
collection={props.collection}
|
||||||
|
onRootClose={handleRootClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</EnteDrawer>
|
</Stack>
|
||||||
</>
|
</EnteDrawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default CollectionShare;
|
export default CollectionShare;
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { Box, Typography } from '@mui/material';
|
|
||||||
import { GalleryContext } from 'pages/gallery';
|
|
||||||
import { AppContext } from 'pages/_app';
|
|
||||||
import React, { useContext } from 'react';
|
|
||||||
import { t } from 'i18next';
|
|
||||||
import { unshareCollection } from 'services/collectionService';
|
|
||||||
import { Collection, CollectionUser } from 'types/collection';
|
|
||||||
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: CollectionUser) => {
|
|
||||||
try {
|
|
||||||
appContext.startLoading();
|
|
||||||
await unshareCollection(collection, sharee.email);
|
|
||||||
await galleryContext.syncWithRemote(false, true);
|
|
||||||
} finally {
|
|
||||||
appContext.finishLoading();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!collection.sharees?.length) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box mb={3}>
|
|
||||||
<Typography variant="small" color="text.muted">
|
|
||||||
{t('SHAREES')}
|
|
||||||
</Typography>
|
|
||||||
{collection.sharees?.map((sharee) => (
|
|
||||||
<ShareeRow
|
|
||||||
key={sharee.email}
|
|
||||||
sharee={sharee}
|
|
||||||
collectionUnshare={collectionUnshare}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { SpaceBetweenFlex } from 'components/Container';
|
|
||||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
|
||||||
import OverflowMenu from 'components/OverflowMenu/menu';
|
|
||||||
|
|
||||||
import NotInterestedIcon from '@mui/icons-material/NotInterested';
|
|
||||||
import { OverflowMenuOption } from 'components/OverflowMenu/option';
|
|
||||||
import { t } from 'i18next';
|
|
||||||
import { CollectionUser } from 'types/collection';
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
sharee: CollectionUser;
|
|
||||||
collectionUnshare: (sharee: CollectionUser) => void;
|
|
||||||
}
|
|
||||||
const ShareeRow = ({ sharee, collectionUnshare }: IProps) => {
|
|
||||||
const handleClick = () => collectionUnshare(sharee);
|
|
||||||
return (
|
|
||||||
<SpaceBetweenFlex>
|
|
||||||
{sharee.email}
|
|
||||||
<OverflowMenu
|
|
||||||
menuPaperProps={{
|
|
||||||
sx: {
|
|
||||||
backgroundColor: (theme) =>
|
|
||||||
theme.colors.background.elevated2,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
ariaControls={`email-share-${sharee.email}`}
|
|
||||||
triggerButtonIcon={<MoreHorizIcon />}>
|
|
||||||
<OverflowMenuOption
|
|
||||||
color="critical"
|
|
||||||
onClick={handleClick}
|
|
||||||
startIcon={<NotInterestedIcon />}>
|
|
||||||
{t('REMOVE')}
|
|
||||||
</OverflowMenuOption>
|
|
||||||
</OverflowMenu>
|
|
||||||
</SpaceBetweenFlex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShareeRow;
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { Stack } from '@mui/material';
|
||||||
|
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
|
||||||
|
import MenuItemDivider from 'components/Menu/MenuItemDivider';
|
||||||
|
import { MenuItemGroup } from 'components/Menu/MenuItemGroup';
|
||||||
|
import MenuSectionTitle from 'components/Menu/MenuSectionTitle';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
|
||||||
|
import { COLLECTION_ROLE } from 'types/collection';
|
||||||
|
import { GalleryContext } from 'pages/gallery';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import Avatar from 'components/pages/gallery/Avatar';
|
||||||
|
import ModeEditIcon from '@mui/icons-material/ModeEdit';
|
||||||
|
import { CollectionSummaryType } from 'constants/collection';
|
||||||
|
import Photo from '@mui/icons-material/Photo';
|
||||||
|
|
||||||
|
export default function SharingDetails({ collection, type }) {
|
||||||
|
const galleryContext = useContext(GalleryContext);
|
||||||
|
|
||||||
|
const ownerEmail =
|
||||||
|
galleryContext.user.id === collection.owner?.id
|
||||||
|
? galleryContext.user?.email
|
||||||
|
: collection.owner?.email;
|
||||||
|
|
||||||
|
const collaborators = collection.sharees
|
||||||
|
?.filter((sharee) => sharee.role === COLLECTION_ROLE.COLLABORATOR)
|
||||||
|
.map((sharee) => sharee.email);
|
||||||
|
|
||||||
|
const viewers =
|
||||||
|
collection.sharees
|
||||||
|
?.filter((sharee) => sharee.role === COLLECTION_ROLE.VIEWER)
|
||||||
|
.map((sharee) => sharee.email) || [];
|
||||||
|
|
||||||
|
const isOwner = galleryContext.user?.id === collection.owner?.id;
|
||||||
|
|
||||||
|
const isMe = (email: string) => email === galleryContext.user?.email;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack>
|
||||||
|
<MenuSectionTitle
|
||||||
|
title={t('OWNER')}
|
||||||
|
icon={<AdminPanelSettingsIcon />}
|
||||||
|
/>
|
||||||
|
<MenuItemGroup>
|
||||||
|
<EnteMenuItem
|
||||||
|
fontWeight="normal"
|
||||||
|
onClick={() => {}}
|
||||||
|
label={isOwner ? t('YOU') : ownerEmail}
|
||||||
|
startIcon={<Avatar email={ownerEmail} />}
|
||||||
|
/>
|
||||||
|
</MenuItemGroup>
|
||||||
|
</Stack>
|
||||||
|
{type === CollectionSummaryType.incomingShareCollaborator &&
|
||||||
|
collaborators?.length > 0 && (
|
||||||
|
<Stack>
|
||||||
|
<MenuSectionTitle
|
||||||
|
title={t('COLLABORATORS')}
|
||||||
|
icon={<ModeEditIcon />}
|
||||||
|
/>
|
||||||
|
<MenuItemGroup>
|
||||||
|
{collaborators.map((item, index) => (
|
||||||
|
<>
|
||||||
|
<EnteMenuItem
|
||||||
|
fontWeight="normal"
|
||||||
|
key={item}
|
||||||
|
onClick={() => {}}
|
||||||
|
label={isMe(item) ? t('YOU') : item}
|
||||||
|
startIcon={<Avatar email={item} />}
|
||||||
|
/>
|
||||||
|
{index !== collaborators.length - 1 && (
|
||||||
|
<MenuItemDivider />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</MenuItemGroup>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{viewers?.length > 0 && (
|
||||||
|
<Stack>
|
||||||
|
<MenuSectionTitle title={t('VIEWERS')} icon={<Photo />} />
|
||||||
|
<MenuItemGroup>
|
||||||
|
{viewers.map((item, index) => (
|
||||||
|
<>
|
||||||
|
<EnteMenuItem
|
||||||
|
fontWeight="normal"
|
||||||
|
key={item}
|
||||||
|
onClick={() => {}}
|
||||||
|
label={isMe(item) ? t('YOU') : item}
|
||||||
|
startIcon={<Avatar email={item} />}
|
||||||
|
/>
|
||||||
|
{index !== viewers.length - 1 && (
|
||||||
|
<MenuItemDivider />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</MenuItemGroup>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -133,6 +133,7 @@ export default function Collections(props: Iprops) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CollectionShare
|
<CollectionShare
|
||||||
|
collectionSummary={collectionSummaries.get(activeCollectionID)}
|
||||||
open={collectionShareModalView}
|
open={collectionShareModalView}
|
||||||
onClose={closeCollectionShare}
|
onClose={closeCollectionShare}
|
||||||
collection={activeCollection.current}
|
collection={activeCollection.current}
|
||||||
|
|
|
@ -58,7 +58,6 @@ const MapBox: React.FC<MapBoxProps> = ({
|
||||||
} else {
|
} else {
|
||||||
if (mapContainer && mapContainer.hasChildNodes()) {
|
if (mapContainer && mapContainer.hasChildNodes()) {
|
||||||
if (mapContainer.firstChild) {
|
if (mapContainer.firstChild) {
|
||||||
console.log('removing child');
|
|
||||||
mapContainer.removeChild(mapContainer.firstChild);
|
mapContainer.removeChild(mapContainer.firstChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,179 +0,0 @@
|
||||||
import React, { useMemo, useState } from 'react';
|
|
||||||
import { Formik, FormikHelpers, FormikState } from 'formik';
|
|
||||||
import * as Yup from 'yup';
|
|
||||||
import SubmitButton from './SubmitButton';
|
|
||||||
import TextField from '@mui/material/TextField';
|
|
||||||
import { FlexWrapper } from './Container';
|
|
||||||
import { Button, FormHelperText } from '@mui/material';
|
|
||||||
import { t } from 'i18next';
|
|
||||||
import Autocomplete from '@mui/material/Autocomplete';
|
|
||||||
|
|
||||||
interface formValues {
|
|
||||||
inputValue: string;
|
|
||||||
}
|
|
||||||
export interface SingleInputAutocompleteProps {
|
|
||||||
callback: (
|
|
||||||
inputValue: string,
|
|
||||||
setFieldError: (errorMessage: string) => void,
|
|
||||||
resetForm: (nextState?: Partial<FormikState<formValues>>) => void
|
|
||||||
) => Promise<void>;
|
|
||||||
fieldType: 'text' | 'email' | 'password';
|
|
||||||
placeholder: string;
|
|
||||||
buttonText: string;
|
|
||||||
submitButtonProps?: any;
|
|
||||||
initialValue?: string;
|
|
||||||
secondaryButtonAction?: () => void;
|
|
||||||
disableAutoFocus?: boolean;
|
|
||||||
hiddenPreInput?: any;
|
|
||||||
caption?: any;
|
|
||||||
hiddenPostInput?: any;
|
|
||||||
autoComplete?: string;
|
|
||||||
blockButton?: boolean;
|
|
||||||
hiddenLabel?: boolean;
|
|
||||||
optionsList?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SingleInputAutocomplete(
|
|
||||||
props: SingleInputAutocompleteProps
|
|
||||||
) {
|
|
||||||
const [selectedOptions, setSelectedOptions] = useState([]);
|
|
||||||
|
|
||||||
const { submitButtonProps } = props;
|
|
||||||
const { sx: buttonSx, ...restSubmitButtonProps } = submitButtonProps ?? {};
|
|
||||||
|
|
||||||
const [loading, SetLoading] = useState(false);
|
|
||||||
|
|
||||||
const submitForm = async (
|
|
||||||
values: formValues,
|
|
||||||
{ setFieldError, resetForm }: FormikHelpers<formValues>
|
|
||||||
) => {
|
|
||||||
SetLoading(true);
|
|
||||||
await props.callback(
|
|
||||||
values.inputValue,
|
|
||||||
(message) => setFieldError('inputValue', message),
|
|
||||||
resetForm
|
|
||||||
);
|
|
||||||
|
|
||||||
if (props.optionsList && props.optionsList.length > 0) {
|
|
||||||
setSelectedOptions([...selectedOptions, values.inputValue]);
|
|
||||||
}
|
|
||||||
|
|
||||||
SetLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validationSchema = useMemo(() => {
|
|
||||||
switch (props.fieldType) {
|
|
||||||
case 'text':
|
|
||||||
return Yup.object().shape({
|
|
||||||
inputValue: Yup.string().required(t('REQUIRED')),
|
|
||||||
});
|
|
||||||
case 'email':
|
|
||||||
return Yup.object().shape({
|
|
||||||
inputValue: Yup.string()
|
|
||||||
.email(t('EMAIL_ERROR'))
|
|
||||||
.required(t('REQUIRED')),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [props.fieldType]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik<formValues>
|
|
||||||
initialValues={{ inputValue: props.initialValue ?? '' }}
|
|
||||||
onSubmit={submitForm}
|
|
||||||
validationSchema={validationSchema}
|
|
||||||
validateOnChange={false}
|
|
||||||
validateOnBlur={false}>
|
|
||||||
{({
|
|
||||||
values,
|
|
||||||
errors,
|
|
||||||
handleChange,
|
|
||||||
handleSubmit,
|
|
||||||
setFieldValue,
|
|
||||||
}) => (
|
|
||||||
<form noValidate onSubmit={handleSubmit}>
|
|
||||||
{props.hiddenPreInput}
|
|
||||||
|
|
||||||
<Autocomplete
|
|
||||||
id="free-solo-demo"
|
|
||||||
filterSelectedOptions
|
|
||||||
freeSolo
|
|
||||||
value={values.inputValue}
|
|
||||||
options={props.optionsList
|
|
||||||
.map((option) => option.toString())
|
|
||||||
.filter(
|
|
||||||
(option) => !selectedOptions.includes(option)
|
|
||||||
)}
|
|
||||||
onChange={(event, newValue) => {
|
|
||||||
setFieldValue('inputValue', newValue);
|
|
||||||
}}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
hiddenLabel={props.hiddenLabel}
|
|
||||||
fullWidth
|
|
||||||
type={props.fieldType}
|
|
||||||
id={props.fieldType}
|
|
||||||
onChange={handleChange('inputValue')}
|
|
||||||
name={props.fieldType}
|
|
||||||
{...(props.hiddenLabel
|
|
||||||
? { placeholder: props.placeholder }
|
|
||||||
: { label: props.placeholder })}
|
|
||||||
error={Boolean(errors.inputValue)}
|
|
||||||
helperText={errors.inputValue}
|
|
||||||
value={values.inputValue}
|
|
||||||
disabled={loading}
|
|
||||||
autoFocus={!props.disableAutoFocus}
|
|
||||||
autoComplete={props.autoComplete}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormHelperText
|
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
top: errors.inputValue ? '-22px' : '0',
|
|
||||||
float: 'right',
|
|
||||||
padding: '0 8px',
|
|
||||||
}}>
|
|
||||||
{props.caption}
|
|
||||||
</FormHelperText>
|
|
||||||
{props.hiddenPostInput}
|
|
||||||
<FlexWrapper
|
|
||||||
justifyContent={'flex-end'}
|
|
||||||
flexWrap={
|
|
||||||
props.blockButton ? 'wrap-reverse' : 'nowrap'
|
|
||||||
}>
|
|
||||||
{props.secondaryButtonAction && (
|
|
||||||
<Button
|
|
||||||
onClick={props.secondaryButtonAction}
|
|
||||||
size="large"
|
|
||||||
color="secondary"
|
|
||||||
sx={{
|
|
||||||
'&&&': {
|
|
||||||
mt: !props.blockButton ? 2 : 0.5,
|
|
||||||
mb: !props.blockButton ? 4 : 0,
|
|
||||||
mr: !props.blockButton ? 1 : 0,
|
|
||||||
...buttonSx,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...restSubmitButtonProps}>
|
|
||||||
{t('CANCEL')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<SubmitButton
|
|
||||||
sx={{
|
|
||||||
'&&&': {
|
|
||||||
mt: 2,
|
|
||||||
...buttonSx,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
buttonText={props.buttonText}
|
|
||||||
loading={loading}
|
|
||||||
{...restSubmitButtonProps}
|
|
||||||
/>
|
|
||||||
</FlexWrapper>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -6,15 +6,22 @@ import { useTheme } from '@mui/material/styles';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
interface AvatarProps {
|
interface AvatarProps {
|
||||||
file: EnteFile;
|
file?: EnteFile;
|
||||||
|
email?: string;
|
||||||
|
opacity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PUBLIC_COLLECTED_FILES_AVATAR_COLOR_CODE = '#000000';
|
const PUBLIC_COLLECTED_FILES_AVATAR_COLOR_CODE = '#000000';
|
||||||
|
|
||||||
const AvatarBase = styled('div')<{ colorCode: string; size: number }>`
|
const AvatarBase = styled('div')<{
|
||||||
|
colorCode: string;
|
||||||
|
size: number;
|
||||||
|
opacity: number;
|
||||||
|
}>`
|
||||||
width: ${({ size }) => `${size}px`};
|
width: ${({ size }) => `${size}px`};
|
||||||
height: ${({ size }) => `${size}px`};
|
height: ${({ size }) => `${size}px`};
|
||||||
background-color: ${({ colorCode }) => `${colorCode}95`};
|
background-color: ${({ colorCode, opacity }) =>
|
||||||
|
`${colorCode}${opacity === 100 ? '' : opacity ?? 95}`};
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -24,7 +31,7 @@ const AvatarBase = styled('div')<{ colorCode: string; size: number }>`
|
||||||
font-size: ${({ size }) => `${Math.floor(size / 2)}px`};
|
font-size: ${({ size }) => `${Math.floor(size / 2)}px`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Avatar: React.FC<AvatarProps> = ({ file }) => {
|
const Avatar: React.FC<AvatarProps> = ({ file, email, opacity }) => {
|
||||||
const { userIDToEmailMap, user } = useContext(GalleryContext);
|
const { userIDToEmailMap, user } = useContext(GalleryContext);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
@ -33,6 +40,9 @@ const Avatar: React.FC<AvatarProps> = ({ file }) => {
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
try {
|
try {
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (file.ownerID !== user.id) {
|
if (file.ownerID !== user.id) {
|
||||||
// getting email from in-memory id-email map
|
// getting email from in-memory id-email map
|
||||||
const email = userIDToEmailMap.get(file.ownerID);
|
const email = userIDToEmailMap.get(file.ownerID);
|
||||||
|
@ -58,16 +68,43 @@ const Avatar: React.FC<AvatarProps> = ({ file }) => {
|
||||||
setColorCode(PUBLIC_COLLECTED_FILES_AVATAR_COLOR_CODE);
|
setColorCode(PUBLIC_COLLECTED_FILES_AVATAR_COLOR_CODE);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError(err, 'AvatarIcon.tsx - useLayoutEffect failed');
|
logError(err, 'AvatarIcon.tsx - useLayoutEffect file failed');
|
||||||
}
|
}
|
||||||
}, []);
|
}, [file]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
try {
|
||||||
|
if (!email) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user.email === email) {
|
||||||
|
setUserLetter(email[0].toUpperCase());
|
||||||
|
setColorCode(PUBLIC_COLLECTED_FILES_AVATAR_COLOR_CODE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Array.from(userIDToEmailMap.keys()).find(
|
||||||
|
(key) => userIDToEmailMap.get(key) === email
|
||||||
|
);
|
||||||
|
if (!id) {
|
||||||
|
logError(Error(), `ID not found for email: ${email}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const colorIndex = id % theme.colors.avatarColors.length;
|
||||||
|
const colorCode = theme.colors.avatarColors[colorIndex];
|
||||||
|
setUserLetter(email[0].toUpperCase());
|
||||||
|
setColorCode(colorCode);
|
||||||
|
} catch (err) {
|
||||||
|
logError(err, 'AvatarIcon.tsx - useLayoutEffect email failed');
|
||||||
|
}
|
||||||
|
}, [email]);
|
||||||
|
|
||||||
if (!colorCode || !userLetter) {
|
if (!colorCode || !userLetter) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AvatarBase size={18} colorCode={colorCode}>
|
<AvatarBase size={18} colorCode={colorCode} opacity={opacity}>
|
||||||
{userLetter}
|
{userLetter}
|
||||||
</AvatarBase>
|
</AvatarBase>
|
||||||
);
|
);
|
||||||
|
|
52
apps/photos/src/components/pages/gallery/AvatarGroup.tsx
Normal file
52
apps/photos/src/components/pages/gallery/AvatarGroup.tsx
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import NumberAvatar from '@mui/material/Avatar';
|
||||||
|
import Avatar from './Avatar';
|
||||||
|
import { Collection } from 'types/collection';
|
||||||
|
|
||||||
|
const AvatarContainer = styled('div')({
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: -5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const AvatarContainerOuter = styled('div')({
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: 8,
|
||||||
|
});
|
||||||
|
const AvatarCounter = styled(NumberAvatar)({
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#fff',
|
||||||
|
});
|
||||||
|
|
||||||
|
const SHAREE_AVATAR_LIMIT = 6;
|
||||||
|
|
||||||
|
const AvatarGroup = ({ sharees }: { sharees: Collection['sharees'] }) => {
|
||||||
|
const hasShareesOverLimit = sharees?.length > SHAREE_AVATAR_LIMIT;
|
||||||
|
const countOfShareesOverLimit = sharees?.length - SHAREE_AVATAR_LIMIT;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AvatarContainerOuter>
|
||||||
|
{sharees?.slice(0, 6).map((sharee) => (
|
||||||
|
<AvatarContainer key={sharee.email}>
|
||||||
|
<Avatar
|
||||||
|
key={sharee.email}
|
||||||
|
email={sharee.email}
|
||||||
|
opacity={100}
|
||||||
|
/>
|
||||||
|
</AvatarContainer>
|
||||||
|
))}
|
||||||
|
{hasShareesOverLimit && (
|
||||||
|
<AvatarContainer key="extra-count">
|
||||||
|
<AvatarCounter>+{countOfShareesOverLimit}</AvatarCounter>
|
||||||
|
</AvatarContainer>
|
||||||
|
)}
|
||||||
|
</AvatarContainerOuter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AvatarGroup;
|
|
@ -24,6 +24,7 @@ import {
|
||||||
createAlbum,
|
createAlbum,
|
||||||
getCollectionSummaries,
|
getCollectionSummaries,
|
||||||
moveToHiddenCollection,
|
moveToHiddenCollection,
|
||||||
|
constructEmailList,
|
||||||
} from 'services/collectionService';
|
} from 'services/collectionService';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
|
|
||||||
|
@ -107,7 +108,7 @@ import SearchResultInfo from 'components/Search/SearchResultInfo';
|
||||||
import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList';
|
import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList';
|
||||||
import UploadInputs from 'components/UploadSelectorInputs';
|
import UploadInputs from 'components/UploadSelectorInputs';
|
||||||
import useFileInput from 'hooks/useFileInput';
|
import useFileInput from 'hooks/useFileInput';
|
||||||
import { User } from 'types/user';
|
import { FamilyData, User } from 'types/user';
|
||||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
import { CenteredFlex } from 'components/Container';
|
import { CenteredFlex } from 'components/Container';
|
||||||
import { checkConnectivity } from 'utils/common';
|
import { checkConnectivity } from 'utils/common';
|
||||||
|
@ -124,6 +125,7 @@ import { isSameDayAnyYear, isInsideLocationTag } from 'utils/search';
|
||||||
import { getSessionExpiredMessage } from 'utils/ui';
|
import { getSessionExpiredMessage } from 'utils/ui';
|
||||||
import { syncEntities } from 'services/entityService';
|
import { syncEntities } from 'services/entityService';
|
||||||
import { constructUserIDToEmailMap } from 'services/collectionService';
|
import { constructUserIDToEmailMap } from 'services/collectionService';
|
||||||
|
import { getLocalFamilyData } from 'utils/user/family';
|
||||||
|
|
||||||
export const DeadCenter = styled('div')`
|
export const DeadCenter = styled('div')`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -147,6 +149,7 @@ const defaultGalleryContext: GalleryContextType = {
|
||||||
authenticateUser: () => null,
|
authenticateUser: () => null,
|
||||||
user: null,
|
user: null,
|
||||||
userIDToEmailMap: null,
|
userIDToEmailMap: null,
|
||||||
|
emailList: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GalleryContext = createContext<GalleryContextType>(
|
export const GalleryContext = createContext<GalleryContextType>(
|
||||||
|
@ -156,6 +159,7 @@ export const GalleryContext = createContext<GalleryContextType>(
|
||||||
export default function Gallery() {
|
export default function Gallery() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
|
const [familyData, setFamilyData] = useState<FamilyData>(null);
|
||||||
const [collections, setCollections] = useState<Collection[]>(null);
|
const [collections, setCollections] = useState<Collection[]>(null);
|
||||||
const [files, setFiles] = useState<EnteFile[]>(null);
|
const [files, setFiles] = useState<EnteFile[]>(null);
|
||||||
const [hiddenFiles, setHiddenFiles] = useState<EnteFile[]>(null);
|
const [hiddenFiles, setHiddenFiles] = useState<EnteFile[]>(null);
|
||||||
|
@ -224,6 +228,7 @@ export default function Gallery() {
|
||||||
useState<CollectionSummaries>();
|
useState<CollectionSummaries>();
|
||||||
const [userIDToEmailMap, setUserIDToEmailMap] =
|
const [userIDToEmailMap, setUserIDToEmailMap] =
|
||||||
useState<Map<number, string>>(null);
|
useState<Map<number, string>>(null);
|
||||||
|
const [emailList, setEmailList] = useState<string[]>(null);
|
||||||
const [activeCollection, setActiveCollection] = useState<number>(undefined);
|
const [activeCollection, setActiveCollection] = useState<number>(undefined);
|
||||||
const [fixCreationTimeView, setFixCreationTimeView] = useState(false);
|
const [fixCreationTimeView, setFixCreationTimeView] = useState(false);
|
||||||
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
|
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
|
||||||
|
@ -283,6 +288,7 @@ export default function Gallery() {
|
||||||
}
|
}
|
||||||
setIsFirstLogin(false);
|
setIsFirstLogin(false);
|
||||||
const user = getData(LS_KEYS.USER);
|
const user = getData(LS_KEYS.USER);
|
||||||
|
const familyData = getLocalFamilyData();
|
||||||
const files = sortFiles(mergeMetadata(await getLocalFiles()));
|
const files = sortFiles(mergeMetadata(await getLocalFiles()));
|
||||||
const hiddenFiles = sortFiles(
|
const hiddenFiles = sortFiles(
|
||||||
mergeMetadata(await getLocalHiddenFiles())
|
mergeMetadata(await getLocalHiddenFiles())
|
||||||
|
@ -291,6 +297,7 @@ export default function Gallery() {
|
||||||
const trashedFiles = await getLocalTrashedFiles();
|
const trashedFiles = await getLocalTrashedFiles();
|
||||||
|
|
||||||
setUser(user);
|
setUser(user);
|
||||||
|
setFamilyData(familyData);
|
||||||
setFiles(files);
|
setFiles(files);
|
||||||
setTrashedFiles(trashedFiles);
|
setTrashedFiles(trashedFiles);
|
||||||
setHiddenFiles(hiddenFiles);
|
setHiddenFiles(hiddenFiles);
|
||||||
|
@ -321,16 +328,21 @@ export default function Gallery() {
|
||||||
}, [collections, files, hiddenFiles, trashedFiles, user]);
|
}, [collections, files, hiddenFiles, trashedFiles, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
if (!collections || !user) {
|
||||||
if (!collections) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
const userIdToEmailMap = constructUserIDToEmailMap(user, collections);
|
||||||
const userIdToEmailMap = await constructUserIDToEmailMap();
|
setUserIDToEmailMap(userIdToEmailMap);
|
||||||
setUserIDToEmailMap(userIdToEmailMap);
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
}, [collections]);
|
}, [collections]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user || !collections) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const emailList = constructEmailList(user, collections, familyData);
|
||||||
|
setEmailList(emailList);
|
||||||
|
}, [user, collections, familyData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
collectionSelectorAttributes && setCollectionSelectorView(true);
|
collectionSelectorAttributes && setCollectionSelectorView(true);
|
||||||
}, [collectionSelectorAttributes]);
|
}, [collectionSelectorAttributes]);
|
||||||
|
@ -901,6 +913,7 @@ export default function Gallery() {
|
||||||
authenticateUser,
|
authenticateUser,
|
||||||
userIDToEmailMap,
|
userIDToEmailMap,
|
||||||
user,
|
user,
|
||||||
|
emailList,
|
||||||
}}>
|
}}>
|
||||||
<FullScreenDropZone
|
<FullScreenDropZone
|
||||||
getDragAndDropRootProps={getDragAndDropRootProps}>
|
getDragAndDropRootProps={getDragAndDropRootProps}>
|
||||||
|
|
|
@ -53,7 +53,7 @@ import AddPhotoAlternateOutlined from '@mui/icons-material/AddPhotoAlternateOutl
|
||||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
import { UploadTypeSelectorIntent } from 'types/gallery';
|
import { UploadTypeSelectorIntent } from 'types/gallery';
|
||||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||||
import { MoreHoriz } from '@mui/icons-material';
|
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||||
import OverflowMenu from 'components/OverflowMenu/menu';
|
import OverflowMenu from 'components/OverflowMenu/menu';
|
||||||
import { OverflowMenuOption } from 'components/OverflowMenu/option';
|
import { OverflowMenuOption } from 'components/OverflowMenu/option';
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ import {
|
||||||
isPinnedCollection,
|
isPinnedCollection,
|
||||||
updateMagicMetadata,
|
updateMagicMetadata,
|
||||||
} from 'utils/magicMetadata';
|
} from 'utils/magicMetadata';
|
||||||
import { User } from 'types/user';
|
import { FamilyData, User } from 'types/user';
|
||||||
import {
|
import {
|
||||||
isQuickLinkCollection,
|
isQuickLinkCollection,
|
||||||
isOutgoingShare,
|
isOutgoingShare,
|
||||||
|
@ -922,7 +922,8 @@ export const renameCollection = async (
|
||||||
|
|
||||||
export const shareCollection = async (
|
export const shareCollection = async (
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
withUserEmail: string
|
withUserEmail: string,
|
||||||
|
role: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
|
@ -935,6 +936,7 @@ export const shareCollection = async (
|
||||||
const shareCollectionRequest = {
|
const shareCollectionRequest = {
|
||||||
collectionID: collection.id,
|
collectionID: collection.id,
|
||||||
email: withUserEmail,
|
email: withUserEmail,
|
||||||
|
role: role,
|
||||||
encryptedKey,
|
encryptedKey,
|
||||||
};
|
};
|
||||||
await HTTPService.post(
|
await HTTPService.post(
|
||||||
|
@ -1405,20 +1407,19 @@ export async function unhideToCollection(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const constructUserIDToEmailMap = async (): Promise<
|
export const constructUserIDToEmailMap = (
|
||||||
Map<number, string>
|
user: User,
|
||||||
> => {
|
collections: Collection[]
|
||||||
|
): Map<number, string> => {
|
||||||
try {
|
try {
|
||||||
const collection = await getLocalCollections();
|
|
||||||
const user: User = getData(LS_KEYS.USER);
|
|
||||||
const userIDToEmailMap = new Map<number, string>();
|
const userIDToEmailMap = new Map<number, string>();
|
||||||
collection.map((item) => {
|
collections.forEach((item) => {
|
||||||
const { owner, sharees } = item;
|
const { owner, sharees } = item;
|
||||||
if (user.id !== owner.id && owner.email) {
|
if (user.id !== owner.id && owner.email) {
|
||||||
userIDToEmailMap.set(owner.id, owner.email);
|
userIDToEmailMap.set(owner.id, owner.email);
|
||||||
}
|
}
|
||||||
if (sharees) {
|
if (sharees) {
|
||||||
sharees.map((item) => {
|
sharees.forEach((item) => {
|
||||||
if (item.id !== user.id)
|
if (item.id !== user.id)
|
||||||
userIDToEmailMap.set(item.id, item.email);
|
userIDToEmailMap.set(item.id, item.email);
|
||||||
});
|
});
|
||||||
|
@ -1428,6 +1429,31 @@ export const constructUserIDToEmailMap = async (): Promise<
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError('Error Mapping UserId to email:', e);
|
logError('Error Mapping UserId to email:', e);
|
||||||
return new Map<number, string>();
|
return new Map<number, string>();
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const constructEmailList = (
|
||||||
|
user: User,
|
||||||
|
collections: Collection[],
|
||||||
|
familyData: FamilyData
|
||||||
|
): string[] => {
|
||||||
|
const emails = collections
|
||||||
|
.map((item) => {
|
||||||
|
if (item.owner.email && item.owner.id !== user.id) {
|
||||||
|
return [item.owner.email];
|
||||||
|
} else {
|
||||||
|
const shareeEmails = item.sharees
|
||||||
|
.filter((sharee) => sharee.email !== user.email)
|
||||||
|
.map((sharee) => sharee.email);
|
||||||
|
return shareeEmails;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
// adding family members
|
||||||
|
if (familyData) {
|
||||||
|
const family = familyData.members.map((member) => member.email);
|
||||||
|
emails.push(...family);
|
||||||
|
}
|
||||||
|
return Array.from(new Set(emails));
|
||||||
|
};
|
||||||
|
|
|
@ -39,6 +39,7 @@ export type GalleryContextType = {
|
||||||
authenticateUser: (callback: () => void) => void;
|
authenticateUser: (callback: () => void) => void;
|
||||||
user: User;
|
user: User;
|
||||||
userIDToEmailMap: Map<number, string>;
|
userIDToEmailMap: Map<number, string>;
|
||||||
|
emailList: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum CollectionSelectorIntent {
|
export enum CollectionSelectorIntent {
|
||||||
|
|
|
@ -320,6 +320,8 @@ export const showShareQuickOption = (type: CollectionSummaryType) => {
|
||||||
type === CollectionSummaryType.outgoingShare ||
|
type === CollectionSummaryType.outgoingShare ||
|
||||||
type === CollectionSummaryType.sharedOnlyViaLink ||
|
type === CollectionSummaryType.sharedOnlyViaLink ||
|
||||||
type === CollectionSummaryType.archived ||
|
type === CollectionSummaryType.archived ||
|
||||||
|
type === CollectionSummaryType.incomingShareViewer ||
|
||||||
|
type === CollectionSummaryType.incomingShareCollaborator ||
|
||||||
type === CollectionSummaryType.pinned
|
type === CollectionSummaryType.pinned
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue