diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx index e52d4abc28..3b425cb965 100644 --- a/web/apps/accounts/src/pages/_app.tsx +++ b/web/apps/accounts/src/pages/_app.tsx @@ -2,12 +2,12 @@ import { staticAppTitle } from "@/base/app"; import { CustomHead } from "@/base/components/Head"; import { AttributedMiniDialog } from "@/base/components/MiniDialog"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; +import { Overlay } from "@/base/components/mui/Container"; import { AppNavbar } from "@/base/components/Navbar"; import { useAttributedMiniDialog } from "@/base/components/utils/dialog"; import { setupI18n } from "@/base/i18n"; import { disableDiskLogs } from "@/base/log"; import { logUnhandledErrorsAndRejections } from "@/base/log-web"; -import { Overlay } from "@ente/shared/components/Container"; import { getTheme } from "@ente/shared/themes"; import { THEME_COLOR } from "@ente/shared/themes/constants"; import { CssBaseline } from "@mui/material"; diff --git a/web/apps/auth/src/pages/_app.tsx b/web/apps/auth/src/pages/_app.tsx index 5557ace23d..b8cb12239d 100644 --- a/web/apps/auth/src/pages/_app.tsx +++ b/web/apps/auth/src/pages/_app.tsx @@ -4,6 +4,7 @@ import { clientPackageName, staticAppTitle } from "@/base/app"; import { CustomHead } from "@/base/components/Head"; import { AttributedMiniDialog } from "@/base/components/MiniDialog"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; +import { Overlay } from "@/base/components/mui/Container"; import { AppNavbar } from "@/base/components/Navbar"; import { genericErrorDialogAttributes, @@ -15,7 +16,6 @@ import { logUnhandledErrorsAndRejections, } from "@/base/log-web"; import { ensure } from "@/utils/ensure"; -import { Overlay } from "@ente/shared/components/Container"; import { MessageContainer } from "@ente/shared/components/MessageContainer"; import { useLocalState } from "@ente/shared/hooks/useLocalState"; import HTTPService from "@ente/shared/network/HTTPService"; diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index 1bfcec9104..ad2f3323d2 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -5,6 +5,7 @@ "dependencies": { "@/accounts": "*", "@/base": "*", + "@/gallery": "*", "@/media": "*", "@/new": "*", "@ente/eslint-config": "*", diff --git a/web/apps/photos/src/components/MemberSubscriptionManage.tsx b/web/apps/photos/src/components/MemberSubscriptionManage.tsx index 5fec26cbdf..62be7e4473 100644 --- a/web/apps/photos/src/components/MemberSubscriptionManage.tsx +++ b/web/apps/photos/src/components/MemberSubscriptionManage.tsx @@ -1,4 +1,5 @@ import { useIsSmallWidth } from "@/base/hooks"; +import { getFamilyPlanAdmin } from "@/new/photos/services/user"; import { AppContext } from "@/new/photos/types/context"; import { FlexWrapper, @@ -9,7 +10,6 @@ import { Box, Button, Dialog, DialogContent, Typography } from "@mui/material"; import { t } from "i18next"; import { useContext } from "react"; import billingService from "services/billingService"; -import { getFamilyPlanAdmin } from "utils/user/family"; export function MemberSubscriptionManage({ open, userDetails, onClose }) { const { setDialogMessage } = useContext(AppContext); diff --git a/web/apps/photos/src/components/PhotoViewer/index.tsx b/web/apps/photos/src/components/PhotoViewer/index.tsx index 366a377545..d1ee5f59de 100644 --- a/web/apps/photos/src/components/PhotoViewer/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/index.tsx @@ -1,9 +1,10 @@ import { isDesktop } from "@/base/app"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; +import { Overlay } from "@/base/components/mui/Container"; import { lowercaseExtension } from "@/base/file"; import log from "@/base/log"; import type { LoadedLivePhotoSourceURL } from "@/media/file"; -import { type EnteFile, fileLogID } from "@/media/file"; +import { fileLogID, type EnteFile } from "@/media/file"; import { FileType } from "@/media/file-type"; import { isHEICExtension, needsJPEGConversion } from "@/media/formats"; import downloadManager from "@/new/photos/services/download"; @@ -25,7 +26,16 @@ import FullscreenOutlinedIcon from "@mui/icons-material/FullscreenOutlined"; import InfoIcon from "@mui/icons-material/InfoOutlined"; import ReplayIcon from "@mui/icons-material/Replay"; import ZoomInOutlinedIcon from "@mui/icons-material/ZoomInOutlined"; -import { Box, Button, styled } from "@mui/material"; +import { + Box, + Button, + CircularProgress, + Paper, + styled, + Typography, + type CircularProgressProps, +} from "@mui/material"; +import Notification from "components/Notification"; import { t } from "i18next"; import isElectron from "is-electron"; import { GalleryContext } from "pages/gallery"; @@ -48,9 +58,6 @@ import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; import { getTrashFileMessage } from "utils/ui"; import { FileInfo, type FileInfoExif, type FileInfoProps } from "./FileInfo"; import ImageEditorOverlay from "./ImageEditorOverlay"; -import CircularProgressWithLabel from "./styledComponents/CircularProgressWithLabel"; -import { ConversionFailedNotification } from "./styledComponents/ConversionFailedNotification"; -import { LivePhotoBtnContainer } from "./styledComponents/LivePhotoBtn"; interface PhotoswipeFullscreenAPI { enter: () => void; @@ -983,3 +990,60 @@ function PhotoViewer(props: PhotoViewerProps) { } export default PhotoViewer; + +function CircularProgressWithLabel( + props: CircularProgressProps & { value: number }, +) { + return ( + <> + + + {`${Math.round(props.value)}%`} + + + ); +} + +interface ConversionFailedNotificationProps { + open: boolean; + onClose: () => void; + onClick: () => void; +} + +const ConversionFailedNotification: React.FC< + ConversionFailedNotificationProps +> = ({ open, onClose, onClick }) => { + return ( + + ); +}; + +const LivePhotoBtnContainer = styled(Paper)` + border-radius: 4px; + position: absolute; + bottom: 10vh; + right: 6vh; + z-index: 10; +`; diff --git a/web/apps/photos/src/components/PhotoViewer/styledComponents/CircularProgressWithLabel.tsx b/web/apps/photos/src/components/PhotoViewer/styledComponents/CircularProgressWithLabel.tsx deleted file mode 100644 index 9556a3a855..0000000000 --- a/web/apps/photos/src/components/PhotoViewer/styledComponents/CircularProgressWithLabel.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Overlay } from "@ente/shared/components/Container"; -import { - CircularProgress, - Typography, - type CircularProgressProps, -} from "@mui/material"; - -function CircularProgressWithLabel( - props: CircularProgressProps & { value: number }, -) { - return ( - <> - - - {`${Math.round(props.value)}%`} - - - ); -} - -export default CircularProgressWithLabel; diff --git a/web/apps/photos/src/components/PhotoViewer/styledComponents/ConversionFailedNotification.tsx b/web/apps/photos/src/components/PhotoViewer/styledComponents/ConversionFailedNotification.tsx deleted file mode 100644 index fe504d1805..0000000000 --- a/web/apps/photos/src/components/PhotoViewer/styledComponents/ConversionFailedNotification.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import Notification from "components/Notification"; -import { t } from "i18next"; - -interface Iprops { - open: boolean; - onClose: () => void; - onClick: () => void; -} - -export const ConversionFailedNotification = ({ - open, - onClose, - onClick, -}: Iprops) => { - return ( - - ); -}; diff --git a/web/apps/photos/src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx b/web/apps/photos/src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx deleted file mode 100644 index 00b8979d5a..0000000000 --- a/web/apps/photos/src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Paper, styled } from "@mui/material"; - -export const LivePhotoBtnContainer = styled(Paper)` - border-radius: 4px; - position: absolute; - bottom: 10vh; - right: 6vh; - z-index: 10; -`; diff --git a/web/apps/photos/src/components/PhotoViewer/styledComponents/Pre.tsx b/web/apps/photos/src/components/PhotoViewer/styledComponents/Pre.tsx deleted file mode 100644 index b088ec9f83..0000000000 --- a/web/apps/photos/src/components/PhotoViewer/styledComponents/Pre.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { styled } from "@mui/material"; -export const Pre = styled("pre")` - color: #aaa; - padding: 7px 15px; -`; diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard.tsx new file mode 100644 index 0000000000..090b4c949f --- /dev/null +++ b/web/apps/photos/src/components/Sidebar/SubscriptionCard.tsx @@ -0,0 +1,358 @@ +import { Overlay } from "@/base/components/mui/Container"; +import type { ButtonishProps } from "@/new/photos/components/mui"; +import { + hasNonAdminFamilyMembers, + isPartOfFamily, +} from "@/new/photos/services/user"; +import { bytesInGB, formattedStorageByteSize } from "@/new/photos/utils/units"; +import { SpaceBetweenFlex } from "@ente/shared/components/Container"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import CircleIcon from "@mui/icons-material/Circle"; +import { + Box, + LinearProgress, + Skeleton, + Stack, + Typography, + styled, + type LinearProgressProps, +} from "@mui/material"; +import { t } from "i18next"; +import type React from "react"; +import { useMemo } from "react"; +import type { UserDetails } from "types/user"; + +interface SubscriptionCardProps { + userDetails: UserDetails; + onClick: () => void; +} + +export const SubscriptionCard: React.FC = ({ + userDetails, + onClick, +}) => { + if (!userDetails) { + return ( + + ); + } + + return ( + + + + + + ); +}; + +const BackgroundOverlay: React.FC = () => { + return ( + + ); +}; + +const ClickOverlay: React.FC = ({ onClick }) => ( + + + +); + +interface SubscriptionCardContentOverlayProps { + userDetails: UserDetails; +} + +export const SubscriptionCardContentOverlay: React.FC< + SubscriptionCardContentOverlayProps +> = ({ userDetails }) => { + return ( + + + {hasNonAdminFamilyMembers(userDetails.familyData) ? ( + + ) : ( + + )} + + + ); +}; + +interface IndividualSubscriptionCardContentProps { + userDetails: UserDetails; +} + +const IndividualSubscriptionCardContent: React.FC< + IndividualSubscriptionCardContentProps +> = ({ userDetails }) => { + const totalStorage = + userDetails.subscription.storage + (userDetails.storageBonus ?? 0); + return ( + <> + + + + ); +}; + +const MobileSmallBox = styled(Box)` + display: none; + @media (max-width: 359px) { + display: block; + } +`; + +const DefaultBox = styled(Box)` + display: none; + @media (min-width: 360px) { + display: block; + } +`; + +interface StorageSectionProps { + usage: number; + storage: number; +} + +const StorageSection: React.FC = ({ usage, storage }) => { + return ( + + + {t("STORAGE")} + + + + {`${formattedStorageByteSize(usage, { round: true })} ${t( + "OF", + )} ${formattedStorageByteSize(storage)} ${t("USED")}`} + + + + + {`${bytesInGB(usage)} / ${bytesInGB(storage)} ${t("storage_unit.gb")} ${t("USED")}`} + + + + ); +}; + +interface IndividualUsageSectionProps { + usage: number; + fileCount: number; + storage: number; +} + +const IndividualUsageSection: React.FC = ({ + usage, + storage, + fileCount, +}) => { + // [Note: Fallback translation for languages with multiple plurals] + // + // Languages like Polish and Arabian have multiple plural forms, and + // currently i18n falls back to the base language translation instead of the + // "_other" form if all the plural forms are not listed out. + // + // As a workaround, name the _other form as the unprefixed name. That is, + // instead of calling the most general plural form as foo_count_other, call + // it foo_count (To keep our heads straight, we adopt the convention that + // all such pluralizable strings use the _count suffix, but that's not a + // requirement from the library). + return ( + + + + {`${formattedStorageByteSize( + storage - usage, + )} ${t("FREE")}`} + + {t("photos_count", { count: fileCount ?? 0 })} + + + + ); +}; + +interface FamilySubscriptionCardContentProps { + userDetails: UserDetails; +} + +const FamilySubscriptionCardContent: React.FC< + FamilySubscriptionCardContentProps +> = ({ userDetails }) => { + const totalUsage = useMemo(() => { + if (isPartOfFamily(userDetails.familyData)) { + return userDetails.familyData.members.reduce( + (sum, currentMember) => sum + currentMember.usage, + 0, + ); + } else { + return userDetails.usage; + } + }, [userDetails]); + const totalStorage = + userDetails.familyData.storage + (userDetails.storageBonus ?? 0); + + return ( + <> + + + + ); +}; + +interface FamilyUsageSectionProps { + userUsage: number; + totalUsage: number; + fileCount: number; + totalStorage: number; +} + +const FamilyUsageSection: React.FC = ({ + userUsage, + totalUsage, + fileCount, + totalStorage, +}) => { + return ( + + + + + + + + + {t("photos_count", { count: fileCount ?? 0 })} + + + + ); +}; + +interface FamilyUsageBarProps { + userUsage: number; + totalUsage: number; + totalStorage: number; +} + +const FamilyUsageBar: React.FC = ({ + userUsage, + totalUsage, + totalStorage, +}) => ( + + + + +); + +type UsageBarProps = Pick & { + used: number; + total: number; +}; + +const UsageBar: React.FC = ({ used, total, sx }) => ( + +); + +const UsageBar_ = styled(LinearProgress)(() => ({ + ".MuiLinearProgress-bar": { + borderRadius: "2px", + }, + borderRadius: "2px", + backgroundColor: "rgba(255, 255, 255, 0.2)", +})); + +interface LegendProps { + label: string; + color: string; +} + +const Legend: React.FC = ({ label, color }) => ( + + + + {label} + + +); + +const LegendDot = styled(CircleIcon)` + font-size: 8.71px; + margin: 0; + margin-inline-end: 4px; + color: inherit; +`; diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/index.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/index.tsx deleted file mode 100644 index 77776745dc..0000000000 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useMemo } from "react"; -import { UserDetails } from "types/user"; -import { isPartOfFamily } from "utils/user/family"; -import StorageSection from "../storageSection"; -import { FamilyUsageSection } from "./usageSection"; - -interface Iprops { - userDetails: UserDetails; -} -export function FamilySubscriptionCardContent({ userDetails }: Iprops) { - const totalUsage = useMemo(() => { - if (isPartOfFamily(userDetails.familyData)) { - return userDetails.familyData.members.reduce( - (sum, currentMember) => sum + currentMember.usage, - 0, - ); - } else { - return userDetails.usage; - } - }, [userDetails]); - const totalStorage = - userDetails.familyData.storage + (userDetails.storageBonus ?? 0); - - return ( - <> - - - - ); -} diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/index.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/index.tsx deleted file mode 100644 index 4c0b1904f2..0000000000 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { SpaceBetweenFlex } from "@ente/shared/components/Container"; -import { Box, Stack, Typography } from "@mui/material"; -import { t } from "i18next"; -import { Legend } from "./legend"; -import { FamilyUsageProgressBar } from "./progressBar"; - -interface Iprops { - userUsage: number; - totalUsage: number; - fileCount: number; - totalStorage: number; -} - -export function FamilyUsageSection({ - userUsage, - totalUsage, - fileCount, - totalStorage, -}: Iprops) { - return ( - - - - - - - - - {t("photos_count", { count: fileCount ?? 0 })} - - - - ); -} diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/legend.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/legend.tsx deleted file mode 100644 index 6caaa2374d..0000000000 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/legend.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { FlexWrapper } from "@ente/shared/components/Container"; -import { Typography } from "@mui/material"; -import { LegendIndicator } from "../../../styledComponents"; - -interface Iprops { - label: string; - color: string; -} -export function Legend({ label, color }: Iprops) { - return ( - - - - {label} - - - ); -} diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/progressBar.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/progressBar.tsx deleted file mode 100644 index ab28b5b8f1..0000000000 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/progressBar.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Box } from "@mui/material"; -import { Progressbar } from "../../../styledComponents"; -interface Iprops { - userUsage: number; - totalUsage: number; - totalStorage: number; -} - -export function FamilyUsageProgressBar({ - userUsage, - totalUsage, - totalStorage, -}: Iprops) { - return ( - - - - - ); -} diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/index.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/index.tsx deleted file mode 100644 index 238058534c..0000000000 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Overlay, SpaceBetweenFlex } from "@ente/shared/components/Container"; -import { UserDetails } from "types/user"; -import { hasNonAdminFamilyMembers } from "utils/user/family"; -import { FamilySubscriptionCardContent } from "./family"; -import { IndividualSubscriptionCardContent } from "./individual"; - -interface Iprops { - userDetails: UserDetails; -} - -export function SubscriptionCardContentOverlay({ userDetails }: Iprops) { - return ( - - - {hasNonAdminFamilyMembers(userDetails.familyData) ? ( - - ) : ( - - )} - - - ); -} diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/index.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/index.tsx deleted file mode 100644 index 9bdc3292c1..0000000000 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { UserDetails } from "types/user"; -import StorageSection from "../storageSection"; -import { IndividualUsageSection } from "./usageSection"; - -interface Iprops { - userDetails: UserDetails; -} - -export function IndividualSubscriptionCardContent({ userDetails }: Iprops) { - const totalStorage = - userDetails.subscription.storage + (userDetails.storageBonus ?? 0); - return ( - <> - - - - ); -} diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx deleted file mode 100644 index 857ac9c633..0000000000 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { formattedStorageByteSize } from "@/new/photos/utils/units"; -import { SpaceBetweenFlex } from "@ente/shared/components/Container"; -import { Box, Typography } from "@mui/material"; -import { t } from "i18next"; - -import { Progressbar } from "../../styledComponents"; - -interface Iprops { - usage: number; - fileCount: number; - storage: number; -} -export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) { - // [Note: Fallback translation for languages with multiple plurals] - // - // Languages like Polish and Arabian have multiple plural forms, and - // currently i18n falls back to the base language translation instead of the - // "_other" form if all the plural forms are not listed out. - // - // As a workaround, name the _other form as the unprefixed name. That is, - // instead of calling the most general plural form as foo_count_other, call - // it foo_count (To keep our heads straight, we adopt the convention that - // all such pluralizable strings use the _count suffix, but that's not a - // requirement from the library). - return ( - - - - {`${formattedStorageByteSize( - storage - usage, - )} ${t("FREE")}`} - - {t("photos_count", { count: fileCount ?? 0 })} - - - - ); -} diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx deleted file mode 100644 index 4ad0ed2149..0000000000 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { bytesInGB, formattedStorageByteSize } from "@/new/photos/utils/units"; -import { Box, Typography, styled } from "@mui/material"; -import { t } from "i18next"; - -const MobileSmallBox = styled(Box)` - display: none; - @media (max-width: 359px) { - display: block; - } -`; - -const DefaultBox = styled(Box)` - display: none; - @media (min-width: 360px) { - display: block; - } -`; -interface Iprops { - usage: number; - storage: number; -} -export default function StorageSection({ usage, storage }: Iprops) { - return ( - - - {t("STORAGE")} - - - - {`${formattedStorageByteSize(usage, { round: true })} ${t( - "OF", - )} ${formattedStorageByteSize(storage)} ${t("USED")}`} - - - - - {`${bytesInGB(usage)} / ${bytesInGB(storage)} ${t("storage_unit.gb")} ${t("USED")}`} - - - - ); -} diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/index.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/index.tsx deleted file mode 100644 index 514c43df81..0000000000 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { FlexWrapper, Overlay } from "@ente/shared/components/Container"; -import ChevronRightIcon from "@mui/icons-material/ChevronRight"; -import { Box, Skeleton } from "@mui/material"; -import { UserDetails } from "types/user"; -import { SubscriptionCardContentOverlay } from "./contentOverlay"; - -const SUBSCRIPTION_CARD_SIZE = 152; - -interface Iprops { - userDetails: UserDetails; - onClick: () => void; -} - -export default function SubscriptionCard({ userDetails, onClick }: Iprops) { - if (!userDetails) { - return ( - - ); - } - - return ( - - - - - - ); -} - -function BackgroundOverlay() { - return ( - - ); -} - -function ClickOverlay({ onClick }) { - return ( - - - - - - ); -} diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/styledComponents.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/styledComponents.tsx deleted file mode 100644 index 90bea72ce7..0000000000 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/styledComponents.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import CircleIcon from "@mui/icons-material/Circle"; -import { LinearProgress, styled } from "@mui/material"; - -export const Progressbar = styled(LinearProgress)(() => ({ - ".MuiLinearProgress-bar": { - borderRadius: "2px", - }, - borderRadius: "2px", - backgroundColor: "rgba(255, 255, 255, 0.2)", -})); - -Progressbar.defaultProps = { - variant: "determinate", -}; - -const DotSeparator = styled(CircleIcon)` - font-size: 4px; - margin: 0 ${({ theme }) => theme.spacing(1)}; - color: inherit; -`; - -export const LegendIndicator = styled(DotSeparator)` - font-size: 8.71px; - margin: 0; - margin-right: 4px; - color: inherit; -`; diff --git a/web/apps/photos/src/components/Sidebar/index.tsx b/web/apps/photos/src/components/Sidebar/index.tsx index 922e6a2148..425c7d291e 100644 --- a/web/apps/photos/src/components/Sidebar/index.tsx +++ b/web/apps/photos/src/components/Sidebar/index.tsx @@ -18,6 +18,7 @@ import { } from "@/new/photos/services/collection"; import type { CollectionSummaries } from "@/new/photos/services/collection/ui"; import { isInternalUser } from "@/new/photos/services/settings"; +import { isFamilyAdmin, isPartOfFamily } from "@/new/photos/services/user"; import { AppContext, useAppContext } from "@/new/photos/types/context"; import { initiateEmail, openURL } from "@/new/photos/utils/web"; import { SpaceBetweenFlex } from "@ente/shared/components/Container"; @@ -77,17 +78,17 @@ import { isSubscriptionCancelled, isSubscriptionPastDue, } from "utils/billing"; -import { isFamilyAdmin, isPartOfFamily } from "utils/user/family"; import { testUpload } from "../../../tests/upload.test"; import { MemberSubscriptionManage } from "../MemberSubscriptionManage"; import { Preferences } from "./Preferences"; -import SubscriptionCard from "./SubscriptionCard"; +import { SubscriptionCard } from "./SubscriptionCard"; interface Iprops { collectionSummaries: CollectionSummaries; sidebarView: boolean; closeSidebar: () => void; } + export default function Sidebar({ collectionSummaries, sidebarView, diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx index 3e840eaa03..a64a302e46 100644 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx +++ b/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx @@ -1,5 +1,9 @@ import { genericRetriableErrorDialogAttributes } from "@/base/components/utils/dialog"; import log from "@/base/log"; +import { + getTotalFamilyUsage, + isPartOfFamily, +} from "@/new/photos/services/user"; import { AppContext } from "@/new/photos/types/context"; import { bytesInGB, formattedStorageByteSize } from "@/new/photos/utils/units"; import { openURL } from "@/new/photos/utils/web"; @@ -8,6 +12,7 @@ import { FluidContainer, SpaceBetweenFlex, } from "@ente/shared/components/Container"; +import { getData, LS_KEYS } from "@ente/shared/storage/localStorage"; import ArrowForward from "@mui/icons-material/ArrowForward"; import ChevronRight from "@mui/icons-material/ChevronRight"; import Close from "@mui/icons-material/Close"; @@ -34,7 +39,7 @@ import billingService, { type PlansResponse } from "services/billingService"; import { getFamilyPortalRedirectURL } from "services/userService"; import { Plan, PLAN_PERIOD, Subscription } from "types/billing"; import { SetLoading } from "types/gallery"; -import { BonusData } from "types/user"; +import { BonusData, UserDetails } from "types/user"; import { activateSubscription, cancelSubscription, @@ -52,8 +57,6 @@ import { updatePaymentMethod, updateSubscription, } from "utils/billing"; -import { getLocalUserDetails } from "utils/user"; -import { getTotalFamilyUsage, isPartOfFamily } from "utils/user/family"; interface PlanSelectorProps { modalView: boolean; @@ -796,3 +799,7 @@ const ManageSubscriptionButton = ({ children, ...props }: ButtonProps) => ( {children} ); + +function getLocalUserDetails(): UserDetails { + return getData(LS_KEYS.USER_DETAILS)?.value; +} diff --git a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx index 0f66a27ed6..64a44ff335 100644 --- a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx +++ b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx @@ -1,3 +1,4 @@ +import { Overlay } from "@/base/components/mui/Container"; import log from "@/base/log"; import { EnteFile } from "@/media/file"; import { FileType } from "@/media/file-type"; @@ -11,7 +12,6 @@ import { } from "@/new/photos/components/PlaceholderThumbnails"; import { TRASH_SECTION } from "@/new/photos/services/collection"; import DownloadManager from "@/new/photos/services/download"; -import { Overlay } from "@ente/shared/components/Container"; import { CustomError } from "@ente/shared/error"; import useLongPress from "@ente/shared/hooks/useLongPress"; import AlbumOutlined from "@mui/icons-material/AlbumOutlined"; diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index 361a0be901..7a1582b8e4 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -2,6 +2,7 @@ import { clientPackageName, staticAppTitle } from "@/base/app"; import { CustomHead } from "@/base/components/Head"; import { AttributedMiniDialog } from "@/base/components/MiniDialog"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; +import { Overlay } from "@/base/components/mui/Container"; import { AppNavbar } from "@/base/components/Navbar"; import { genericErrorDialogAttributes, @@ -24,7 +25,6 @@ import DownloadManager from "@/new/photos/services/download"; import { runMigrations } from "@/new/photos/services/migrations"; import { initML, isMLSupported } from "@/new/photos/services/ml"; import { AppContext } from "@/new/photos/types/context"; -import { Overlay } from "@ente/shared/components/Container"; import DialogBox from "@ente/shared/components/DialogBox"; import { DialogBoxAttributes } from "@ente/shared/components/DialogBox/types"; import { MessageContainer } from "@ente/shared/components/MessageContainer"; diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 9b5c433337..ff039f7a67 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -1,3 +1,4 @@ +import { sessionExpiredDialogAttributes } from "@/accounts/components/LoginComponents"; import { stashRedirect } from "@/accounts/services/redirect"; import { NavbarBase } from "@/base/components/Navbar"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; @@ -44,6 +45,7 @@ import { } from "@/new/photos/services/search"; import type { SearchOption } from "@/new/photos/services/search/types"; import { initSettings } from "@/new/photos/services/settings"; +import { getLocalFamilyData } from "@/new/photos/services/user"; import { useAppContext } from "@/new/photos/types/context"; import { splitByPredicate } from "@/utils/array"; import { ensure } from "@/utils/ensure"; @@ -129,8 +131,6 @@ import { handleCollectionOps, } from "utils/collection"; import { FILE_OPS_TYPE, getSelectedFiles, handleFileOps } from "utils/file"; -import { getSessionExpiredMessage } from "utils/ui"; -import { getLocalFamilyData } from "utils/user/family"; const defaultGalleryContext: GalleryContextType = { showPlanSelectorModal: () => null, @@ -228,6 +228,7 @@ export default function Gallery() { showLoadingBar, hideLoadingBar, setDialogMessage, + showMiniDialog, logout, ...appContext } = useAppContext(); @@ -548,9 +549,8 @@ export default function Gallery() { }; }, [selectAll, clearSelection]); - const showSessionExpiredMessage = () => { - setDialogMessage(getSessionExpiredMessage(logout)); - }; + const showSessionExpiredDialog = () => + showMiniDialog(sessionExpiredDialogAttributes(logout)); const syncWithRemote = async (force = false, silent = false) => { if (!navigator.onLine) return; @@ -609,7 +609,7 @@ export default function Gallery() { } catch (e) { switch (e.message) { case CustomError.SESSION_EXPIRED: - showSessionExpiredMessage(); + showSessionExpiredDialog(); break; case CustomError.KEY_MISSING: clearKeys(); @@ -1026,6 +1026,7 @@ export default function Gallery() { isFirstUpload={areOnlySystemCollections( collectionSummaries, )} + showSessionExpiredMessage={showSessionExpiredDialog} {...{ dragAndDropFiles, openFileSelector, @@ -1036,7 +1037,6 @@ export default function Gallery() { fileSelectorZipFiles, uploadTypeSelectorIntent, uploadTypeSelectorView, - showSessionExpiredMessage, }} /> ), }); - -export const getSessionExpiredMessage = ( - action: () => void, -): DialogBoxAttributes => ({ - title: t("session_expired"), - content: t("session_expired_message"), - - nonClosable: true, - proceed: { - text: t("login"), - action, - variant: "accent", - }, -}); diff --git a/web/apps/photos/src/utils/user/family.ts b/web/apps/photos/src/utils/user/family.ts deleted file mode 100644 index 0456976cc2..0000000000 --- a/web/apps/photos/src/utils/user/family.ts +++ /dev/null @@ -1,43 +0,0 @@ -import log from "@/base/log"; -import type { FamilyData, FamilyMember } from "@/new/photos/services/user"; -import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; -import type { User } from "@ente/shared/user/types"; - -export function getLocalFamilyData(): FamilyData { - return getData(LS_KEYS.FAMILY_DATA); -} - -// isPartOfFamily return true if the current user is part of some family plan -export function isPartOfFamily(familyData: FamilyData): boolean { - return Boolean( - familyData && familyData.members && familyData.members.length > 0, - ); -} - -// hasNonAdminFamilyMembers return true if the admin user has members in his family -export function hasNonAdminFamilyMembers(familyData: FamilyData): boolean { - return Boolean(isPartOfFamily(familyData) && familyData.members.length > 1); -} - -export function isFamilyAdmin(familyData: FamilyData): boolean { - const familyAdmin: FamilyMember = getFamilyPlanAdmin(familyData); - const user: User = getData(LS_KEYS.USER); - return familyAdmin.email === user.email; -} - -export function getFamilyPlanAdmin(familyData: FamilyData): FamilyMember { - if (isPartOfFamily(familyData)) { - return familyData.members.find((x) => x.isAdmin); - } else { - log.error( - "invalid getFamilyPlanAdmin call - verify user is part of family plan before calling this method", - ); - } -} - -export function getTotalFamilyUsage(familyData: FamilyData): number { - return familyData.members.reduce( - (sum, currentMember) => sum + currentMember.usage, - 0, - ); -} diff --git a/web/apps/photos/src/utils/user/index.ts b/web/apps/photos/src/utils/user/index.ts deleted file mode 100644 index 7404fb5887..0000000000 --- a/web/apps/photos/src/utils/user/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { getData, LS_KEYS } from "@ente/shared/storage/localStorage"; -import { UserDetails } from "types/user"; - -export function getLocalUserDetails(): UserDetails { - return getData(LS_KEYS.USER_DETAILS)?.value; -} diff --git a/web/packages/base/components/mui/Container.tsx b/web/packages/base/components/mui/Container.tsx index d985fc033a..abaa0e2dce 100644 --- a/web/packages/base/components/mui/Container.tsx +++ b/web/packages/base/components/mui/Container.tsx @@ -24,3 +24,15 @@ export const CenteredBox = styled("div")` justify-content: center; align-items: center; `; + +/** + * An absolute positioned div that fills the entire nearest relatively + * positioned ancestor. + */ +export const Overlay = styled("div")` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +`; diff --git a/web/packages/gallery/.eslintrc.js b/web/packages/gallery/.eslintrc.js new file mode 100644 index 0000000000..348075cd4f --- /dev/null +++ b/web/packages/gallery/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["@/build-config/eslintrc-next"], +}; diff --git a/web/packages/gallery/README.md b/web/packages/gallery/README.md new file mode 100644 index 0000000000..49cddae88d --- /dev/null +++ b/web/packages/gallery/README.md @@ -0,0 +1,12 @@ +## @/gallery + +A package for sharing code between our apps that show media (photos, videos) in +a gallery like view. + +Specifically, this is the intersection of code required by both the photos and +public albums apps. + +### Packaging + +This (internal) package exports a React TypeScript library. We rely on the +importing project to transpile and bundle it. diff --git a/web/packages/gallery/package.json b/web/packages/gallery/package.json new file mode 100644 index 0000000000..4281b87fd9 --- /dev/null +++ b/web/packages/gallery/package.json @@ -0,0 +1,6 @@ +{ + "name": "@/gallery", + "version": "0.0.0", + "private": true, + "dependencies": {} +} diff --git a/web/packages/gallery/tsconfig.json b/web/packages/gallery/tsconfig.json new file mode 100644 index 0000000000..b2a1203623 --- /dev/null +++ b/web/packages/gallery/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@/build-config/tsconfig-next.json", + "include": [".", "../../packages/base/global-electron.d.ts"] +} diff --git a/web/packages/media/upload.ts b/web/packages/gallery/upload.ts similarity index 100% rename from web/packages/media/upload.ts rename to web/packages/gallery/upload.ts diff --git a/web/packages/new/photos/components/PlaceholderThumbnails.tsx b/web/packages/new/photos/components/PlaceholderThumbnails.tsx index 0fcab06569..d6fab83a28 100644 --- a/web/packages/new/photos/components/PlaceholderThumbnails.tsx +++ b/web/packages/new/photos/components/PlaceholderThumbnails.tsx @@ -1,5 +1,5 @@ +import { Overlay } from "@/base/components/mui/Container"; import { FileType } from "@/media/file-type"; -import { Overlay } from "@ente/shared/components/Container"; import PhotoOutlined from "@mui/icons-material/PhotoOutlined"; import PlayCircleOutlineOutlined from "@mui/icons-material/PlayCircleOutlineOutlined"; import { styled } from "@mui/material"; diff --git a/web/packages/new/photos/components/gallery/BarImpl.tsx b/web/packages/new/photos/components/gallery/BarImpl.tsx index 6821b622b0..29c9d7987c 100644 --- a/web/packages/new/photos/components/gallery/BarImpl.tsx +++ b/web/packages/new/photos/components/gallery/BarImpl.tsx @@ -1,3 +1,4 @@ +import { Overlay } from "@/base/components/mui/Container"; import { useIsSmallWidth } from "@/base/hooks"; import { CollectionsSortOptions } from "@/new/photos/components/CollectionsSortOptions"; import { FilledIconButton } from "@/new/photos/components/mui"; @@ -18,7 +19,6 @@ import type { } from "@/new/photos/services/collection/ui"; import type { Person } from "@/new/photos/services/ml/people"; import { ensure } from "@/utils/ensure"; -import { Overlay } from "@ente/shared/components/Container"; import ArchiveIcon from "@mui/icons-material/Archive"; import ExpandMore from "@mui/icons-material/ExpandMore"; import Favorite from "@mui/icons-material/FavoriteRounded"; diff --git a/web/packages/new/photos/services/settings.ts b/web/packages/new/photos/services/settings.ts index 063fb378e0..92a6a2d149 100644 --- a/web/packages/new/photos/services/settings.ts +++ b/web/packages/new/photos/services/settings.ts @@ -6,7 +6,7 @@ import { localUser } from "@/base/local-user"; import log from "@/base/log"; -import { updateShouldDisableCFUploadProxy } from "@/media/upload"; +import { updateShouldDisableCFUploadProxy } from "@/gallery/upload"; import { nullToUndefined } from "@/utils/transform"; import { z } from "zod"; import { fetchFeatureFlags, updateRemoteFlag } from "./remote-store"; diff --git a/web/packages/new/photos/services/user.ts b/web/packages/new/photos/services/user.ts index 58bd087e74..5c20573cab 100644 --- a/web/packages/new/photos/services/user.ts +++ b/web/packages/new/photos/services/user.ts @@ -1,5 +1,8 @@ import { authenticatedRequestHeaders, ensureOk } from "@/base/http"; +import log from "@/base/log"; import { apiURL } from "@/base/origins"; +import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; +import type { User } from "@ente/shared/user/types"; import { z } from "zod"; export interface FamilyMember { @@ -15,6 +18,52 @@ export interface FamilyData { members: FamilyMember[]; } +export function getLocalFamilyData(): FamilyData { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return getData(LS_KEYS.FAMILY_DATA); +} + +// isPartOfFamily return true if the current user is part of some family plan +export function isPartOfFamily(familyData: FamilyData): boolean { + return Boolean( + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain, @typescript-eslint/no-unnecessary-condition + familyData && familyData.members && familyData.members.length > 0, + ); +} + +// hasNonAdminFamilyMembers return true if the admin user has members in his family +export function hasNonAdminFamilyMembers(familyData: FamilyData): boolean { + return Boolean(isPartOfFamily(familyData) && familyData.members.length > 1); +} + +export function isFamilyAdmin(familyData: FamilyData): boolean { + const familyAdmin: FamilyMember = getFamilyPlanAdmin(familyData); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const user: User = getData(LS_KEYS.USER); + return familyAdmin.email === user.email; +} + +export function getFamilyPlanAdmin(familyData: FamilyData): FamilyMember { + if (isPartOfFamily(familyData)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return familyData.members.find((x) => x.isAdmin)!; + } else { + log.error( + "invalid getFamilyPlanAdmin call - verify user is part of family plan before calling this method", + ); + throw new Error( + "invalid getFamilyPlanAdmin call - verify user is part of family plan before calling this method", + ); + } +} + +export function getTotalFamilyUsage(familyData: FamilyData): number { + return familyData.members.reduce( + (sum, currentMember) => sum + currentMember.usage, + 0, + ); +} + /** * Fetch the two-factor status (whether or not it is enabled) from remote. */ diff --git a/web/packages/shared/components/Container.tsx b/web/packages/shared/components/Container.tsx index 9a1eb9666e..7852439cb4 100644 --- a/web/packages/shared/components/Container.tsx +++ b/web/packages/shared/components/Container.tsx @@ -32,14 +32,6 @@ export const FluidContainer = styled(FlexWrapper)` flex: 1; `; -export const Overlay = styled(Box)` - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; -`; - export const HorizontalFlex = styled(Box)({ display: "flex", });