mirror of
https://github.com/lux-tel/session-desktop.git
synced 2026-10-30 20:33:24 +00:00
334 lines
8.5 KiB
TypeScript
334 lines
8.5 KiB
TypeScript
import { CSSProperties, MouseEvent, MutableRefObject, useRef } from 'react';
|
|
|
|
import { isUndefined } from 'lodash';
|
|
import useUnmount from 'react-use/lib/useUnmount';
|
|
import styled from 'styled-components';
|
|
import { getAppDispatch } from '../../state/dispatch';
|
|
import { useDisableDrag } from '../../hooks/useDisableDrag';
|
|
import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
|
|
import { updateLightBoxOptions } from '../../state/ducks/modalDialog';
|
|
import * as MIME from '../../types/MIME';
|
|
import { GoogleChrome } from '../../util';
|
|
import { Flex } from '../basic/Flex';
|
|
import { SessionIconSize } from '../icon';
|
|
import { AriaLabels } from '../../util/hardcodedAriaLabels';
|
|
import { LUCIDE_ICONS_UNICODE } from '../icon/lucide';
|
|
import { SessionLucideIconButton } from '../icon/SessionIconButton';
|
|
import { useHotkey } from '../../hooks/useHotkey';
|
|
|
|
type Props = {
|
|
contentType: MIME.MIMEType | undefined;
|
|
objectURL: string;
|
|
caption?: string;
|
|
onNext?: () => void;
|
|
onPrevious?: () => void;
|
|
onSave?: () => void;
|
|
onClose?: () => void;
|
|
};
|
|
|
|
const CONTROLS_WIDTH = 50;
|
|
const CONTROLS_SPACING = 10;
|
|
|
|
const styles = {
|
|
container: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
position: 'fixed',
|
|
width: '100vw',
|
|
height: '100vh',
|
|
left: 0,
|
|
zIndex: 150, // modals are 100
|
|
right: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
backgroundColor: 'var(--lightbox-background-color)',
|
|
} as CSSProperties,
|
|
mainContainer: {
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
flexGrow: 1,
|
|
paddingTop: 40,
|
|
paddingLeft: 40,
|
|
paddingRight: 40,
|
|
paddingBottom: 0,
|
|
minHeight: 0,
|
|
overflow: 'hidden',
|
|
minWidth: 0,
|
|
} as CSSProperties,
|
|
objectContainer: {
|
|
position: 'relative',
|
|
flexGrow: 1,
|
|
display: 'inline-flex',
|
|
justifyContent: 'center',
|
|
} as CSSProperties,
|
|
objectParentContainer: {
|
|
flexGrow: 1,
|
|
textAlign: 'center' as const,
|
|
margin: 'auto',
|
|
},
|
|
object: {
|
|
flexGrow: 1,
|
|
flexShrink: 0,
|
|
maxWidth: '80vw',
|
|
maxHeight: '80vh',
|
|
objectFit: 'contain',
|
|
} as CSSProperties,
|
|
caption: {
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
textAlign: 'center',
|
|
color: 'black',
|
|
padding: '1em',
|
|
paddingLeft: '3em',
|
|
paddingRight: '3em',
|
|
backgroundColor: 'var(--lightbox-caption-background-color)',
|
|
} as CSSProperties,
|
|
controlsOffsetPlaceholder: {
|
|
width: CONTROLS_WIDTH,
|
|
marginRight: CONTROLS_SPACING,
|
|
flexShrink: 0,
|
|
},
|
|
controls: {
|
|
width: CONTROLS_WIDTH,
|
|
flexShrink: 0,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
marginLeft: CONTROLS_SPACING,
|
|
justifyContent: 'space-between',
|
|
} as CSSProperties,
|
|
navigationContainer: {
|
|
flexShrink: 0,
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
justifyContent: 'center',
|
|
padding: 10,
|
|
height: '70px', // force it so the buttons stick to the bottom
|
|
} as CSSProperties,
|
|
saveButton: {
|
|
marginTop: 10,
|
|
},
|
|
iconButtonPlaceholder: {
|
|
// Dimensions match `.iconButton`:
|
|
display: 'inline-block',
|
|
width: 30,
|
|
height: 30,
|
|
},
|
|
};
|
|
|
|
const StyledIconButton = styled.div`
|
|
.session-icon-button {
|
|
opacity: 0.4;
|
|
transition: opacity var(--default-duration);
|
|
|
|
&:hover {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
`;
|
|
|
|
interface IconButtonProps {
|
|
onClick?: () => void;
|
|
style?: CSSProperties;
|
|
unicode:
|
|
| LUCIDE_ICONS_UNICODE.CHEVRON_RIGHT
|
|
| LUCIDE_ICONS_UNICODE.CHEVRON_LEFT
|
|
| LUCIDE_ICONS_UNICODE.X
|
|
| LUCIDE_ICONS_UNICODE.ARROW_DOWN_TO_LINE;
|
|
}
|
|
|
|
const IconButton = ({ onClick, unicode }: IconButtonProps) => {
|
|
const clickHandler = (): void => {
|
|
if (!onClick) {
|
|
return;
|
|
}
|
|
onClick();
|
|
};
|
|
|
|
// default to huge, only download is bigger
|
|
let iconSize: SessionIconSize = 'huge';
|
|
switch (unicode) {
|
|
case LUCIDE_ICONS_UNICODE.ARROW_DOWN_TO_LINE:
|
|
iconSize = 'huge2';
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return (
|
|
<StyledIconButton>
|
|
<SessionLucideIconButton
|
|
unicode={unicode}
|
|
iconSize={iconSize}
|
|
// the lightbox has a dark background
|
|
iconColor="var(--lightbox-icon-stroke-color)"
|
|
onClick={clickHandler}
|
|
/>
|
|
</StyledIconButton>
|
|
);
|
|
};
|
|
|
|
const IconButtonPlaceholder = () => <div style={styles.iconButtonPlaceholder} />;
|
|
|
|
const LightboxObject = ({
|
|
objectURL,
|
|
contentType,
|
|
renderedRef,
|
|
onObjectClick,
|
|
}: {
|
|
objectURL: string;
|
|
contentType: MIME.MIMEType;
|
|
renderedRef: MutableRefObject<any>;
|
|
onObjectClick: (event: any) => any;
|
|
}) => {
|
|
const { urlToLoad } = useEncryptedFileFetch(objectURL, contentType, false);
|
|
|
|
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
|
|
|
|
// auto play video on showing a video attachment
|
|
useUnmount(() => {
|
|
if (!renderedRef?.current) {
|
|
return;
|
|
}
|
|
renderedRef.current.pause.pause();
|
|
});
|
|
const disableDrag = useDisableDrag();
|
|
|
|
if (isImageTypeSupported) {
|
|
return (
|
|
<img
|
|
style={styles.object as any}
|
|
onDragStart={disableDrag}
|
|
src={urlToLoad}
|
|
alt={AriaLabels.imageSentInConversation}
|
|
ref={renderedRef}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);
|
|
if (isVideoTypeSupported) {
|
|
if (urlToLoad) {
|
|
if (renderedRef?.current?.paused) {
|
|
void renderedRef?.current?.play();
|
|
}
|
|
}
|
|
|
|
return (
|
|
<video
|
|
role="button"
|
|
ref={renderedRef}
|
|
controls={true}
|
|
style={styles.object as any}
|
|
key={urlToLoad}
|
|
>
|
|
<source src={urlToLoad} />
|
|
</video>
|
|
);
|
|
}
|
|
|
|
const isUnsupportedImageType = !isImageTypeSupported && MIME.isImage(contentType);
|
|
const isUnsupportedVideoType = !isVideoTypeSupported && MIME.isVideo(contentType);
|
|
if (isUnsupportedImageType || isUnsupportedVideoType) {
|
|
return (
|
|
<SessionLucideIconButton
|
|
unicode={
|
|
isUnsupportedVideoType ? LUCIDE_ICONS_UNICODE.CLAPERBOARD : LUCIDE_ICONS_UNICODE.IMAGE
|
|
}
|
|
iconSize="huge2"
|
|
onClick={onObjectClick}
|
|
iconColor="var(--lightbox-icon-stroke-color)"
|
|
/>
|
|
);
|
|
}
|
|
|
|
window.log.info('Lightbox: Unexpected content type', { contentType });
|
|
|
|
return (
|
|
<SessionLucideIconButton
|
|
unicode={LUCIDE_ICONS_UNICODE.FILE}
|
|
iconSize="huge2"
|
|
onClick={onObjectClick}
|
|
iconColor="var(--lightbox-icon-stroke-color)"
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const Lightbox = (props: Props) => {
|
|
const renderedRef = useRef<any>(null);
|
|
const dispatch = getAppDispatch();
|
|
const { caption, contentType, objectURL, onNext, onPrevious, onSave, onClose } = props;
|
|
|
|
const onObjectClick = (event: any) => {
|
|
event.stopPropagation();
|
|
dispatch(updateLightBoxOptions(null));
|
|
};
|
|
|
|
useHotkey('Escape', e => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
dispatch(updateLightBoxOptions(null));
|
|
});
|
|
|
|
const handleClose = () => {
|
|
if (onClose) {
|
|
onClose();
|
|
}
|
|
dispatch(updateLightBoxOptions(null));
|
|
};
|
|
|
|
const onContainerClick = (event: MouseEvent<HTMLDivElement>) => {
|
|
if (renderedRef && event.target === renderedRef.current) {
|
|
return;
|
|
}
|
|
handleClose();
|
|
};
|
|
|
|
return (
|
|
<div style={styles.container} role="dialog" onClick={onContainerClick}>
|
|
<div style={styles.mainContainer}>
|
|
<div style={styles.controlsOffsetPlaceholder} />
|
|
<div style={styles.objectParentContainer} role="button">
|
|
<div style={styles.objectContainer}>
|
|
{!isUndefined(contentType) ? (
|
|
<LightboxObject
|
|
objectURL={objectURL}
|
|
contentType={contentType}
|
|
renderedRef={renderedRef}
|
|
onObjectClick={onObjectClick}
|
|
/>
|
|
) : null}
|
|
{caption ? <div style={styles.caption}>{caption}</div> : null}
|
|
</div>
|
|
</div>
|
|
<div style={styles.controls}>
|
|
<Flex $container={true}>
|
|
<IconButton unicode={LUCIDE_ICONS_UNICODE.X} onClick={handleClose} />
|
|
</Flex>
|
|
|
|
{onSave ? (
|
|
<IconButton
|
|
unicode={LUCIDE_ICONS_UNICODE.ARROW_DOWN_TO_LINE}
|
|
onClick={onSave}
|
|
style={styles.saveButton}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
<div style={styles.navigationContainer as any}>
|
|
{onPrevious ? (
|
|
<IconButton unicode={LUCIDE_ICONS_UNICODE.CHEVRON_LEFT} onClick={onPrevious} />
|
|
) : (
|
|
<IconButtonPlaceholder />
|
|
)}
|
|
{onNext ? (
|
|
<IconButton unicode={LUCIDE_ICONS_UNICODE.CHEVRON_RIGHT} onClick={onNext} />
|
|
) : (
|
|
<IconButtonPlaceholder />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|