Inline
This commit is contained in:
@@ -3,43 +3,65 @@ import {
|
||||
MenuItemGroup,
|
||||
MenuSectionTitle,
|
||||
} from "@/base/components/Menu";
|
||||
import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
|
||||
import { LoadingButton } from "@/base/components/mui/LoadingButton";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import { useModalVisibility } from "@/base/components/utils/modal";
|
||||
import { sharedCryptoWorker } from "@/base/crypto";
|
||||
import log from "@/base/log";
|
||||
import type {
|
||||
Collection,
|
||||
PublicURL,
|
||||
UpdatePublicURL,
|
||||
} from "@/media/collection";
|
||||
import { COLLECTION_ROLE } from "@/media/collection";
|
||||
import { COLLECTION_ROLE, type CollectionUser } from "@/media/collection";
|
||||
import { PublicLinkCreated } from "@/new/photos/components/share/PublicLinkCreated";
|
||||
import type { CollectionSummary } from "@/new/photos/services/collection/ui";
|
||||
import { useAppContext } from "@/new/photos/types/context";
|
||||
import { AppContext, useAppContext } from "@/new/photos/types/context";
|
||||
import { FlexWrapper } from "@ente/shared/components/Container";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import SingleInputForm, {
|
||||
type SingleInputFormProps,
|
||||
} from "@ente/shared/components/SingleInputForm";
|
||||
import { formatDateTime } from "@ente/shared/time/format";
|
||||
import { default as Add, default as AddIcon } from "@mui/icons-material/Add";
|
||||
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import BlockIcon from "@mui/icons-material/Block";
|
||||
import ChevronRightIcon, {
|
||||
default as ChevronRight,
|
||||
} from "@mui/icons-material/ChevronRight";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopyOutlined";
|
||||
import DoneIcon from "@mui/icons-material/Done";
|
||||
import DownloadSharp from "@mui/icons-material/DownloadSharp";
|
||||
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||
import LinkIcon from "@mui/icons-material/Link";
|
||||
import ModeEditIcon from "@mui/icons-material/ModeEdit";
|
||||
import Photo from "@mui/icons-material/Photo";
|
||||
import Photo, { default as PhotoIcon } from "@mui/icons-material/Photo";
|
||||
import PublicIcon from "@mui/icons-material/Public";
|
||||
import RemoveCircleOutline from "@mui/icons-material/RemoveCircleOutline";
|
||||
import { Dialog, DialogProps, Stack, Typography } from "@mui/material";
|
||||
import Workspaces from "@mui/icons-material/Workspaces";
|
||||
import {
|
||||
Dialog,
|
||||
DialogProps,
|
||||
FormHelperText,
|
||||
Stack,
|
||||
styled,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import NumberAvatar from "@mui/material/Avatar";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Avatar from "components/pages/gallery/Avatar";
|
||||
import { Formik, type FormikHelpers } from "formik";
|
||||
import { t } from "i18next";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import {
|
||||
createShareableURL,
|
||||
deleteShareableURL,
|
||||
shareCollection,
|
||||
unshareCollection,
|
||||
updateShareableURL,
|
||||
} from "services/collectionService";
|
||||
import { SetPublicShareProp } from "types/publicCollection";
|
||||
@@ -49,7 +71,7 @@ import {
|
||||
shareExpiryOptions,
|
||||
} from "utils/collection";
|
||||
import { handleSharingErrors } from "utils/error/ui";
|
||||
import EmailShare from "./emailShare";
|
||||
import * as Yup from "yup";
|
||||
|
||||
interface CollectionShareProps {
|
||||
open: boolean;
|
||||
@@ -297,6 +319,870 @@ const EnablePublicShareOptions: React.FC<EnablePublicShareOptionsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
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", {
|
||||
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
|
||||
? 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
interface AddParticipantProps {
|
||||
collection: Collection;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onRootClose: () => void;
|
||||
type: COLLECTION_ROLE.VIEWER | COLLECTION_ROLE.COLLABORATOR;
|
||||
}
|
||||
|
||||
const AddParticipant: React.FC<AddParticipantProps> = ({
|
||||
open,
|
||||
collection,
|
||||
onClose,
|
||||
onRootClose,
|
||||
type,
|
||||
}) => {
|
||||
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 ({
|
||||
email,
|
||||
emails,
|
||||
}) => {
|
||||
// if email is provided, means user has custom entered email, so, will need to validate for self sharing
|
||||
// and already shared
|
||||
if (email) {
|
||||
if (email === user.email) {
|
||||
throw new Error(t("SHARE_WITH_SELF"));
|
||||
} else if (
|
||||
collection?.sharees?.find((value) => value.email === email)
|
||||
) {
|
||||
throw new Error(t("ALREADY_SHARED", { email: email }));
|
||||
}
|
||||
// set emails to array of one email
|
||||
emails = [email];
|
||||
}
|
||||
for (const email of emails) {
|
||||
if (
|
||||
email === user.email ||
|
||||
collection?.sharees?.find((value) => value.email === email)
|
||||
) {
|
||||
// can just skip this email
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await shareCollection(collection, email, type);
|
||||
await syncWithRemote(false, true);
|
||||
} catch (e) {
|
||||
const errorMessage = handleSharingErrors(e);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRootClose = () => {
|
||||
onClose();
|
||||
onRootClose();
|
||||
};
|
||||
|
||||
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
|
||||
if (reason === "backdropClick") {
|
||||
handleRootClose();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarDrawer 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>
|
||||
</SidebarDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
interface AddParticipantFormValues {
|
||||
inputValue: string;
|
||||
selectedOptions: string[];
|
||||
}
|
||||
|
||||
export interface AddParticipantFormProps {
|
||||
callback: (props: { email?: string; emails?: string[] }) => 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[];
|
||||
}
|
||||
|
||||
const AddParticipantForm: React.FC<AddParticipantFormProps> = (props) => {
|
||||
const { submitButtonProps } = props;
|
||||
const { sx: buttonSx, ...restSubmitButtonProps } = submitButtonProps ?? {};
|
||||
|
||||
const [loading, SetLoading] = useState(false);
|
||||
|
||||
const submitForm = async (
|
||||
values: AddParticipantFormValues,
|
||||
{ setFieldError, resetForm }: FormikHelpers<AddParticipantFormValues>,
|
||||
) => {
|
||||
try {
|
||||
SetLoading(true);
|
||||
if (values.inputValue !== "") {
|
||||
await props.callback({ email: values.inputValue });
|
||||
} else if (values.selectedOptions.length !== 0) {
|
||||
await props.callback({ emails: values.selectedOptions });
|
||||
}
|
||||
SetLoading(false);
|
||||
props.onClose();
|
||||
resetForm();
|
||||
} catch (e) {
|
||||
setFieldError("inputValue", e?.message);
|
||||
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")),
|
||||
});
|
||||
}
|
||||
}, [props.fieldType]);
|
||||
|
||||
const handleInputFieldClick = (setFieldValue) => {
|
||||
setFieldValue("selectedOptions", []);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik<AddParticipantFormValues>
|
||||
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}
|
||||
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 && (
|
||||
<FocusVisibleButton
|
||||
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")}
|
||||
</FocusVisibleButton>
|
||||
)}
|
||||
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
color="accent"
|
||||
fullWidth
|
||||
buttonText={props.buttonText}
|
||||
loading={loading}
|
||||
sx={{ mt: 2, mb: 4 }}
|
||||
{...restSubmitButtonProps}
|
||||
>
|
||||
{props.buttonText}
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</FlexWrapper>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
interface ManageEmailShareProps {
|
||||
collection: Collection;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onRootClose: () => void;
|
||||
peopleCount: number;
|
||||
}
|
||||
|
||||
const ManageEmailShare: React.FC<ManageEmailShareProps> = ({
|
||||
open,
|
||||
collection,
|
||||
onClose,
|
||||
onRootClose,
|
||||
peopleCount,
|
||||
}) => {
|
||||
const { showLoadingBar, hideLoadingBar } = 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 {
|
||||
showLoadingBar();
|
||||
await unshareCollection(collection, email);
|
||||
await galleryContext.syncWithRemote(false, true);
|
||||
} finally {
|
||||
hideLoadingBar();
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<SidebarDrawer
|
||||
anchor="right"
|
||||
open={open}
|
||||
onClose={handleDrawerClose}
|
||||
>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<Titlebar
|
||||
onClose={onClose}
|
||||
title={collection.name}
|
||||
onRootClose={handleRootClose}
|
||||
caption={t("participants_count", {
|
||||
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>
|
||||
</SidebarDrawer>
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface ManageParticipantProps {
|
||||
open: boolean;
|
||||
collection: Collection;
|
||||
onClose: () => void;
|
||||
onRootClose: () => void;
|
||||
selectedParticipant: CollectionUser;
|
||||
collectionUnshare: (email: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const ManageParticipant: React.FC<ManageParticipantProps> = ({
|
||||
collection,
|
||||
open,
|
||||
onClose,
|
||||
onRootClose,
|
||||
selectedParticipant,
|
||||
collectionUnshare,
|
||||
}) => {
|
||||
const { showMiniDialog } = useAppContext();
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
|
||||
if (reason === "backdropClick") {
|
||||
onRootClose();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
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) {
|
||||
log.error(handleSharingErrors(e), e);
|
||||
}
|
||||
};
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
showMiniDialog({
|
||||
title: t("CHANGE_PERMISSION"),
|
||||
message: contentText,
|
||||
continue: {
|
||||
text: buttonText,
|
||||
color: "critical",
|
||||
action: () => updateCollectionRole(selectedEmail, newRole),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const removeParticipant = () => {
|
||||
showMiniDialog({
|
||||
title: t("REMOVE_PARTICIPANT"),
|
||||
message: (
|
||||
<Trans
|
||||
i18nKey="REMOVE_PARTICIPANT_MESSAGE"
|
||||
values={{
|
||||
selectedEmail: `${selectedParticipant.email}`,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
continue: {
|
||||
text: t("CONFIRM_REMOVE"),
|
||||
color: "critical",
|
||||
action: handleRemove,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!selectedParticipant) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarDrawer 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="critical"
|
||||
fontWeight="normal"
|
||||
onClick={removeParticipant}
|
||||
label={"Remove"}
|
||||
startIcon={<BlockIcon />}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</SidebarDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
interface PublicShareProps {
|
||||
collection: Collection;
|
||||
onRootClose: () => void;
|
||||
@@ -1,122 +0,0 @@
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import { COLLECTION_ROLE, type Collection } from "@/media/collection";
|
||||
import { DialogProps, Stack } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
|
||||
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 ({
|
||||
email,
|
||||
emails,
|
||||
}) => {
|
||||
// if email is provided, means user has custom entered email, so, will need to validate for self sharing
|
||||
// and already shared
|
||||
if (email) {
|
||||
if (email === user.email) {
|
||||
throw new Error(t("SHARE_WITH_SELF"));
|
||||
} else if (
|
||||
collection?.sharees?.find((value) => value.email === email)
|
||||
) {
|
||||
throw new Error(t("ALREADY_SHARED", { email: email }));
|
||||
}
|
||||
// set emails to array of one email
|
||||
emails = [email];
|
||||
}
|
||||
for (const email of emails) {
|
||||
if (
|
||||
email === user.email ||
|
||||
collection?.sharees?.find((value) => value.email === email)
|
||||
) {
|
||||
// can just skip this email
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await shareCollection(collection, email, type);
|
||||
await syncWithRemote(false, true);
|
||||
} catch (e) {
|
||||
const errorMessage = handleSharingErrors(e);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRootClose = () => {
|
||||
onClose();
|
||||
onRootClose();
|
||||
};
|
||||
|
||||
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
|
||||
if (reason === "backdropClick") {
|
||||
handleRootClose();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarDrawer 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>
|
||||
</SidebarDrawer>
|
||||
);
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
import {
|
||||
MenuItemDivider,
|
||||
MenuItemGroup,
|
||||
MenuSectionTitle,
|
||||
} from "@/base/components/Menu";
|
||||
import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
|
||||
import { LoadingButton } from "@/base/components/mui/LoadingButton";
|
||||
import { FlexWrapper } from "@ente/shared/components/Container";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import DoneIcon from "@mui/icons-material/Done";
|
||||
import { FormHelperText, Stack } from "@mui/material";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Avatar from "components/pages/gallery/Avatar";
|
||||
import { Formik, type FormikHelpers } from "formik";
|
||||
import { t } from "i18next";
|
||||
import { useMemo, useState } from "react";
|
||||
import * as Yup from "yup";
|
||||
|
||||
interface formValues {
|
||||
inputValue: string;
|
||||
selectedOptions: string[];
|
||||
}
|
||||
export interface AddParticipantFormProps {
|
||||
callback: (props: { email?: string; emails?: string[] }) => 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 [loading, SetLoading] = useState(false);
|
||||
|
||||
const submitForm = async (
|
||||
values: formValues,
|
||||
{ setFieldError, resetForm }: FormikHelpers<formValues>,
|
||||
) => {
|
||||
try {
|
||||
SetLoading(true);
|
||||
if (values.inputValue !== "") {
|
||||
await props.callback({ email: values.inputValue });
|
||||
} else if (values.selectedOptions.length !== 0) {
|
||||
await props.callback({ emails: values.selectedOptions });
|
||||
}
|
||||
SetLoading(false);
|
||||
props.onClose();
|
||||
resetForm();
|
||||
} catch (e) {
|
||||
setFieldError("inputValue", e?.message);
|
||||
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")),
|
||||
});
|
||||
}
|
||||
}, [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}
|
||||
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 && (
|
||||
<FocusVisibleButton
|
||||
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")}
|
||||
</FocusVisibleButton>
|
||||
)}
|
||||
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
color="accent"
|
||||
fullWidth
|
||||
buttonText={props.buttonText}
|
||||
loading={loading}
|
||||
sx={{ mt: 2, mb: 4 }}
|
||||
{...restSubmitButtonProps}
|
||||
>
|
||||
{props.buttonText}
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</FlexWrapper>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
import {
|
||||
MenuItemDivider,
|
||||
MenuItemGroup,
|
||||
MenuSectionTitle,
|
||||
} from "@/base/components/Menu";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import {
|
||||
COLLECTION_ROLE,
|
||||
type Collection,
|
||||
type CollectionUser,
|
||||
} from "@/media/collection";
|
||||
import { AppContext } from "@/new/photos/types/context";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import Add from "@mui/icons-material/Add";
|
||||
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import ModeEditIcon from "@mui/icons-material/ModeEdit";
|
||||
import Photo from "@mui/icons-material/Photo";
|
||||
import { DialogProps, Stack } from "@mui/material";
|
||||
import Avatar from "components/pages/gallery/Avatar";
|
||||
import { t } from "i18next";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import { useContext, useRef, useState } from "react";
|
||||
import { unshareCollection } from "services/collectionService";
|
||||
import AddParticipant from "./AddParticipant";
|
||||
import ManageParticipant from "./ManageParticipant";
|
||||
|
||||
interface Iprops {
|
||||
collection: Collection;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onRootClose: () => void;
|
||||
peopleCount: number;
|
||||
}
|
||||
|
||||
export default function ManageEmailShare({
|
||||
open,
|
||||
collection,
|
||||
onClose,
|
||||
onRootClose,
|
||||
peopleCount,
|
||||
}: Iprops) {
|
||||
const { showLoadingBar, hideLoadingBar } = 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 {
|
||||
showLoadingBar();
|
||||
await unshareCollection(collection, email);
|
||||
await galleryContext.syncWithRemote(false, true);
|
||||
} finally {
|
||||
hideLoadingBar();
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<SidebarDrawer
|
||||
anchor="right"
|
||||
open={open}
|
||||
onClose={handleDrawerClose}
|
||||
>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<Titlebar
|
||||
onClose={onClose}
|
||||
title={collection.name}
|
||||
onRootClose={handleRootClose}
|
||||
caption={t("participants_count", {
|
||||
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>
|
||||
</SidebarDrawer>
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
|
||||
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import log from "@/base/log";
|
||||
import type { Collection, CollectionUser } from "@/media/collection";
|
||||
import { useAppContext } from "@/new/photos/types/context";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import BlockIcon from "@mui/icons-material/Block";
|
||||
import DoneIcon from "@mui/icons-material/Done";
|
||||
import ModeEditIcon from "@mui/icons-material/ModeEdit";
|
||||
import PhotoIcon from "@mui/icons-material/Photo";
|
||||
import { DialogProps, Stack, Typography } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import { useContext } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { shareCollection } from "services/collectionService";
|
||||
import { handleSharingErrors } from "utils/error/ui";
|
||||
|
||||
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 { showMiniDialog } = useAppContext();
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
|
||||
if (reason === "backdropClick") {
|
||||
onRootClose();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
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) {
|
||||
log.error(handleSharingErrors(e), e);
|
||||
}
|
||||
};
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
showMiniDialog({
|
||||
title: t("CHANGE_PERMISSION"),
|
||||
message: contentText,
|
||||
continue: {
|
||||
text: buttonText,
|
||||
color: "critical",
|
||||
action: () => updateCollectionRole(selectedEmail, newRole),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const removeParticipant = () => {
|
||||
showMiniDialog({
|
||||
title: t("REMOVE_PARTICIPANT"),
|
||||
message: (
|
||||
<Trans
|
||||
i18nKey="REMOVE_PARTICIPANT_MESSAGE"
|
||||
values={{
|
||||
selectedEmail: `${selectedParticipant.email}`,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
continue: {
|
||||
text: t("CONFIRM_REMOVE"),
|
||||
color: "critical",
|
||||
action: handleRemove,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!selectedParticipant) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarDrawer 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="critical"
|
||||
fontWeight="normal"
|
||||
onClick={removeParticipant}
|
||||
label={"Remove"}
|
||||
startIcon={<BlockIcon />}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</SidebarDrawer>
|
||||
);
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import { COLLECTION_ROLE, type Collection } from "@/media/collection";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
MenuItemDivider,
|
||||
MenuItemGroup,
|
||||
MenuSectionTitle,
|
||||
} from "@/base/components/Menu";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||
import Workspaces from "@mui/icons-material/Workspaces";
|
||||
import { Stack, styled } from "@mui/material";
|
||||
import NumberAvatar from "@mui/material/Avatar";
|
||||
import Avatar from "components/pages/gallery/Avatar";
|
||||
import { t } from "i18next";
|
||||
import AddParticipant from "./AddParticipant";
|
||||
import ManageEmailShare from "./ManageEmailShare";
|
||||
|
||||
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", {
|
||||
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
|
||||
? 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user