Share album redesign (#1210)

This commit is contained in:
Abhinav Kumar 2023-07-14 13:13:26 +05:30 committed by GitHub
commit 2307e8b093
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1249 additions and 429 deletions

View file

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

View file

@ -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')
}>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -133,6 +133,7 @@ export default function Collections(props: Iprops) {
/>
<CollectionShare
collectionSummary={collectionSummaries.get(activeCollectionID)}
open={collectionShareModalView}
onClose={closeCollectionShare}
collection={activeCollection.current}

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -39,6 +39,7 @@ export type GalleryContextType = {
authenticateUser: (callback: () => void) => void;
user: User;
userIDToEmailMap: Map<number, string>;
emailList: string[];
};
export enum CollectionSelectorIntent {

View file

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