Share album redesign (#1210)
This commit is contained in:
commit
2307e8b093
|
@ -355,6 +355,33 @@
|
|||
"INSTALL": "Install",
|
||||
"SHARING_DETAILS": "Sharing details",
|
||||
"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",
|
||||
"LINK_EXPIRED": "Link expired",
|
||||
"LINK_EXPIRED_MESSAGE": "This link has either expired or been disabled!",
|
||||
|
@ -490,7 +517,6 @@
|
|||
},
|
||||
"COPY_LINK": "Copy link",
|
||||
"DONE": "Done",
|
||||
"ADD_EMAIL_TITLE": "Share with specific people",
|
||||
"LINK_SHARE_TITLE": "Or share a link",
|
||||
"REMOVE_LINK": "Remove link",
|
||||
"CREATE_PUBLIC_SHARING": "Create public link",
|
||||
|
|
|
@ -20,12 +20,15 @@ export function ShareQuickOption({
|
|||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
// collectionSummaryType === CollectionSummaryType.incomingShare
|
||||
// ? t('SHARING_DETAILS')
|
||||
// :
|
||||
collectionSummaryType === CollectionSummaryType.outgoingShare ||
|
||||
collectionSummaryType ===
|
||||
CollectionSummaryType.sharedOnlyViaLink
|
||||
CollectionSummaryType.incomingShareViewer ||
|
||||
collectionSummaryType ===
|
||||
CollectionSummaryType.incomingShareCollaborator
|
||||
? t('SHARING_DETAILS')
|
||||
: collectionSummaryType ===
|
||||
CollectionSummaryType.outgoingShare ||
|
||||
collectionSummaryType ===
|
||||
CollectionSummaryType.sharedOnlyViaLink
|
||||
? t('MODIFY_SHARING')
|
||||
: 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 React from 'react';
|
||||
import { Collection } from 'types/collection';
|
||||
import { Collection, CollectionSummary } from 'types/collection';
|
||||
import { EnteDrawer } from 'components/EnteDrawer';
|
||||
import PublicShare from './publicShare';
|
||||
import WorkspacesIcon from '@mui/icons-material/Workspaces';
|
||||
import { t } from 'i18next';
|
||||
import MenuSectionTitle from 'components/Menu/MenuSectionTitle';
|
||||
import { DialogProps, Stack } from '@mui/material';
|
||||
import Titlebar from 'components/Titlebar';
|
||||
import EmailShare from './emailShare';
|
||||
import { CollectionSummaryType } from 'constants/collection';
|
||||
import SharingDetails from './sharingDetails';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
collection: Collection;
|
||||
collectionSummary: CollectionSummary;
|
||||
}
|
||||
|
||||
function CollectionShare(props: Props) {
|
||||
function CollectionShare({ collectionSummary, ...props }: Props) {
|
||||
const handleRootClose = () => {
|
||||
props.onClose();
|
||||
};
|
||||
|
@ -29,36 +29,53 @@ function CollectionShare(props: Props) {
|
|||
if (!props.collection) {
|
||||
return <></>;
|
||||
}
|
||||
const { type } = collectionSummary;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EnteDrawer
|
||||
anchor="right"
|
||||
open={props.open}
|
||||
onClose={handleDrawerClose}
|
||||
BackdropProps={{
|
||||
<EnteDrawer
|
||||
anchor="right"
|
||||
open={props.open}
|
||||
onClose={handleDrawerClose}
|
||||
slotProps={{
|
||||
backdrop: {
|
||||
sx: { '&&&': { backgroundColor: 'transparent' } },
|
||||
}}>
|
||||
<Stack spacing={'4px'} py={'12px'}>
|
||||
<Titlebar
|
||||
onClose={props.onClose}
|
||||
title={t('SHARE_COLLECTION')}
|
||||
onRootClose={handleRootClose}
|
||||
/>
|
||||
<Stack py={'20px'} px={'8px'}>
|
||||
<MenuSectionTitle
|
||||
title={t('ADD_EMAIL_TITLE')}
|
||||
icon={<WorkspacesIcon />}
|
||||
/>
|
||||
<EmailShare collection={props.collection} />
|
||||
<PublicShare
|
||||
},
|
||||
}}>
|
||||
<Stack spacing={'4px'} py={'12px'}>
|
||||
<Titlebar
|
||||
onClose={props.onClose}
|
||||
title={
|
||||
type ===
|
||||
CollectionSummaryType.incomingShareCollaborator ||
|
||||
type === CollectionSummaryType.incomingShareViewer
|
||||
? t('SHARING_DETAILS')
|
||||
: t('SHARE_COLLECTION')
|
||||
}
|
||||
onRootClose={handleRootClose}
|
||||
caption={props.collection.name}
|
||||
/>
|
||||
<Stack py={'20px'} px={'8px'} gap={'24px'}>
|
||||
{type === CollectionSummaryType.incomingShareCollaborator ||
|
||||
type === CollectionSummaryType.incomingShareViewer ? (
|
||||
<SharingDetails
|
||||
collection={props.collection}
|
||||
onRootClose={handleRootClose}
|
||||
type={type}
|
||||
/>
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
<EmailShare
|
||||
collection={props.collection}
|
||||
onRootClose={handleRootClose}
|
||||
/>
|
||||
<PublicShare
|
||||
collection={props.collection}
|
||||
onRootClose={handleRootClose}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
</>
|
||||
</Stack>
|
||||
</EnteDrawer>
|
||||
);
|
||||
}
|
||||
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
|
||||
collectionSummary={collectionSummaries.get(activeCollectionID)}
|
||||
open={collectionShareModalView}
|
||||
onClose={closeCollectionShare}
|
||||
collection={activeCollection.current}
|
||||
|
|
|
@ -58,7 +58,6 @@ const MapBox: React.FC<MapBoxProps> = ({
|
|||
} else {
|
||||
if (mapContainer && mapContainer.hasChildNodes()) {
|
||||
if (mapContainer.firstChild) {
|
||||
console.log('removing child');
|
||||
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';
|
||||
|
||||
interface AvatarProps {
|
||||
file: EnteFile;
|
||||
file?: EnteFile;
|
||||
email?: string;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
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`};
|
||||
height: ${({ size }) => `${size}px`};
|
||||
background-color: ${({ colorCode }) => `${colorCode}95`};
|
||||
background-color: ${({ colorCode, opacity }) =>
|
||||
`${colorCode}${opacity === 100 ? '' : opacity ?? 95}`};
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -24,7 +31,7 @@ const AvatarBase = styled('div')<{ colorCode: string; size: number }>`
|
|||
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 theme = useTheme();
|
||||
|
||||
|
@ -33,6 +40,9 @@ const Avatar: React.FC<AvatarProps> = ({ file }) => {
|
|||
|
||||
useLayoutEffect(() => {
|
||||
try {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (file.ownerID !== user.id) {
|
||||
// getting email from in-memory id-email map
|
||||
const email = userIDToEmailMap.get(file.ownerID);
|
||||
|
@ -58,16 +68,43 @@ const Avatar: React.FC<AvatarProps> = ({ file }) => {
|
|||
setColorCode(PUBLIC_COLLECTED_FILES_AVATAR_COLOR_CODE);
|
||||
}
|
||||
} 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) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AvatarBase size={18} colorCode={colorCode}>
|
||||
<AvatarBase size={18} colorCode={colorCode} opacity={opacity}>
|
||||
{userLetter}
|
||||
</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,
|
||||
getCollectionSummaries,
|
||||
moveToHiddenCollection,
|
||||
constructEmailList,
|
||||
} from 'services/collectionService';
|
||||
import { t } from 'i18next';
|
||||
|
||||
|
@ -107,7 +108,7 @@ import SearchResultInfo from 'components/Search/SearchResultInfo';
|
|||
import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList';
|
||||
import UploadInputs from 'components/UploadSelectorInputs';
|
||||
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 { CenteredFlex } from 'components/Container';
|
||||
import { checkConnectivity } from 'utils/common';
|
||||
|
@ -124,6 +125,7 @@ import { isSameDayAnyYear, isInsideLocationTag } from 'utils/search';
|
|||
import { getSessionExpiredMessage } from 'utils/ui';
|
||||
import { syncEntities } from 'services/entityService';
|
||||
import { constructUserIDToEmailMap } from 'services/collectionService';
|
||||
import { getLocalFamilyData } from 'utils/user/family';
|
||||
|
||||
export const DeadCenter = styled('div')`
|
||||
flex: 1;
|
||||
|
@ -147,6 +149,7 @@ const defaultGalleryContext: GalleryContextType = {
|
|||
authenticateUser: () => null,
|
||||
user: null,
|
||||
userIDToEmailMap: null,
|
||||
emailList: null,
|
||||
};
|
||||
|
||||
export const GalleryContext = createContext<GalleryContextType>(
|
||||
|
@ -156,6 +159,7 @@ export const GalleryContext = createContext<GalleryContextType>(
|
|||
export default function Gallery() {
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState(null);
|
||||
const [familyData, setFamilyData] = useState<FamilyData>(null);
|
||||
const [collections, setCollections] = useState<Collection[]>(null);
|
||||
const [files, setFiles] = useState<EnteFile[]>(null);
|
||||
const [hiddenFiles, setHiddenFiles] = useState<EnteFile[]>(null);
|
||||
|
@ -224,6 +228,7 @@ export default function Gallery() {
|
|||
useState<CollectionSummaries>();
|
||||
const [userIDToEmailMap, setUserIDToEmailMap] =
|
||||
useState<Map<number, string>>(null);
|
||||
const [emailList, setEmailList] = useState<string[]>(null);
|
||||
const [activeCollection, setActiveCollection] = useState<number>(undefined);
|
||||
const [fixCreationTimeView, setFixCreationTimeView] = useState(false);
|
||||
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
|
||||
|
@ -283,6 +288,7 @@ export default function Gallery() {
|
|||
}
|
||||
setIsFirstLogin(false);
|
||||
const user = getData(LS_KEYS.USER);
|
||||
const familyData = getLocalFamilyData();
|
||||
const files = sortFiles(mergeMetadata(await getLocalFiles()));
|
||||
const hiddenFiles = sortFiles(
|
||||
mergeMetadata(await getLocalHiddenFiles())
|
||||
|
@ -291,6 +297,7 @@ export default function Gallery() {
|
|||
const trashedFiles = await getLocalTrashedFiles();
|
||||
|
||||
setUser(user);
|
||||
setFamilyData(familyData);
|
||||
setFiles(files);
|
||||
setTrashedFiles(trashedFiles);
|
||||
setHiddenFiles(hiddenFiles);
|
||||
|
@ -321,16 +328,21 @@ export default function Gallery() {
|
|||
}, [collections, files, hiddenFiles, trashedFiles, user]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!collections) {
|
||||
return;
|
||||
}
|
||||
const userIdToEmailMap = await constructUserIDToEmailMap();
|
||||
setUserIDToEmailMap(userIdToEmailMap);
|
||||
};
|
||||
fetchData();
|
||||
if (!collections || !user) {
|
||||
return;
|
||||
}
|
||||
const userIdToEmailMap = constructUserIDToEmailMap(user, collections);
|
||||
setUserIDToEmailMap(userIdToEmailMap);
|
||||
}, [collections]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !collections) {
|
||||
return;
|
||||
}
|
||||
const emailList = constructEmailList(user, collections, familyData);
|
||||
setEmailList(emailList);
|
||||
}, [user, collections, familyData]);
|
||||
|
||||
useEffect(() => {
|
||||
collectionSelectorAttributes && setCollectionSelectorView(true);
|
||||
}, [collectionSelectorAttributes]);
|
||||
|
@ -901,6 +913,7 @@ export default function Gallery() {
|
|||
authenticateUser,
|
||||
userIDToEmailMap,
|
||||
user,
|
||||
emailList,
|
||||
}}>
|
||||
<FullScreenDropZone
|
||||
getDragAndDropRootProps={getDragAndDropRootProps}>
|
||||
|
|
|
@ -53,7 +53,7 @@ import AddPhotoAlternateOutlined from '@mui/icons-material/AddPhotoAlternateOutl
|
|||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
import { UploadTypeSelectorIntent } from 'types/gallery';
|
||||
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 { OverflowMenuOption } from 'components/OverflowMenu/option';
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ import {
|
|||
isPinnedCollection,
|
||||
updateMagicMetadata,
|
||||
} from 'utils/magicMetadata';
|
||||
import { User } from 'types/user';
|
||||
import { FamilyData, User } from 'types/user';
|
||||
import {
|
||||
isQuickLinkCollection,
|
||||
isOutgoingShare,
|
||||
|
@ -922,7 +922,8 @@ export const renameCollection = async (
|
|||
|
||||
export const shareCollection = async (
|
||||
collection: Collection,
|
||||
withUserEmail: string
|
||||
withUserEmail: string,
|
||||
role: string
|
||||
) => {
|
||||
try {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
|
@ -935,6 +936,7 @@ export const shareCollection = async (
|
|||
const shareCollectionRequest = {
|
||||
collectionID: collection.id,
|
||||
email: withUserEmail,
|
||||
role: role,
|
||||
encryptedKey,
|
||||
};
|
||||
await HTTPService.post(
|
||||
|
@ -1405,20 +1407,19 @@ export async function unhideToCollection(
|
|||
}
|
||||
}
|
||||
|
||||
export const constructUserIDToEmailMap = async (): Promise<
|
||||
Map<number, string>
|
||||
> => {
|
||||
export const constructUserIDToEmailMap = (
|
||||
user: User,
|
||||
collections: Collection[]
|
||||
): Map<number, string> => {
|
||||
try {
|
||||
const collection = await getLocalCollections();
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
const userIDToEmailMap = new Map<number, string>();
|
||||
collection.map((item) => {
|
||||
collections.forEach((item) => {
|
||||
const { owner, sharees } = item;
|
||||
if (user.id !== owner.id && owner.email) {
|
||||
userIDToEmailMap.set(owner.id, owner.email);
|
||||
}
|
||||
if (sharees) {
|
||||
sharees.map((item) => {
|
||||
sharees.forEach((item) => {
|
||||
if (item.id !== user.id)
|
||||
userIDToEmailMap.set(item.id, item.email);
|
||||
});
|
||||
|
@ -1428,6 +1429,31 @@ export const constructUserIDToEmailMap = async (): Promise<
|
|||
} catch (e) {
|
||||
logError('Error Mapping UserId to email:', e);
|
||||
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;
|
||||
user: User;
|
||||
userIDToEmailMap: Map<number, string>;
|
||||
emailList: string[];
|
||||
};
|
||||
|
||||
export enum CollectionSelectorIntent {
|
||||
|
|
|
@ -320,6 +320,8 @@ export const showShareQuickOption = (type: CollectionSummaryType) => {
|
|||
type === CollectionSummaryType.outgoingShare ||
|
||||
type === CollectionSummaryType.sharedOnlyViaLink ||
|
||||
type === CollectionSummaryType.archived ||
|
||||
type === CollectionSummaryType.incomingShareViewer ||
|
||||
type === CollectionSummaryType.incomingShareCollaborator ||
|
||||
type === CollectionSummaryType.pinned
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue