[web] File list code cleanup (#6491)
This commit is contained in:
@@ -9,7 +9,7 @@ import { imageURLGenerator } from "services/render";
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const [isEmpty, setIsEmpty] = useState(false);
|
||||
const [imageURL, setImageURL] = useState<string | undefined>();
|
||||
const [imageURL, setImageURL] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -1347,9 +1347,7 @@ const ManagePublicShareOptions: React.FC<ManagePublicShareOptionsProps> = ({
|
||||
setBlockingLoad,
|
||||
onRemotePull,
|
||||
}) => {
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const [copied, handleCopyLink] = useClipboardCopy(resolvedURL);
|
||||
|
||||
@@ -1362,7 +1360,7 @@ const ManagePublicShareOptions: React.FC<ManagePublicShareOptionsProps> = ({
|
||||
updates: UpdatePublicURLAttributes,
|
||||
) => {
|
||||
setBlockingLoad(true);
|
||||
setErrorMessage(undefined);
|
||||
setErrorMessage("");
|
||||
try {
|
||||
setPublicURL(await updatePublicURL(collection.id, updates));
|
||||
void onRemotePull({ silent: true });
|
||||
@@ -1375,7 +1373,7 @@ const ManagePublicShareOptions: React.FC<ManagePublicShareOptionsProps> = ({
|
||||
};
|
||||
const handleRemovePublicLink = async () => {
|
||||
setBlockingLoad(true);
|
||||
setErrorMessage(undefined);
|
||||
setErrorMessage("");
|
||||
try {
|
||||
await deleteShareURL(collection.id);
|
||||
setPublicURL(undefined);
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
CollectionShare,
|
||||
type CollectionShareProps,
|
||||
} from "components/Collections/CollectionShare";
|
||||
import type { TimeStampListItem } from "components/FileList";
|
||||
import type { FileListHeaderOrFooter } from "components/FileList";
|
||||
import { useModalVisibility } from "ente-base/components/utils/modal";
|
||||
import {
|
||||
isSaveCancelled,
|
||||
@@ -49,7 +49,7 @@ type GalleryBarAndListHeaderProps = Omit<
|
||||
barCollectionSummaries: CollectionSummaries;
|
||||
activeCollection: Collection;
|
||||
setActiveCollectionID: (collectionID: number) => void;
|
||||
setPhotoListHeader: (value: TimeStampListItem) => void;
|
||||
setFileListHeader: (header: FileListHeaderOrFooter) => void;
|
||||
saveGroups: SaveGroup[];
|
||||
} & Pick<CollectionHeaderProps, "onRemotePull" | "onAddSaveGroup"> &
|
||||
Pick<
|
||||
@@ -62,11 +62,11 @@ type GalleryBarAndListHeaderProps = Omit<
|
||||
* dialogs that might be triggered by actions on either the bar or the header..
|
||||
*
|
||||
* This component manages the sticky horizontally scrollable bar shown at the
|
||||
* top of the gallery, AND the non-sticky header shown below the bar, at the top
|
||||
* of the actual list of items.
|
||||
* top of the gallery, AND the (non-sticky) header shown below the bar, at the
|
||||
* top of the actual list of items.
|
||||
*
|
||||
* These are disparate views - indeed, the list header is not even a child of
|
||||
* this component but is instead proxied via {@link setPhotoListHeader}. Still,
|
||||
* this component but is instead proxied via {@link setFileListHeader}. Still,
|
||||
* having this intermediate wrapper component allows us to move some of the
|
||||
* common concerns shared by both the gallery bar and list header (e.g. some
|
||||
* dialogs that can be invoked from both places) into this file instead of
|
||||
@@ -95,7 +95,7 @@ export const GalleryBarAndListHeader: React.FC<
|
||||
onRemotePull,
|
||||
onAddSaveGroup,
|
||||
onSelectPerson,
|
||||
setPhotoListHeader,
|
||||
setFileListHeader,
|
||||
}) => {
|
||||
const { show: showAllAlbums, props: allAlbumsVisibilityProps } =
|
||||
useModalVisibility();
|
||||
@@ -134,7 +134,7 @@ export const GalleryBarAndListHeader: React.FC<
|
||||
useEffect(() => {
|
||||
if (shouldHide) return;
|
||||
|
||||
setPhotoListHeader({
|
||||
setFileListHeader({
|
||||
item:
|
||||
mode != "people" ? (
|
||||
<CollectionHeader
|
||||
@@ -159,7 +159,6 @@ export const GalleryBarAndListHeader: React.FC<
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
tag: "header",
|
||||
height: 68,
|
||||
});
|
||||
}, [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import AlbumOutlinedIcon from "@mui/icons-material/AlbumOutlined";
|
||||
import FavoriteRoundedIcon from "@mui/icons-material/FavoriteRounded";
|
||||
import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined";
|
||||
import { Box, Checkbox, Link, Typography, styled } from "@mui/material";
|
||||
import { Box, Checkbox, Typography, styled } from "@mui/material";
|
||||
import Avatar from "components/Avatar";
|
||||
import type { LocalUser } from "ente-accounts/services/user";
|
||||
import { assertionFailed } from "ente-base/assert";
|
||||
@@ -31,7 +31,6 @@ import { PseudoCollectionID } from "ente-new/photos/services/collection-summary"
|
||||
import { t } from "i18next";
|
||||
import memoize from "memoize-one";
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import {
|
||||
VariableSizeList as List,
|
||||
type ListChildComponentProps,
|
||||
@@ -49,18 +48,27 @@ export const SPACE_BTW_DATES = 44;
|
||||
|
||||
const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244;
|
||||
|
||||
const FOOTER_HEIGHT = 90;
|
||||
const ALBUM_FOOTER_HEIGHT = 75;
|
||||
const ALBUM_FOOTER_HEIGHT_WITH_REFERRAL = 113;
|
||||
/**
|
||||
* A component with an explicit height suitable for being plugged in as the
|
||||
* {@link header} or {@link footer} of the {@link FileList}.
|
||||
*/
|
||||
export interface FileListHeaderOrFooter {
|
||||
/**
|
||||
* The component itself.
|
||||
*/
|
||||
item: React.ReactNode;
|
||||
/**
|
||||
* The height of the component (in px).
|
||||
*/
|
||||
height: number;
|
||||
}
|
||||
|
||||
export type FileListItemTag = "header" | "publicAlbumsFooter" | "date" | "file";
|
||||
|
||||
export interface TimeStampListItem {
|
||||
interface TimeStampListItem {
|
||||
/**
|
||||
* An optional {@link FileListItemTag} that can be used to identify item
|
||||
* types for conditional behaviour.
|
||||
*/
|
||||
tag?: FileListItemTag;
|
||||
tag?: "date" | "file";
|
||||
items?: FileListAnnotatedFile[];
|
||||
itemStartIndex?: number;
|
||||
date?: string;
|
||||
@@ -122,6 +130,18 @@ export interface FileListProps {
|
||||
* another mode in which the gallery operates.
|
||||
*/
|
||||
modePlus?: GalleryBarMode | "search";
|
||||
/**
|
||||
* An optional component shown before all the items in the list.
|
||||
*
|
||||
* It is not sticky, and scrolls along with the content of the list.
|
||||
*/
|
||||
header?: FileListHeaderOrFooter;
|
||||
/**
|
||||
* An optional component shown after all the items in the list.
|
||||
*
|
||||
* It is not sticky, and scrolls along with the content of the list.
|
||||
*/
|
||||
footer?: FileListHeaderOrFooter;
|
||||
/**
|
||||
* The logged in user, if any.
|
||||
*
|
||||
@@ -130,11 +150,13 @@ export interface FileListProps {
|
||||
* omit this prop.
|
||||
*/
|
||||
user?: LocalUser;
|
||||
showAppDownloadBanner?: boolean;
|
||||
/**
|
||||
* If `true`, then the current listing is showing magic search results.
|
||||
* If `true`, then the default behaviour of grouping files by their date is
|
||||
* suppressed.
|
||||
*
|
||||
* This behaviour is used when showing magic search results.
|
||||
*/
|
||||
isMagicSearchResult?: boolean;
|
||||
disableGrouping?: boolean;
|
||||
selectable?: boolean;
|
||||
setSelected: (
|
||||
selected: SelectedState | ((selected: SelectedState) => SelectedState),
|
||||
@@ -157,16 +179,6 @@ export interface FileListProps {
|
||||
* omitted when running in the public albums app.
|
||||
*/
|
||||
emailByUserID?: Map<number, string>;
|
||||
/**
|
||||
* An optional {@link TimeStampListItem} shown before all the items in the
|
||||
* list. It is not sticky, and scrolls along with the content of the list.
|
||||
*/
|
||||
header?: TimeStampListItem;
|
||||
/**
|
||||
* An optional {@link TimeStampListItem} shown after all the items in the
|
||||
* list. It is not sticky, and scrolls along with the content of the list.
|
||||
*/
|
||||
footer?: TimeStampListItem;
|
||||
/**
|
||||
* Called when the user activates the thumbnail at the given {@link index}.
|
||||
*
|
||||
@@ -185,10 +197,10 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
mode,
|
||||
modePlus,
|
||||
header,
|
||||
footer,
|
||||
user,
|
||||
annotatedFiles,
|
||||
showAppDownloadBanner,
|
||||
isMagicSearchResult,
|
||||
disableGrouping,
|
||||
selectable,
|
||||
selected,
|
||||
setSelected,
|
||||
@@ -196,7 +208,6 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
activePersonID,
|
||||
favoriteFileIDs,
|
||||
emailByUserID,
|
||||
footer,
|
||||
onItemClick,
|
||||
}) => {
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
@@ -246,7 +257,7 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
if (header) {
|
||||
timeStampList.push(asFullSpanListItem(header));
|
||||
}
|
||||
if (isMagicSearchResult) {
|
||||
if (disableGrouping) {
|
||||
noGrouping(timeStampList);
|
||||
} else {
|
||||
groupByTime(timeStampList);
|
||||
@@ -258,15 +269,10 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
if (timeStampList.length === 1) {
|
||||
timeStampList.push(getEmptyListItem());
|
||||
}
|
||||
timeStampList.push(getVacuumItem(timeStampList));
|
||||
const footerHeight = footer?.height ?? 0;
|
||||
timeStampList.push(getVacuumItem(timeStampList, footerHeight));
|
||||
if (footer) {
|
||||
timeStampList.push(asFullSpanListItem(footer));
|
||||
} else if (showAppDownloadBanner) {
|
||||
timeStampList.push(getAppDownloadFooter());
|
||||
}
|
||||
|
||||
if (publicCollectionGalleryContext.credentials) {
|
||||
timeStampList.push(getAlbumsFooter());
|
||||
}
|
||||
|
||||
setTimeStampList(timeStampList);
|
||||
@@ -283,7 +289,7 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
annotatedFiles,
|
||||
header,
|
||||
footer,
|
||||
isMagicSearchResult,
|
||||
disableGrouping,
|
||||
publicCollectionGalleryContext.credentials,
|
||||
]);
|
||||
|
||||
@@ -358,15 +364,7 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getVacuumItem = (timeStampList) => {
|
||||
let footerHeight;
|
||||
if (publicCollectionGalleryContext.credentials) {
|
||||
footerHeight = publicCollectionGalleryContext.referralCode
|
||||
? ALBUM_FOOTER_HEIGHT_WITH_REFERRAL
|
||||
: ALBUM_FOOTER_HEIGHT;
|
||||
} else {
|
||||
footerHeight = FOOTER_HEIGHT;
|
||||
}
|
||||
const getVacuumItem = (timeStampList, footerHeight: number) => {
|
||||
const fileListHeight = (() => {
|
||||
let sum = 0;
|
||||
const getCurrentItemSize = getItemSize(timeStampList);
|
||||
@@ -384,95 +382,6 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getAppDownloadFooter = (): TimeStampListItem => ({
|
||||
tag: "publicAlbumsFooter",
|
||||
height: FOOTER_HEIGHT,
|
||||
item: (
|
||||
<FooterContainer span={columns}>
|
||||
<Typography variant="small" sx={{ color: "text.faint" }}>
|
||||
<Trans
|
||||
i18nKey={"install_mobile_app"}
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
href="https://play.google.com/store/apps/details?id=io.ente.photos"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
/>
|
||||
),
|
||||
b: (
|
||||
<Link
|
||||
href="https://apps.apple.com/in/app/ente-photos/id1542026904"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</FooterContainer>
|
||||
),
|
||||
});
|
||||
|
||||
const getAlbumsFooter = (): TimeStampListItem => ({
|
||||
tag: "publicAlbumsFooter",
|
||||
height: publicCollectionGalleryContext.referralCode
|
||||
? ALBUM_FOOTER_HEIGHT_WITH_REFERRAL
|
||||
: ALBUM_FOOTER_HEIGHT,
|
||||
item: (
|
||||
<AlbumFooterContainer
|
||||
span={columns}
|
||||
hasReferral={!!publicCollectionGalleryContext.referralCode}
|
||||
>
|
||||
{/* Make the entire area tappable, otherwise it is hard to
|
||||
get at on mobile devices. */}
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Link
|
||||
color="text.base"
|
||||
sx={{ "&:hover": { color: "inherit" } }}
|
||||
target="_blank"
|
||||
href={"https://ente.io"}
|
||||
>
|
||||
<Typography variant="small">
|
||||
<Trans
|
||||
i18nKey="shared_using"
|
||||
components={{
|
||||
a: (
|
||||
<Typography
|
||||
variant="small"
|
||||
component="span"
|
||||
sx={{ color: "accent.main" }}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
values={{ url: "ente.io" }}
|
||||
/>
|
||||
</Typography>
|
||||
</Link>
|
||||
{publicCollectionGalleryContext.referralCode ? (
|
||||
<FullStretchContainer>
|
||||
<Typography
|
||||
sx={{
|
||||
marginTop: "12px",
|
||||
padding: "8px",
|
||||
color: "accent.contrastText",
|
||||
}}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={"sharing_referral_code"}
|
||||
values={{
|
||||
referralCode:
|
||||
publicCollectionGalleryContext.referralCode,
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</FullStretchContainer>
|
||||
) : null}
|
||||
</Box>
|
||||
</AlbumFooterContainer>
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks and merge multiple dates into a single row.
|
||||
*/
|
||||
@@ -953,41 +862,6 @@ const DateContainer = styled(ListItemContainer)(
|
||||
`,
|
||||
);
|
||||
|
||||
const FooterContainer = styled(ListItemContainer)`
|
||||
margin-bottom: 0.75rem;
|
||||
@media (max-width: 540px) {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
margin-top: calc(2rem + 20px);
|
||||
`;
|
||||
|
||||
const AlbumFooterContainer = styled(ListItemContainer, {
|
||||
shouldForwardProp: (propName) => propName != "hasReferral",
|
||||
})<{ hasReferral: boolean }>`
|
||||
margin-top: 48px;
|
||||
margin-bottom: ${({ hasReferral }) => (!hasReferral ? `10px` : "0px")};
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const FullStretchContainer = styled("div")(
|
||||
({ theme }) => `
|
||||
margin: 0 -24px;
|
||||
width: calc(100% + 46px);
|
||||
left: -24px;
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) {
|
||||
margin: 0 -4px;
|
||||
width: calc(100% + 6px);
|
||||
left: -4px;
|
||||
}
|
||||
background-color: ${theme.vars.palette.accent.main};
|
||||
`,
|
||||
);
|
||||
|
||||
const NothingContainer = styled(ListItemContainer)`
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -58,8 +58,7 @@ export type FileListWithViewerProps = {
|
||||
| "modePlus"
|
||||
| "header"
|
||||
| "footer"
|
||||
| "showAppDownloadBanner"
|
||||
| "isMagicSearchResult"
|
||||
| "disableGrouping"
|
||||
| "selectable"
|
||||
| "selected"
|
||||
| "setSelected"
|
||||
@@ -98,8 +97,7 @@ export const FileListWithViewer: React.FC<FileListWithViewerProps> = ({
|
||||
user,
|
||||
files,
|
||||
enableDownload,
|
||||
showAppDownloadBanner,
|
||||
isMagicSearchResult,
|
||||
disableGrouping,
|
||||
selectable,
|
||||
selected,
|
||||
setSelected,
|
||||
@@ -186,8 +184,7 @@ export const FileListWithViewer: React.FC<FileListWithViewerProps> = ({
|
||||
header,
|
||||
footer,
|
||||
user,
|
||||
showAppDownloadBanner,
|
||||
isMagicSearchResult,
|
||||
disableGrouping,
|
||||
selectable,
|
||||
selected,
|
||||
setSelected,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import FileUploadOutlinedIcon from "@mui/icons-material/FileUploadOutlined";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import { IconButton, Stack, Typography } from "@mui/material";
|
||||
import { IconButton, Link, Stack, Typography } from "@mui/material";
|
||||
import { AuthenticateUser } from "components/AuthenticateUser";
|
||||
import { GalleryBarAndListHeader } from "components/Collections/GalleryBarAndListHeader";
|
||||
import { DownloadStatusNotifications } from "components/DownloadStatusNotifications";
|
||||
import { type TimeStampListItem } from "components/FileList";
|
||||
import type { FileListHeaderOrFooter } from "components/FileList";
|
||||
import { FileListWithViewer } from "components/FileListWithViewer";
|
||||
import { FixCreationTime } from "components/FixCreationTime";
|
||||
import { Sidebar } from "components/Sidebar";
|
||||
@@ -180,9 +180,8 @@ const Page: React.FC = () => {
|
||||
const [fixCreationTimeFiles, setFixCreationTimeFiles] = useState<
|
||||
EnteFile[]
|
||||
>([]);
|
||||
// The (non-sticky) header shown at the top of the gallery items.
|
||||
const [fileListHeader, setFileListHeader] = useState<
|
||||
TimeStampListItem | undefined
|
||||
FileListHeaderOrFooter | undefined
|
||||
>(undefined);
|
||||
|
||||
const [openCollectionSelector, setOpenCollectionSelector] = useState(false);
|
||||
@@ -409,14 +408,13 @@ const Page: React.FC = () => {
|
||||
useEffect(() => {
|
||||
if (isInSearchMode && state.searchSuggestion) {
|
||||
setFileListHeader({
|
||||
height: 104,
|
||||
item: (
|
||||
<SearchResultsHeader
|
||||
searchSuggestion={state.searchSuggestion}
|
||||
fileCount={state.searchResults?.length ?? 0}
|
||||
/>
|
||||
),
|
||||
tag: "header",
|
||||
height: 104,
|
||||
});
|
||||
}
|
||||
}, [isInSearchMode, state.searchSuggestion, state.searchResults]);
|
||||
@@ -966,6 +964,14 @@ const Page: React.FC = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
const showAppDownloadFooter =
|
||||
state.collectionFiles.length < 30 && !isInSearchMode;
|
||||
|
||||
const fileListFooter = useMemo(
|
||||
() => (showAppDownloadFooter ? createAppDownloadFooter() : undefined),
|
||||
[showAppDownloadFooter],
|
||||
);
|
||||
|
||||
const showSelectionBar =
|
||||
selected.count > 0 && selected.collectionID === activeCollectionID;
|
||||
|
||||
@@ -1072,7 +1078,7 @@ const Page: React.FC = () => {
|
||||
activeCollection,
|
||||
activeCollectionID,
|
||||
activePerson,
|
||||
setPhotoListHeader: setFileListHeader,
|
||||
setFileListHeader,
|
||||
saveGroups,
|
||||
onAddSaveGroup,
|
||||
}}
|
||||
@@ -1148,13 +1154,11 @@ const Page: React.FC = () => {
|
||||
mode={barMode}
|
||||
modePlus={isInSearchMode ? "search" : barMode}
|
||||
header={fileListHeader}
|
||||
footer={fileListFooter}
|
||||
user={user}
|
||||
files={filteredFiles}
|
||||
enableDownload={true}
|
||||
showAppDownloadBanner={
|
||||
state.collectionFiles.length < 30 && !isInSearchMode
|
||||
}
|
||||
isMagicSearchResult={state.searchSuggestion?.type == "clip"}
|
||||
disableGrouping={state.searchSuggestion?.type == "clip"}
|
||||
selectable={true}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
@@ -1382,3 +1386,39 @@ const handleSubscriptionCompletionRedirectIfNeeded = async (
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createAppDownloadFooter = (): FileListHeaderOrFooter => ({
|
||||
item: (
|
||||
<Typography
|
||||
variant="small"
|
||||
sx={{
|
||||
alignSelf: "flex-end",
|
||||
marginInline: "auto",
|
||||
marginBlock: 0.75,
|
||||
textAlign: "center",
|
||||
color: "text.faint",
|
||||
}}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={"install_mobile_app"}
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
href="https://play.google.com/store/apps/details?id=io.ente.photos"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
/>
|
||||
),
|
||||
b: (
|
||||
<Link
|
||||
href="https://apps.apple.com/in/app/ente-photos/id1542026904"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
),
|
||||
height: 90,
|
||||
});
|
||||
|
||||
@@ -3,7 +3,15 @@ import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternate
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
||||
import { Box, Button, IconButton, Stack, styled, Tooltip } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Link,
|
||||
Stack,
|
||||
styled,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { DownloadStatusNotifications } from "components/DownloadStatusNotifications";
|
||||
import { FileListWithViewer } from "components/FileListWithViewer";
|
||||
@@ -84,6 +92,7 @@ import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { type FileWithPath } from "react-dropzone";
|
||||
import { Trans } from "react-i18next";
|
||||
import { uploadManager } from "services/upload-manager";
|
||||
import { getSelectedFiles, type SelectedState } from "utils/file";
|
||||
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
|
||||
@@ -98,7 +107,8 @@ export default function PublicCollectionGallery() {
|
||||
const [publicFiles, setPublicFiles] = useState<EnteFile[] | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [errorMessage, setErrorMessage] = useState<string>(null);
|
||||
const [referralCode, setReferralCode] = useState<string>("");
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isPasswordProtected, setIsPasswordProtected] = useState(false);
|
||||
const [uploadTypeSelectorView, setUploadTypeSelectorView] = useState(false);
|
||||
@@ -117,7 +127,6 @@ export default function PublicCollectionGallery() {
|
||||
const credentials = useRef<PublicAlbumsCredentials | undefined>(undefined);
|
||||
const collectionKey = useRef<string>(null);
|
||||
const url = useRef<string>(null);
|
||||
const referralCode = useRef<string>("");
|
||||
|
||||
const { saveGroups, onAddSaveGroup, onRemoveSaveGroup } = useSaveGroups();
|
||||
|
||||
@@ -180,8 +189,9 @@ export default function PublicCollectionGallery() {
|
||||
const accessToken = t;
|
||||
let accessTokenJWT: string | undefined;
|
||||
if (collection) {
|
||||
referralCode.current =
|
||||
await savedLastPublicCollectionReferralCode();
|
||||
setReferralCode(
|
||||
(await savedLastPublicCollectionReferralCode()) ?? "",
|
||||
);
|
||||
setPublicCollection(collection);
|
||||
setIsPasswordProtected(
|
||||
!!collection.publicURLs[0]?.passwordEnabled,
|
||||
@@ -223,13 +233,13 @@ export default function PublicCollectionGallery() {
|
||||
try {
|
||||
const { collection, referralCode: userReferralCode } =
|
||||
await pullCollection(accessToken, collectionKey.current);
|
||||
referralCode.current = userReferralCode;
|
||||
setReferralCode(userReferralCode);
|
||||
|
||||
setPublicCollection(collection);
|
||||
const isPasswordProtected =
|
||||
!!collection.publicURLs[0]?.passwordEnabled;
|
||||
setIsPasswordProtected(isPasswordProtected);
|
||||
setErrorMessage(null);
|
||||
setErrorMessage("");
|
||||
|
||||
// Remove the locally cached accessTokenJWT if the sharer has
|
||||
// disabled password protection on the link.
|
||||
@@ -390,7 +400,7 @@ export default function PublicCollectionGallery() {
|
||||
publicCollection && publicFiles
|
||||
? {
|
||||
item: (
|
||||
<ListHeader
|
||||
<FileListHeader
|
||||
{...{
|
||||
publicCollection,
|
||||
publicFiles,
|
||||
@@ -398,27 +408,19 @@ export default function PublicCollectionGallery() {
|
||||
}}
|
||||
/>
|
||||
),
|
||||
tag: "header" as const,
|
||||
height: 68,
|
||||
height: fileListHeaderHeight,
|
||||
}
|
||||
: undefined,
|
||||
[onAddSaveGroup, publicCollection, publicFiles],
|
||||
);
|
||||
|
||||
const fileListFooter = useMemo(
|
||||
() =>
|
||||
onAddPhotos
|
||||
? {
|
||||
item: (
|
||||
<CenteredFill sx={{ marginTop: "56px" }}>
|
||||
<AddMorePhotosButton onClick={onAddPhotos} />
|
||||
</CenteredFill>
|
||||
),
|
||||
height: 104,
|
||||
}
|
||||
: undefined,
|
||||
[onAddPhotos],
|
||||
);
|
||||
const fileListFooter = useMemo(() => {
|
||||
const props = { referralCode, onAddPhotos };
|
||||
return {
|
||||
item: <FileListFooter {...props} />,
|
||||
height: fileListFooterHeightForProps(props),
|
||||
};
|
||||
}, [referralCode, onAddPhotos]);
|
||||
|
||||
if (loading && (!publicFiles || !credentials.current)) {
|
||||
return <LoadingIndicator />;
|
||||
@@ -460,10 +462,7 @@ export default function PublicCollectionGallery() {
|
||||
}
|
||||
|
||||
// TODO: memo this (after the dependencies are traceable).
|
||||
const context = {
|
||||
credentials: credentials.current,
|
||||
referralCode: referralCode.current,
|
||||
};
|
||||
const context = { credentials: credentials.current };
|
||||
|
||||
return (
|
||||
<PublicCollectionGalleryContext.Provider value={context}>
|
||||
@@ -627,13 +626,24 @@ const SelectedFileOptions: React.FC<SelectedFileOptionsProps> = ({
|
||||
</Stack>
|
||||
);
|
||||
|
||||
interface ListHeaderProps {
|
||||
interface FileListHeaderProps {
|
||||
publicCollection: Collection;
|
||||
publicFiles: EnteFile[];
|
||||
onAddSaveGroup: AddSaveGroup;
|
||||
}
|
||||
|
||||
const ListHeader: React.FC<ListHeaderProps> = ({
|
||||
/**
|
||||
* The fixed height (in px) of {@link FileListHeader}.
|
||||
*/
|
||||
const fileListHeaderHeight = 68;
|
||||
|
||||
/**
|
||||
* A header shown before the listing of files.
|
||||
*
|
||||
* It scrolls along with the content. It has a fixed height,
|
||||
* {@link fileListHeaderHeight}.
|
||||
*/
|
||||
const FileListHeader: React.FC<FileListHeaderProps> = ({
|
||||
publicCollection,
|
||||
publicFiles,
|
||||
onAddSaveGroup,
|
||||
@@ -671,3 +681,85 @@ const ListHeader: React.FC<ListHeaderProps> = ({
|
||||
</GalleryItemsHeaderAdapter>
|
||||
);
|
||||
};
|
||||
|
||||
interface FileListFooterProps {
|
||||
referralCode?: string;
|
||||
onAddPhotos?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The dynamic (prop-depedent) height of {@link FileListFooter}.
|
||||
*/
|
||||
const fileListFooterHeightForProps = ({
|
||||
referralCode,
|
||||
onAddPhotos,
|
||||
}: FileListFooterProps) => (onAddPhotos ? 104 : 0) + (referralCode ? 113 : 75);
|
||||
|
||||
/**
|
||||
* A footer shown after the listing of files.
|
||||
*
|
||||
* It scrolls along with the content. It has a dynamic height, dependent on the
|
||||
* props, calculated using {@link fileListFooterHeightForProps}.
|
||||
*/
|
||||
|
||||
const FileListFooter: React.FC<FileListFooterProps> = ({
|
||||
referralCode,
|
||||
onAddPhotos,
|
||||
}) => (
|
||||
<Stack sx={{ flex: 1, alignSelf: "flex-end" }}>
|
||||
{onAddPhotos && (
|
||||
<CenteredFill>
|
||||
<AddMorePhotosButton onClick={onAddPhotos} />
|
||||
</CenteredFill>
|
||||
)}
|
||||
{/* Make the entire area tappable, otherwise it is hard to
|
||||
get at on mobile devices. */}
|
||||
<Link
|
||||
color="text.muted"
|
||||
sx={{
|
||||
mt: "48px",
|
||||
mb: "6px",
|
||||
textAlign: "center",
|
||||
"&:hover": { color: "inherit" },
|
||||
}}
|
||||
target="_blank"
|
||||
href="https://ente.io"
|
||||
>
|
||||
<Typography variant="small">
|
||||
<Trans
|
||||
i18nKey="shared_using"
|
||||
components={{
|
||||
a: (
|
||||
<Typography
|
||||
variant="small"
|
||||
component="span"
|
||||
sx={{ color: "accent.main" }}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
values={{ url: "ente.io" }}
|
||||
/>
|
||||
</Typography>
|
||||
</Link>
|
||||
{referralCode && (
|
||||
<Typography
|
||||
sx={{
|
||||
mt: "6px",
|
||||
mb: 0,
|
||||
/* Negative margin to extend to edges by counteracting the
|
||||
maximum margin that can be added by FileViewer. */
|
||||
mx: "-24px",
|
||||
padding: "8px",
|
||||
bgcolor: "accent.main",
|
||||
color: "accent.contrastText",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={"sharing_referral_code"}
|
||||
values={{ referralCode }}
|
||||
/>
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -8,11 +8,9 @@ export interface PublicCollectionGalleryContextType {
|
||||
* undefined when we're in the default photos app context.
|
||||
*/
|
||||
credentials: PublicAlbumsCredentials | undefined;
|
||||
referralCode: string | null;
|
||||
}
|
||||
|
||||
export const PublicCollectionGalleryContext =
|
||||
createContext<PublicCollectionGalleryContextType>({
|
||||
credentials: undefined,
|
||||
referralCode: null,
|
||||
});
|
||||
|
||||
@@ -38,8 +38,8 @@ interface GalleryItemsSummaryProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* A component suitable for being used as a (non-sticky) summary displayed on
|
||||
* top of the of a list of photos (or other items) shown in the gallery.
|
||||
* A component suitable for being used as a summary displayed on top of the of a
|
||||
* list of photos (or other items) shown in the gallery.
|
||||
*/
|
||||
export const GalleryItemsSummary: React.FC<GalleryItemsSummaryProps> = ({
|
||||
name,
|
||||
|
||||
Reference in New Issue
Block a user