Pre release (#1213)

This commit is contained in:
Abhinav Kumar 2023-06-30 14:16:14 +05:30 committed by GitHub
commit c3724b8bbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 1335 additions and 471 deletions

View file

@ -2,11 +2,6 @@ const cp = require('child_process');
const { getIsSentryEnabled } = require('./sentryConfigUtil');
module.exports = {
COOP_COEP_HEADERS: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
WEB_SECURITY_HEADERS: {
'Strict-Transport-Security': ' max-age=63072000',
'X-Content-Type-Options': 'nosniff',
@ -20,7 +15,7 @@ module.exports = {
// self is safe enough
'default-src': "'self'",
// data to allow two factor qr code
'img-src': "'self' blob: data:",
'img-src': "'self' blob: data: https://*.openstreetmap.org",
'media-src': "'self' blob:",
'manifest-src': "'self'",
'style-src': "'self' 'unsafe-inline'",

View file

@ -53,6 +53,8 @@
"idb": "^7.0.0",
"is-electron": "^2.2.0",
"jszip": "3.8.0",
"leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "^0.1.1",
"libsodium-wrappers": "^0.7.8",
"localforage": "^1.9.0",
"memoize-one": "^6.0.0",
@ -93,6 +95,7 @@
"@next/bundle-analyzer": "^13.1.6",
"@types/bs58": "^4.0.1",
"@types/debounce-promise": "^3.1.3",
"@types/leaflet": "^1.9.3",
"@types/libsodium-wrappers": "^0.7.8",
"@types/node": "^14.6.4",
"@types/photoswipe": "^4.1.1",

View file

@ -232,6 +232,15 @@
"CAPTION_PLACEHOLDER": "",
"LOCATION": "",
"SHOW_ON_MAP": "",
"MAP": "",
"MAP_SETTINGS": "",
"ENABLE_MAPS": "",
"DISABLE_MAPS": "",
"ENABLE_MAP_DESCRIPTION_1": "",
"ENABLE_MAP_DESCRIPTION_2": "",
"ENABLE_MAP_DESCRIPTION_3": "",
"DISABLE_MAP_DESCRIPTION_1": "",
"DISABLE_MAP_DESCRIPTION_2": "",
"DETAILS": "",
"VIEW_EXIF": "",
"NO_EXIF": "",

View file

@ -232,6 +232,14 @@
"CAPTION_PLACEHOLDER": "Add a description",
"LOCATION": "Location",
"SHOW_ON_MAP": "View on OpenStreetMap",
"MAP":"Map",
"MAP_SETTINGS":"Map Settings",
"ENABLE_MAPS":"Enable Maps?",
"ENABLE_MAP":"Enable map",
"DISABLE_MAPS":"Disable Maps?",
"ENABLE_MAP_DESCRIPTION":"<p>This will show your photos on a world map.</p> <p>The map is hosted by <a>OpenStreetMap</a>, and the exact locations of your photos are never shared.</p> <p>You can disable this feature anytime from Settings.</p>",
"DISABLE_MAP_DESCRIPTION":"<p>This will disable the display of your photos on a world map.</p> <p>You can enable this feature anytime from Settings.</p>",
"DISABLE_MAP":"Disable map",
"DETAILS": "Details",
"VIEW_EXIF": "View all EXIF data",
"NO_EXIF": "No EXIF data",

View file

@ -232,6 +232,15 @@
"CAPTION_PLACEHOLDER": "Añadir una descripción",
"LOCATION": "Localización",
"SHOW_ON_MAP": "Ver en OpenStreetMap",
"MAP": "",
"MAP_SETTINGS": "",
"ENABLE_MAPS": "",
"DISABLE_MAPS": "",
"ENABLE_MAP_DESCRIPTION_1": "",
"ENABLE_MAP_DESCRIPTION_2": "",
"ENABLE_MAP_DESCRIPTION_3": "",
"DISABLE_MAP_DESCRIPTION_1": "",
"DISABLE_MAP_DESCRIPTION_2": "",
"DETAILS": "Detalles",
"VIEW_EXIF": "Ver todos los datos de EXIF",
"NO_EXIF": "No hay datos EXIF",

View file

@ -232,6 +232,15 @@
"CAPTION_PLACEHOLDER": "",
"LOCATION": "",
"SHOW_ON_MAP": "",
"MAP": "",
"MAP_SETTINGS": "",
"ENABLE_MAPS": "",
"DISABLE_MAPS": "",
"ENABLE_MAP_DESCRIPTION_1": "",
"ENABLE_MAP_DESCRIPTION_2": "",
"ENABLE_MAP_DESCRIPTION_3": "",
"DISABLE_MAP_DESCRIPTION_1": "",
"DISABLE_MAP_DESCRIPTION_2": "",
"DETAILS": "",
"VIEW_EXIF": "",
"NO_EXIF": "",

View file

@ -232,6 +232,15 @@
"CAPTION_PLACEHOLDER": "",
"LOCATION": "",
"SHOW_ON_MAP": "",
"MAP": "",
"MAP_SETTINGS": "",
"ENABLE_MAPS": "",
"DISABLE_MAPS": "",
"ENABLE_MAP_DESCRIPTION_1": "",
"ENABLE_MAP_DESCRIPTION_2": "",
"ENABLE_MAP_DESCRIPTION_3": "",
"DISABLE_MAP_DESCRIPTION_1": "",
"DISABLE_MAP_DESCRIPTION_2": "",
"DETAILS": "",
"VIEW_EXIF": "",
"NO_EXIF": "",

View file

@ -232,6 +232,15 @@
"CAPTION_PLACEHOLDER": "Ajouter une description",
"LOCATION": "Emplacement",
"SHOW_ON_MAP": "Visualiser sur OpenStreetMap",
"MAP": "",
"MAP_SETTINGS": "",
"ENABLE_MAPS": "",
"DISABLE_MAPS": "",
"ENABLE_MAP_DESCRIPTION_1": "",
"ENABLE_MAP_DESCRIPTION_2": "",
"ENABLE_MAP_DESCRIPTION_3": "",
"DISABLE_MAP_DESCRIPTION_1": "",
"DISABLE_MAP_DESCRIPTION_2": "",
"DETAILS": "Détails",
"VIEW_EXIF": "Visualiser toutes les données EXIF",
"NO_EXIF": "Aucune donnée EXIF",

View file

@ -232,6 +232,15 @@
"CAPTION_PLACEHOLDER": "",
"LOCATION": "",
"SHOW_ON_MAP": "",
"MAP": "",
"MAP_SETTINGS": "",
"ENABLE_MAPS": "",
"DISABLE_MAPS": "",
"ENABLE_MAP_DESCRIPTION_1": "",
"ENABLE_MAP_DESCRIPTION_2": "",
"ENABLE_MAP_DESCRIPTION_3": "",
"DISABLE_MAP_DESCRIPTION_1": "",
"DISABLE_MAP_DESCRIPTION_2": "",
"DETAILS": "",
"VIEW_EXIF": "",
"NO_EXIF": "",

View file

@ -232,6 +232,15 @@
"CAPTION_PLACEHOLDER": "Voeg een beschrijving toe",
"LOCATION": "Locatie",
"SHOW_ON_MAP": "Bekijk op OpenStreetMap",
"MAP": "",
"MAP_SETTINGS": "",
"ENABLE_MAPS": "",
"DISABLE_MAPS": "",
"ENABLE_MAP_DESCRIPTION_1": "",
"ENABLE_MAP_DESCRIPTION_2": "",
"ENABLE_MAP_DESCRIPTION_3": "",
"DISABLE_MAP_DESCRIPTION_1": "",
"DISABLE_MAP_DESCRIPTION_2": "",
"DETAILS": "Details",
"VIEW_EXIF": "Bekijk alle EXIF gegevens",
"NO_EXIF": "Geen EXIF gegevens",

View file

@ -232,6 +232,15 @@
"CAPTION_PLACEHOLDER": "",
"LOCATION": "",
"SHOW_ON_MAP": "",
"MAP": "",
"MAP_SETTINGS": "",
"ENABLE_MAPS": "",
"DISABLE_MAPS": "",
"ENABLE_MAP_DESCRIPTION_1": "",
"ENABLE_MAP_DESCRIPTION_2": "",
"ENABLE_MAP_DESCRIPTION_3": "",
"DISABLE_MAP_DESCRIPTION_1": "",
"DISABLE_MAP_DESCRIPTION_2": "",
"DETAILS": "",
"VIEW_EXIF": "",
"NO_EXIF": "",

View file

@ -232,6 +232,15 @@
"CAPTION_PLACEHOLDER": "",
"LOCATION": "",
"SHOW_ON_MAP": "",
"MAP": "",
"MAP_SETTINGS": "",
"ENABLE_MAPS": "",
"DISABLE_MAPS": "",
"ENABLE_MAP_DESCRIPTION_1": "",
"ENABLE_MAP_DESCRIPTION_2": "",
"ENABLE_MAP_DESCRIPTION_3": "",
"DISABLE_MAP_DESCRIPTION_1": "",
"DISABLE_MAP_DESCRIPTION_2": "",
"DETAILS": "",
"VIEW_EXIF": "",
"NO_EXIF": "",

View file

@ -232,6 +232,15 @@
"CAPTION_PLACEHOLDER": "",
"LOCATION": "",
"SHOW_ON_MAP": "",
"MAP": "",
"MAP_SETTINGS": "",
"ENABLE_MAPS": "",
"DISABLE_MAPS": "",
"ENABLE_MAP_DESCRIPTION_1": "",
"ENABLE_MAP_DESCRIPTION_2": "",
"ENABLE_MAP_DESCRIPTION_3": "",
"DISABLE_MAP_DESCRIPTION_1": "",
"DISABLE_MAP_DESCRIPTION_2": "",
"DETAILS": "",
"VIEW_EXIF": "",
"NO_EXIF": "",

View file

@ -232,6 +232,15 @@
"CAPTION_PLACEHOLDER": "添加说明",
"LOCATION": "地理位置",
"SHOW_ON_MAP": "在 OpenStreetMap 上查看",
"MAP": "",
"MAP_SETTINGS": "",
"ENABLE_MAPS": "",
"DISABLE_MAPS": "",
"ENABLE_MAP_DESCRIPTION_1": "",
"ENABLE_MAP_DESCRIPTION_2": "",
"ENABLE_MAP_DESCRIPTION_3": "",
"DISABLE_MAP_DESCRIPTION_1": "",
"DISABLE_MAP_DESCRIPTION_2": "",
"DETAILS": "详情",
"VIEW_EXIF": "查看所有 EXIF 数据",
"NO_EXIF": "无 EXIF 数据",

View file

@ -0,0 +1,80 @@
import { useEffect, useRef } from 'react';
import { styled } from '@mui/material';
import { runningInBrowser } from 'utils/common';
import { MapButton } from './MapButton';
import 'leaflet/dist/leaflet.css';
import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css'; // Re-uses images from ~leaflet package
import { t } from 'i18next';
runningInBrowser() && require('leaflet-defaulticon-compatibility');
const L = runningInBrowser()
? (require('leaflet') as typeof import('leaflet'))
: null;
const LAYER_TILE_URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
const LAYER_TILE_ATTRIBUTION =
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
const ZOOM_LEVEL = 16;
const MapBoxContainer = styled('div')`
height: 200px;
width: 100%;
`;
const MapBoxEnableContainer = styled(MapBoxContainer)`
position: relative;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.09);
`;
interface MapBoxProps {
location: { latitude: number; longitude: number };
mapEnabled: boolean;
openUpdateMapConfirmationDialog: () => void;
}
const MapBox: React.FC<MapBoxProps> = ({
location,
mapEnabled,
openUpdateMapConfirmationDialog,
}) => {
const mapBoxContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const mapContainer = mapBoxContainerRef.current;
if (mapEnabled) {
const position: L.LatLngTuple = [
location.latitude,
location.longitude,
];
if (mapContainer && !mapContainer.hasChildNodes()) {
const map = L.map(mapContainer).setView(position, ZOOM_LEVEL);
L.tileLayer(LAYER_TILE_URL, {
attribution: LAYER_TILE_ATTRIBUTION,
}).addTo(map);
L.marker(position).addTo(map).openPopup();
}
} else {
if (mapContainer && mapContainer.hasChildNodes()) {
if (mapContainer.firstChild) {
console.log('removing child');
mapContainer.removeChild(mapContainer.firstChild);
}
}
}
}, [mapEnabled]);
return mapEnabled ? (
<MapBoxContainer ref={mapBoxContainerRef} />
) : (
<MapBoxEnableContainer>
<MapButton onClick={openUpdateMapConfirmationDialog}>
{' '}
{t('ENABLE_MAP')}
</MapButton>
</MapBoxEnableContainer>
);
};
export default MapBox;

View file

@ -0,0 +1,9 @@
import { Button, ButtonProps, styled } from '@mui/material';
import { CSSProperties } from '@mui/material/styles/createTypography';
export const MapButton = styled((props: ButtonProps) => (
<Button color="secondary" {...props} />
))(({ theme }) => ({
...(theme.typography.small as CSSProperties),
padding: '8px',
}));

View file

@ -7,6 +7,7 @@ import { RenderCaption } from './RenderCaption';
import CopyButton from 'components/CodeBlock/CopyButton';
import { formatDate, formatTime } from 'utils/time/format';
import Titlebar from 'components/Titlebar';
import MapBox from './MapBox';
import InfoItem from './InfoItem';
import { FlexWrapper } from 'components/Container';
import EnteSpinner from 'components/EnteSpinner';
@ -32,6 +33,10 @@ import { ObjectLabelList } from 'components/MachineLearning/ObjectList';
import { AppContext } from 'pages/_app';
import { t } from 'i18next';
import { GalleryContext } from 'pages/gallery';
import {
getMapDisableConfirmationDialog,
getMapEnableConfirmationDialog,
} from 'utils/ui';
export const FileInfoSidebar = styled((props: DialogProps) => (
<EnteDrawer {...props} anchor="right" />
@ -170,9 +175,24 @@ export function FileInfo({
}
const onCollectionChipClick = (collectionID) => {
galleryContext.setActiveCollection(collectionID);
galleryContext.setIsInSearchMode(false);
closePhotoViewer();
};
const openEnableMapConfirmationDialog = () =>
appContext.setDialogBoxAttributesV2(
getMapEnableConfirmationDialog(() =>
appContext.updateMapEnabled(true)
)
);
const openDisableMapConfirmationDialog = () =>
appContext.setDialogBoxAttributesV2(
getMapDisableConfirmationDialog(() =>
appContext.updateMapEnabled(false)
)
);
return (
<FileInfoSidebar open={showInfo} onClose={handleCloseInfo}>
<Titlebar onClose={handleCloseInfo} title={t('INFO')} backIsClose />
@ -210,25 +230,48 @@ export function FileInfo({
)}
{location && (
<InfoItem
icon={<LocationOnOutlined />}
title={t('LOCATION')}
caption={
<Link
href={getOpenStreetMapLink(location)}
target="_blank"
sx={{ fontWeight: 'bold' }}>
{t('SHOW_ON_MAP')}
</Link>
}
customEndButton={
<CopyButton
code={getOpenStreetMapLink(location)}
color="secondary"
size="medium"
/>
}
/>
<>
<InfoItem
icon={<LocationOnOutlined />}
title={t('LOCATION')}
caption={
!appContext.mapEnabled ? (
<Link
href={getOpenStreetMapLink(location)}
target="_blank"
sx={{ fontWeight: 'bold' }}>
{t('SHOW_ON_MAP')}
</Link>
) : (
<LinkButton
onClick={
openDisableMapConfirmationDialog
}
sx={{
textDecoration: 'none',
color: 'text.muted',
fontWeight: 'bold',
}}>
{t('DISABLE_MAP')}
</LinkButton>
)
}
customEndButton={
<CopyButton
code={getOpenStreetMapLink(location)}
color="secondary"
size="medium"
/>
}
/>
<MapBox
location={location}
mapEnabled={appContext.mapEnabled}
openUpdateMapConfirmationDialog={
openEnableMapConfirmationDialog
}
/>
</>
)}
<InfoItem
icon={<TextSnippetOutlined />}

View file

@ -7,6 +7,7 @@ import MenuSectionTitle from 'components/Menu/MenuSectionTitle';
import Titlebar from 'components/Titlebar';
import { useState } from 'react';
import { t } from 'i18next';
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
import { MenuItemGroup } from 'components/Menu/MenuItemGroup';

View file

@ -0,0 +1,34 @@
import { Stack, Box, Button, Typography } from '@mui/material';
import Titlebar from 'components/Titlebar';
import { Trans } from 'react-i18next';
import { t } from 'i18next';
export default function EnableMap({ onClose, disableMap, onRootClose }) {
return (
<Stack spacing={'4px'} py={'12px'}>
<Titlebar
onClose={onClose}
title={t('DISABLE_MAPS')}
onRootClose={onRootClose}
/>
<Stack py={'20px'} px={'8px'} spacing={'32px'}>
<Box px={'8px'}>
<Typography color="text.muted">
<Trans i18nKey={'DISABLE_MAP_DESCRIPTION'} />
</Typography>
</Box>
<Stack px={'8px'} spacing={'8px'}>
<Button
color={'critical'}
size="large"
onClick={disableMap}>
{t('DISABLE')}
</Button>
<Button color={'secondary'} size="large" onClick={onClose}>
{t('CANCEL')}
</Button>
</Stack>
</Stack>
</Stack>
);
}

View file

@ -0,0 +1,43 @@
import { Stack, Box, Button, Typography, Link } from '@mui/material';
import Titlebar from 'components/Titlebar';
import { Trans } from 'react-i18next';
import { t } from 'i18next';
export const OPEN_STREET_MAP_LINK = 'https://www.openstreetmap.org/';
export default function EnableMap({ onClose, enableMap, onRootClose }) {
return (
<Stack spacing={'4px'} py={'12px'}>
<Titlebar
onClose={onClose}
title={t('ENABLE_MAPS')}
onRootClose={onRootClose}
/>
<Stack py={'20px'} px={'8px'} spacing={'32px'}>
<Box px={'8px'}>
{' '}
<Typography color="text.muted">
<Trans
i18nKey={'ENABLE_MAP_DESCRIPTION'}
components={{
a: (
<Link
target="_blank"
href={OPEN_STREET_MAP_LINK}
/>
),
}}
/>
</Typography>
</Box>
<Stack px={'8px'} spacing={'8px'}>
<Button color={'accent'} size="large" onClick={enableMap}>
{t('ENABLE')}
</Button>
<Button color={'secondary'} size="large" onClick={onClose}>
{t('CANCEL')}
</Button>
</Stack>
</Stack>
</Stack>
);
}

View file

@ -0,0 +1,75 @@
import { Box, DialogProps } from '@mui/material';
import { EnteDrawer } from 'components/EnteDrawer';
import { AppContext } from 'pages/_app';
import { useContext } from 'react';
import { logError } from 'utils/sentry';
import EnableMap from '../EnableMap';
import DisableMap from '../DisableMap';
const ModifyMapEnabled = ({ open, onClose, onRootClose, mapEnabled }) => {
const { somethingWentWrong, updateMapEnabled } = useContext(AppContext);
const disableMap = async () => {
try {
await updateMapEnabled(false);
onClose();
} catch (e) {
logError(e, 'Disable Map failed');
somethingWentWrong();
}
};
const enableMap = async () => {
try {
await updateMapEnabled(true);
onClose();
} catch (e) {
logError(e, 'Enable Map failed');
somethingWentWrong();
}
};
const handleRootClose = () => {
onClose();
onRootClose();
};
const handleDrawerClose: DialogProps['onClose'] = (_, reason) => {
if (reason === 'backdropClick') {
handleRootClose();
} else {
onClose();
}
};
return (
<Box>
<EnteDrawer
anchor="left"
transitionDuration={0}
open={open}
onClose={handleDrawerClose}
slotProps={{
backdrop: {
sx: { '&&&': { backgroundColor: 'transparent' } },
},
}}>
{mapEnabled ? (
<DisableMap
onClose={onClose}
disableMap={disableMap}
onRootClose={handleRootClose}
/>
) : (
<EnableMap
onClose={onClose}
enableMap={enableMap}
onRootClose={handleRootClose}
/>
)}
</EnteDrawer>
</Box>
);
};
export default ModifyMapEnabled;

View file

@ -0,0 +1,81 @@
import { Box, DialogProps, Stack } from '@mui/material';
import { EnteDrawer } from 'components/EnteDrawer';
import Titlebar from 'components/Titlebar';
import { useContext, useEffect, useState } from 'react';
import { t } from 'i18next';
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
import { MenuItemGroup } from 'components/Menu/MenuItemGroup';
import ModifyMapEnabled from './ModifyMapEnabled';
import { getMapEnabledStatus } from 'services/userService';
import { AppContext } from 'pages/_app';
export default function MapSettings({ open, onClose, onRootClose }) {
const { mapEnabled, updateMapEnabled } = useContext(AppContext);
const [modifyMapEnabledView, setModifyMapEnabledView] = useState(false);
const openModifyMapEnabled = () => setModifyMapEnabledView(true);
const closeModifyMapEnabled = () => setModifyMapEnabledView(false);
useEffect(() => {
if (!open) {
return;
}
const main = async () => {
const remoteMapValue = await getMapEnabledStatus();
updateMapEnabled(remoteMapValue);
};
main();
}, [open]);
const handleRootClose = () => {
onClose();
onRootClose();
};
const handleDrawerClose: DialogProps['onClose'] = (_, reason) => {
if (reason === 'backdropClick') {
handleRootClose();
} else {
onClose();
}
};
return (
<EnteDrawer
transitionDuration={0}
open={open}
onClose={handleDrawerClose}
BackdropProps={{
sx: { '&&&': { backgroundColor: 'transparent' } },
}}>
<Stack spacing={'4px'} py={'12px'}>
<Titlebar
onClose={onClose}
title={t('MAP')}
onRootClose={handleRootClose}
/>
<Box px={'8px'}>
<Stack py="20px" spacing="24px">
<Box>
<MenuItemGroup>
<EnteMenuItem
onClick={openModifyMapEnabled}
variant="toggle"
checked={mapEnabled}
label={t('MAP_SETTINGS')}
/>
</MenuItemGroup>
</Box>
</Stack>
</Box>
</Stack>
<ModifyMapEnabled
open={modifyMapEnabledView}
mapEnabled={mapEnabled}
onClose={closeModifyMapEnabled}
onRootClose={handleRootClose}
/>
</EnteDrawer>
);
}

View file

@ -7,15 +7,20 @@ import { useState } from 'react';
import { t } from 'i18next';
import AdvancedSettings from '../AdvancedSettings';
import MapSettings from '../MapSetting';
import { LanguageSelector } from './LanguageSelector';
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
export default function Preferences({ open, onClose, onRootClose }) {
const [advancedSettingsView, setAdvancedSettingsView] = useState(false);
const [mapSettingsView, setMapSettingsView] = useState(false);
const openAdvancedSettings = () => setAdvancedSettingsView(true);
const closeAdvancedSettings = () => setAdvancedSettingsView(false);
const openMapSettings = () => setMapSettingsView(true);
const closeMapSettings = () => setMapSettingsView(false);
const handleRootClose = () => {
onClose();
onRootClose();
@ -53,6 +58,11 @@ export default function Preferences({ open, onClose, onRootClose }) {
label={t('ADVANCED')}
/>
)}
<EnteMenuItem
onClick={openMapSettings}
endIcon={<ChevronRight />}
label={t('MAP')}
/>
</Stack>
</Box>
</Stack>
@ -61,6 +71,11 @@ export default function Preferences({ open, onClose, onRootClose }) {
onClose={closeAdvancedSettings}
onRootClose={onRootClose}
/>
<MapSettings
open={mapSettingsView}
onClose={closeMapSettings}
onRootClose={onRootClose}
/>
</EnteDrawer>
);
}

View file

@ -0,0 +1,76 @@
import React, { useState, useContext, useLayoutEffect } from 'react';
import { EnteFile } from 'types/file';
import { GalleryContext } from 'pages/gallery';
import { styled } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { logError } from 'utils/sentry';
interface AvatarProps {
file: EnteFile;
}
const PUBLIC_COLLECTED_FILES_AVATAR_COLOR_CODE = '#000000';
const AvatarBase = styled('div')<{ colorCode: string; size: number }>`
width: ${({ size }) => `${size}px`};
height: ${({ size }) => `${size}px`};
background-color: ${({ colorCode }) => `${colorCode}95`};
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-weight: bold;
font-size: ${({ size }) => `${Math.floor(size / 2)}px`};
`;
const Avatar: React.FC<AvatarProps> = ({ file }) => {
const { userIDToEmailMap, user } = useContext(GalleryContext);
const theme = useTheme();
const [colorCode, setColorCode] = useState('');
const [userLetter, setUserLetter] = useState('');
useLayoutEffect(() => {
try {
if (file.ownerID !== user.id) {
// getting email from in-memory id-email map
const email = userIDToEmailMap.get(file.ownerID);
if (!email) {
logError(Error(), 'email not found in userIDToEmailMap');
return;
}
const colorIndex =
file.ownerID % theme.colors.avatarColors.length;
const colorCode = theme.colors.avatarColors[colorIndex];
setUserLetter(email[0].toUpperCase());
setColorCode(colorCode);
} else if (file.ownerID === user.id) {
const uploaderName = file.pubMagicMetadata.data.uploaderName;
if (!uploaderName) {
logError(
Error(),
'uploaderName not found in file.pubMagicMetadata.data'
);
return;
}
setUserLetter(uploaderName[0].toUpperCase());
setColorCode(PUBLIC_COLLECTED_FILES_AVATAR_COLOR_CODE);
}
} catch (err) {
logError(err, 'AvatarIcon.tsx - useLayoutEffect failed');
}
}, []);
if (!colorCode || !userLetter) {
return <></>;
}
return (
<AvatarBase size={18} colorCode={colorCode}>
{userLetter}
</AvatarBase>
);
};
export default Avatar;

View file

@ -19,6 +19,9 @@ import {
} from 'components/PlaceholderThumbnails';
import { FILE_TYPE } from 'constants/file';
import AlbumOutlined from '@mui/icons-material/AlbumOutlined';
import Avatar from './Avatar';
import { User } from 'types/user';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
interface IProps {
file: EnteFile;
@ -108,6 +111,14 @@ export const HoverOverlay = styled('div')<{ checked: boolean }>`
'background:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0))'};
`;
export const AvatarOverlay = styled(Overlay)`
display: flex;
justify-content: flex-end;
align-items: flex-start;
padding-right: 5px;
padding-top: 5px;
`;
export const InSelectRangeOverLay = styled('div')<{ $active: boolean }>`
opacity: ${(props) => (!props.$active ? 0 : 1)};
left: 0;
@ -323,6 +334,8 @@ export default function PreviewCard(props: IProps) {
}
};
const user: User = getData(LS_KEYS.USER);
return (
<Cont
key={`thumb-${file.id}-${props.showPlaceholder}`}
@ -358,6 +371,14 @@ export default function PreviewCard(props: IProps) {
)
)}
<SelectedOverlay selected={selected} />
{(file.ownerID !== user.id ||
(file.ownerID === user.id &&
file.pubMagicMetadata?.data?.uploaderName)) && (
<AvatarOverlay>
<Avatar file={file} />
</AvatarOverlay>
)}
<HoverOverlay checked={selected} />
<InSelectRangeOverLay
$active={isRangeSelectActive && isInsSelectRange}

View file

@ -91,8 +91,6 @@ export const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = {
export const A_SEC_IN_MICROSECONDS = 1e6;
export const USE_CF_PROXY = false;
export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = {
rootFolderName: '',
hasNestedFolders: false,

View file

@ -36,6 +36,7 @@ import {
import {
getFamilyPortalRedirectURL,
getRoadmapRedirectURL,
updateMapEnabledStatus,
} from 'services/userService';
import { CustomError } from 'utils/error';
import {
@ -76,6 +77,7 @@ import {
import exportService from 'services/export';
import { ExportStage } from 'constants/export';
import { REDIRECTS } from 'constants/redirects';
import { getLocalMapEnabled, setLocalMapEnabled } from 'utils/storage';
const redirectMap = new Map([
[REDIRECTS.ROADMAP, getRoadmapRedirectURL],
@ -102,7 +104,9 @@ type AppContextType = {
redirectURL: string;
setRedirectURL: (url: string) => void;
mlSearchEnabled: boolean;
mapEnabled: boolean;
updateMlSearchEnabled: (enabled: boolean) => Promise<void>;
updateMapEnabled: (enabled: boolean) => Promise<void>;
startLoading: () => void;
finishLoading: () => void;
closeMessageDialog: () => void;
@ -147,6 +151,7 @@ export default function App(props) {
const [redirectName, setRedirectName] = useState<string>(null);
const [redirectURL, setRedirectURL] = useState(null);
const [mlSearchEnabled, setMlSearchEnabled] = useState(false);
const [mapEnabled, setMapEnabled] = useState(false);
const isLoadingBarRunning = useRef(false);
const loadingBar = useRef(null);
const [dialogMessage, setDialogMessage] = useState<DialogBoxAttributes>();
@ -249,6 +254,10 @@ export default function App(props) {
}
}, []);
useEffect(() => {
setMapEnabled(getLocalMapEnabled());
}, []);
useEffect(() => {
if (!isElectron()) {
return;
@ -394,6 +403,16 @@ export default function App(props) {
}
};
const updateMapEnabled = async (enabled: boolean) => {
try {
await updateMapEnabledStatus(enabled);
setLocalMapEnabled(enabled);
setMapEnabled(enabled);
} catch (e) {
logError(e, 'Error while updating mapEnabled');
}
};
const startLoading = () => {
!isLoadingBarRunning.current && loadingBar.current?.continuousStart();
isLoadingBarRunning.current = true;
@ -493,6 +512,8 @@ export default function App(props) {
setThemeColor,
somethingWentWrong,
setDialogBoxAttributesV2,
mapEnabled,
updateMapEnabled,
}}>
{(loading || !isI18nReady) && (
<Overlay

View file

@ -38,7 +38,11 @@ import {
setIsFirstLogin,
setJustSignedUp,
} from 'utils/storage';
import { isTokenValid, validateKey } from 'services/userService';
import {
isTokenValid,
syncMapEnabled,
validateKey,
} from 'services/userService';
import { useDropzone } from 'react-dropzone';
import EnteSpinner from 'components/EnteSpinner';
import { LoadingOverlay } from 'components/LoadingOverlay';
@ -120,6 +124,7 @@ import { IsArchived } from 'utils/magicMetadata';
import { isSameDayAnyYear, isInsideLocationTag } from 'utils/search';
import { getSessionExpiredMessage } from 'utils/ui';
import { syncEntities } from 'services/entityService';
import { constructUserIDToEmailMap } from 'services/collectionService';
export const DeadCenter = styled('div')`
flex: 1;
@ -137,10 +142,12 @@ const defaultGalleryContext: GalleryContextType = {
setActiveCollection: () => null,
syncWithRemote: () => null,
setBlockingLoad: () => null,
setIsInSearchMode: () => null,
photoListHeader: null,
openExportModal: () => null,
authenticateUser: () => null,
user: null,
userIDToEmailMap: null,
};
export const GalleryContext = createContext<GalleryContextType>(
@ -216,6 +223,8 @@ export default function Gallery() {
useContext(AppContext);
const [collectionSummaries, setCollectionSummaries] =
useState<CollectionSummaries>();
const [userIDToEmailMap, setUserIDToEmailMap] =
useState<Map<number, string>>(null);
const [activeCollection, setActiveCollection] = useState<number>(undefined);
const [fixCreationTimeView, setFixCreationTimeView] = useState(false);
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
@ -312,6 +321,17 @@ export default function Gallery() {
setDerivativeState(user, collections, files, trashedFiles, hiddenFiles);
}, [collections, files, hiddenFiles, trashedFiles, user]);
useEffect(() => {
const fetchData = async () => {
if (!collections) {
return;
}
const userIdToEmailMap = await constructUserIDToEmailMap();
setUserIDToEmailMap(userIdToEmailMap);
};
fetchData();
}, [collections]);
useEffect(() => {
collectionSelectorAttributes && setCollectionSelectorView(true);
}, [collectionSelectorAttributes]);
@ -564,6 +584,7 @@ export default function Gallery() {
await syncHiddenFiles(hiddenCollections, setHiddenFiles);
await syncTrash(collections, setTrashedFiles);
await syncEntities();
await syncMapEnabled();
} catch (e) {
switch (e.message) {
case ServerErrorCodes.SESSION_EXPIRED:
@ -602,7 +623,6 @@ export default function Gallery() {
setFavItemIds(favItemIds);
const archivedCollections = getArchivedCollections(collections);
setArchivedCollections(archivedCollections);
const collectionSummaries = await getCollectionSummaries(
user,
collections,
@ -849,9 +869,11 @@ export default function Gallery() {
setActiveCollection,
syncWithRemote,
setBlockingLoad,
setIsInSearchMode,
photoListHeader,
openExportModal,
authenticateUser,
userIDToEmailMap,
user,
}}>
<FullScreenDropZone

View file

@ -1260,3 +1260,30 @@ export async function unhideToCollection(
throw e;
}
}
export const constructUserIDToEmailMap = async (): Promise<
Map<number, string>
> => {
try {
const collection = await getLocalCollections();
const user: User = getData(LS_KEYS.USER);
const userIDToEmailMap = new Map<number, string>();
collection.map((item) => {
const { owner, sharees } = item;
if (user.id !== owner.id && owner.email) {
userIDToEmailMap.set(owner.id, owner.email);
}
if (sharees) {
sharees.map((item) => {
if (item.id !== user.id)
userIDToEmailMap.set(item.id, item.email);
});
}
});
return userIDToEmailMap;
} catch (e) {
logError('Error Mapping UserId to email:', e);
return new Map<number, string>();
throw e;
}
};

View file

@ -11,7 +11,6 @@ import { getFileType } from 'services/typeDetectionService';
import { getLocalTrashedFiles } from './trashService';
import { UploadURL } from 'types/upload';
import { S3FileAttributes } from 'types/file';
import { USE_CF_PROXY } from 'constants/upload';
import { Remote } from 'comlink';
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
@ -112,20 +111,12 @@ export async function uploadThumbnail(
updatedThumbnail,
fileKey
);
let thumbnailObjectKey: string = null;
if (USE_CF_PROXY) {
thumbnailObjectKey = await uploadHttpClient.putFileV2(
uploadURL,
encryptedThumbnail.encryptedData,
() => {}
);
} else {
thumbnailObjectKey = await uploadHttpClient.putFile(
uploadURL,
encryptedThumbnail.encryptedData,
() => {}
);
}
const thumbnailObjectKey = await uploadHttpClient.putFile(
uploadURL,
encryptedThumbnail.encryptedData,
() => {}
);
return {
objectKey: thumbnailObjectKey,
decryptionHeader: encryptedThumbnail.decryptionHeader,

View file

@ -1,7 +1,6 @@
import {
FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART,
RANDOM_PERCENTAGE_PROGRESS_FOR_PUT,
USE_CF_PROXY,
} from 'constants/upload';
import UIService from './uiService';
import UploadHttpClient from './uploadHttpClient';
@ -63,7 +62,7 @@ export async function uploadStreamInParts(
index
);
let eTag = null;
if (USE_CF_PROXY) {
if (!uploadService.getIsCFUploadProxyDisabled()) {
eTag = await UploadHttpClient.putFilePartV2(
fileUploadURL,
uploadChunk,
@ -117,7 +116,7 @@ async function completeMultipartUpload(
{ CompleteMultipartUpload: { Part: partEtags } },
options
);
if (USE_CF_PROXY) {
if (!uploadService.getIsCFUploadProxyDisabled()) {
await UploadHttpClient.completeMultipartUploadV2(completeURL, body);
} else {
await UploadHttpClient.completeMultipartUpload(completeURL, body);

View file

@ -37,6 +37,7 @@ import {
getPublicCollectionUID,
} from 'services/publicCollectionService';
import { getDedicatedCryptoWorker } from 'utils/comlink/ComlinkCryptoWorker';
import { getDisableCFUploadProxyFlag } from 'services/userService';
const MAX_CONCURRENT_UPLOADS = 4;
@ -61,7 +62,8 @@ class UploadManager {
publicCollectProps: PublicUploadProps
) {
UIService.init(progressUpdater);
UploadService.init(publicCollectProps);
const isCFUploadProxyDisabled = await getDisableCFUploadProxyFlag();
UploadService.init(publicCollectProps, isCFUploadProxyDisabled);
this.setFiles = setFiles;
this.publicUploadProps = publicCollectProps;
}

View file

@ -31,7 +31,6 @@ import {
import { encryptFile, getFileSize, readFile } from './fileService';
import { uploadStreamUsingMultipart } from './multiPartUploadService';
import UIService from './uiService';
import { USE_CF_PROXY } from 'constants/upload';
import { Remote } from 'comlink';
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
import publicUploadHttpClient from './publicUploadHttpClient';
@ -52,8 +51,14 @@ class UploadService {
private publicUploadProps: PublicUploadProps = undefined;
init(publicUploadProps: PublicUploadProps) {
private isCFUploadProxyDisabled: boolean = false;
init(
publicUploadProps: PublicUploadProps,
isCFUploadProxyDisabled: boolean
) {
this.publicUploadProps = publicUploadProps;
this.isCFUploadProxyDisabled = isCFUploadProxyDisabled;
}
async setFileCount(fileCount: number) {
@ -73,6 +78,10 @@ class UploadService {
return this.uploaderName;
}
getIsCFUploadProxyDisabled() {
return this.isCFUploadProxyDisabled;
}
reducePendingUploadCount() {
this.pendingUploadCount--;
}
@ -158,7 +167,7 @@ class UploadService {
file.localID
);
const fileUploadURL = await this.getUploadURL();
if (USE_CF_PROXY) {
if (!this.isCFUploadProxyDisabled) {
fileObjectKey = await UploadHttpClient.putFileV2(
fileUploadURL,
file.file.encryptedData as Uint8Array,
@ -174,7 +183,7 @@ class UploadService {
}
const thumbnailUploadURL = await this.getUploadURL();
let thumbnailObjectKey: string = null;
if (USE_CF_PROXY) {
if (!this.isCFUploadProxyDisabled) {
thumbnailObjectKey = await UploadHttpClient.putFileV2(
thumbnailUploadURL,
file.thumbnail.encryptedData,

View file

@ -19,6 +19,7 @@ import {
UserDetails,
DeleteChallengeResponse,
GetRemoteStoreValueResponse,
GetFeatureFlagResponse,
} from 'types/user';
import { ServerErrorCodes } from 'utils/error';
import isElectron from 'is-electron';
@ -28,6 +29,7 @@ import { B64EncryptionResult } from 'types/crypto';
import { getLocalFamilyData, isPartOfFamily } from 'utils/user/family';
import { AxiosResponse } from 'axios';
import { APPS, getAppName } from 'constants/apps';
import { setLocalMapEnabled } from 'utils/storage';
const ENDPOINT = getEndpoint();
@ -452,3 +454,66 @@ export const updateFaceSearchEnabledStatus = async (newStatus: boolean) => {
throw e;
}
};
export const syncMapEnabled = async () => {
try {
const status = await getMapEnabledStatus();
setLocalMapEnabled(status);
} catch (e) {
logError(e, 'failed to sync map enabled status');
throw e;
}
};
export const getMapEnabledStatus = async () => {
try {
const token = getToken();
const resp: AxiosResponse<GetRemoteStoreValueResponse> =
await HTTPService.get(
`${ENDPOINT}/remote-store`,
{
key: 'mapEnabled',
defaultValue: false,
},
{
'X-Auth-Token': token,
}
);
return resp.data.value === 'true';
} catch (e) {
logError(e, 'failed to get map enabled status');
throw e;
}
};
export const updateMapEnabledStatus = async (newStatus: boolean) => {
try {
const token = getToken();
await HTTPService.post(
`${ENDPOINT}/remote-store/update`,
{
key: 'mapEnabled',
value: newStatus.toString(),
},
null,
{
'X-Auth-Token': token,
}
);
} catch (e) {
logError(e, 'failed to update map enabled status');
throw e;
}
};
export async function getDisableCFUploadProxyFlag(): Promise<boolean> {
try {
const featureFlags = (
await fetch('https://static.ente.io/feature_flags.json')
).json() as GetFeatureFlagResponse;
return featureFlags.disableCFUploadProxy;
} catch (e) {
logError(e, 'failed to get feature flags');
return false;
}
}

View file

@ -63,6 +63,31 @@ const darkThemeColors: Omit<ThemeColorsOptions, keyof FixedColors> = {
},
],
},
avatarColors: [
'#76549A',
'#DF7861',
'#94B49F',
'#87A2FB',
'#C689C6',
'#937DC2',
'#325288',
'#85B4E0',
'#C1A3A3',
'#E1A059',
'#426165',
'#6B77B2',
'#957FEF',
'#DD9DE2',
'#82AB8B',
'#9BBBE8',
'#8FBEBE',
'#8AC3A1',
'#A8B0F2',
'#B0C695',
'#E99AAD',
'#D18484',
'#78B5A7',
],
};
export default darkThemeColors;

View file

@ -56,6 +56,31 @@ const lightThemeColors: Omit<ThemeColorsOptions, keyof FixedColors> = {
},
],
},
avatarColors: [
'#76549A',
'#DF7861',
'#94B49F',
'#87A2FB',
'#C689C6',
'#937DC2',
'#325288',
'#85B4E0',
'#C1A3A3',
'#E1A059',
'#426165',
'#6B77B2',
'#957FEF',
'#DD9DE2',
'#82AB8B',
'#9BBBE8',
'#8FBEBE',
'#8AC3A1',
'#A8B0F2',
'#B0C695',
'#E99AAD',
'#D18484',
'#78B5A7',
],
};
export default lightThemeColors;

View file

@ -111,6 +111,7 @@ declare module '@mui/material/styles' {
blur: BlurStrength;
white: Omit<Strength, 'faint'>;
black: Omit<Strength, 'faint'>;
avatarColors: AvatarColors;
}
interface ThemeColorsOptions {
@ -126,6 +127,7 @@ declare module '@mui/material/styles' {
blur?: Partial<BlurStrength>;
white?: Partial<Omit<Strength, 'faint'>>;
black?: Partial<Omit<Strength, 'faint'>>;
avatarColors?: Partial<AvatarColors>;
}
interface ColorStrength {
@ -192,5 +194,7 @@ declare module '@mui/material/styles' {
muted: number;
faint: number;
}
type AvatarColors = Array<string>;
}
export {};

View file

@ -33,10 +33,12 @@ export type GalleryContextType = {
setActiveCollection: (collection: number) => void;
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
setBlockingLoad: (value: boolean) => void;
setIsInSearchMode: (value: boolean) => void;
photoListHeader: TimeStampListItem;
openExportModal: () => void;
authenticateUser: (callback: () => void) => void;
user: User;
userIDToEmailMap: Map<number, string>;
};
export enum CollectionSelectorIntent {

View file

@ -102,3 +102,7 @@ export interface UpdateRemoteStoreValueRequest {
key: string;
value: string;
}
export interface GetFeatureFlagResponse {
disableCFUploadProxy?: boolean;
}

View file

@ -45,7 +45,7 @@ export const getPublicCollectionThumbnailURL = (id: number) => {
};
export const getUploadEndpoint = () => {
const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
const endpoint = process.env.NEXT_PUBLIC_ENTE_UPLOAD_ENDPOINT;
if (isDevDeployment() && endpoint) {
return endpoint;
}

View file

@ -26,3 +26,11 @@ export function setLivePhotoInfoShownCount(count) {
export function getUserLocale(): Language {
return getData(LS_KEYS.LOCALE)?.value;
}
export function getLocalMapEnabled(): boolean {
return getData(LS_KEYS.MAP_ENABLED)?.value ?? false;
}
export function setLocalMapEnabled(value: boolean) {
setData(LS_KEYS.MAP_ENABLED, { value });
}

View file

@ -22,6 +22,7 @@ export enum LS_KEYS {
WAIT_TIME = 'waitTime',
API_ENDPOINT = 'apiEndpoint',
LOCALE = 'locale',
MAP_ENABLED = 'mapEnabled',
}
export const setData = (key: LS_KEYS, value: object) => {

View file

@ -9,6 +9,8 @@ import InfoOutlined from '@mui/icons-material/InfoRounded';
import { Trans } from 'react-i18next';
import { Subscription } from 'types/billing';
import { logoutUser } from 'services/userService';
import { Link } from '@mui/material';
import { OPEN_STREET_MAP_LINK } from 'components/Sidebar/EnableMap';
export const getDownloadAppMessage = (): DialogBoxAttributes => {
return {
title: t('DOWNLOAD_APP'),
@ -129,3 +131,36 @@ export const getSessionExpiredMessage = (): DialogBoxAttributes => ({
variant: 'accent',
},
});
export const getMapEnableConfirmationDialog = (
enableMapHelper
): DialogBoxAttributes => ({
title: t('ENABLE_MAPS'),
content: (
<Trans
i18nKey={'ENABLE_MAP_DESCRIPTION'}
components={{
a: <Link target="_blank" href={OPEN_STREET_MAP_LINK} />,
}}
/>
),
proceed: {
action: enableMapHelper,
text: t('ENABLE'),
variant: 'accent',
},
close: { text: t('CANCEL') },
});
export const getMapDisableConfirmationDialog = (
disableMapHelper
): DialogBoxAttributes => ({
title: t('DISABLE_MAPS'),
content: <Trans i18nKey={'DISABLE_MAP_DESCRIPTION'} />,
proceed: {
action: disableMapHelper,
text: t('DISABLE'),
variant: 'accent',
},
close: { text: t('CANCEL') },
});

879
yarn.lock

File diff suppressed because it is too large Load diff