2021-08-23 23:14:53 +00:00
|
|
|
// Copyright 2018-2021 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
import React, {
|
|
|
|
MouseEvent,
|
|
|
|
ReactNode,
|
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
useRef,
|
|
|
|
useState,
|
|
|
|
} from 'react';
|
2018-04-15 04:27:30 +00:00
|
|
|
import classNames from 'classnames';
|
2021-08-24 21:47:14 +00:00
|
|
|
import moment from 'moment';
|
2021-08-23 23:14:53 +00:00
|
|
|
import { createPortal } from 'react-dom';
|
2021-08-24 21:47:14 +00:00
|
|
|
import { noop } from 'lodash';
|
2018-04-25 22:15:57 +00:00
|
|
|
|
|
|
|
import * as GoogleChrome from '../util/GoogleChrome';
|
2021-08-23 23:14:53 +00:00
|
|
|
import { AttachmentType, isGIF } from '../types/Attachment';
|
|
|
|
import { Avatar, AvatarSize } from './Avatar';
|
|
|
|
import { ConversationType } from '../state/ducks/conversations';
|
|
|
|
import { IMAGE_PNG, isImage, isVideo } from '../types/MIME';
|
2019-01-14 21:49:58 +00:00
|
|
|
import { LocalizerType } from '../types/Util';
|
2021-08-23 23:14:53 +00:00
|
|
|
import { MediaItemType, MessageAttributesType } from '../types/MediaItem';
|
2021-08-24 21:47:14 +00:00
|
|
|
import { formatDuration } from '../util/formatDuration';
|
2018-05-22 19:31:43 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
export type PropsType = {
|
2021-08-06 00:17:05 +00:00
|
|
|
children?: ReactNode;
|
2018-04-15 03:27:03 +00:00
|
|
|
close: () => void;
|
2021-08-23 23:14:53 +00:00
|
|
|
getConversation?: (id: string) => ConversationType;
|
2019-01-14 21:49:58 +00:00
|
|
|
i18n: LocalizerType;
|
2021-08-24 21:47:14 +00:00
|
|
|
isViewOnce?: boolean;
|
2021-08-23 23:14:53 +00:00
|
|
|
media: Array<MediaItemType>;
|
|
|
|
onForward?: (messageId: string) => void;
|
|
|
|
onSave?: (options: {
|
|
|
|
attachment: AttachmentType;
|
|
|
|
message: MessageAttributesType;
|
|
|
|
index: number;
|
|
|
|
}) => void;
|
|
|
|
selectedIndex?: number;
|
2021-01-14 18:07:05 +00:00
|
|
|
};
|
2018-04-15 03:27:03 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
export function Lightbox({
|
|
|
|
children,
|
|
|
|
close,
|
|
|
|
getConversation,
|
|
|
|
media,
|
|
|
|
i18n,
|
2021-08-24 21:47:14 +00:00
|
|
|
isViewOnce = false,
|
2021-08-23 23:14:53 +00:00
|
|
|
onForward,
|
|
|
|
onSave,
|
2021-08-24 21:47:14 +00:00
|
|
|
selectedIndex: initialSelectedIndex = 0,
|
2021-08-23 23:14:53 +00:00
|
|
|
}: PropsType): JSX.Element | null {
|
|
|
|
const [root, setRoot] = React.useState<HTMLElement | undefined>();
|
|
|
|
const [selectedIndex, setSelectedIndex] = useState<number>(
|
2021-08-24 21:47:14 +00:00
|
|
|
initialSelectedIndex
|
2021-08-23 23:14:53 +00:00
|
|
|
);
|
2018-04-15 03:27:03 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
const [previousFocus, setPreviousFocus] = useState<HTMLElement | undefined>();
|
2021-08-24 21:47:14 +00:00
|
|
|
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
|
|
|
|
null
|
|
|
|
);
|
|
|
|
const [videoTime, setVideoTime] = useState<number | undefined>();
|
2021-08-23 23:14:53 +00:00
|
|
|
const [zoomed, setZoomed] = useState(false);
|
|
|
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
const focusRef = useRef<HTMLDivElement | null>(null);
|
2018-04-26 15:18:24 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
const restorePreviousFocus = useCallback(() => {
|
|
|
|
if (previousFocus && previousFocus.focus) {
|
|
|
|
previousFocus.focus();
|
2018-04-26 15:18:24 +00:00
|
|
|
}
|
2021-08-23 23:14:53 +00:00
|
|
|
}, [previousFocus]);
|
2018-04-26 15:18:24 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
const onPrevious = useCallback(() => {
|
|
|
|
setSelectedIndex(prevSelectedIndex => Math.max(prevSelectedIndex - 1, 0));
|
|
|
|
}, []);
|
2018-04-15 04:27:30 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
const onNext = useCallback(() => {
|
|
|
|
setSelectedIndex(prevSelectedIndex =>
|
|
|
|
Math.min(prevSelectedIndex + 1, media.length - 1)
|
|
|
|
);
|
|
|
|
}, [media]);
|
2019-11-07 21:36:16 +00:00
|
|
|
|
2021-08-24 21:47:14 +00:00
|
|
|
const onTimeUpdate = useCallback(() => {
|
|
|
|
if (!videoElement) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
setVideoTime(videoElement.currentTime);
|
|
|
|
}, [setVideoTime, videoElement]);
|
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
const handleSave = () => {
|
|
|
|
const mediaItem = media[selectedIndex];
|
|
|
|
const { attachment, message, index } = mediaItem;
|
2019-10-03 19:03:46 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
onSave?.({ attachment, message, index });
|
|
|
|
};
|
2018-07-18 23:02:10 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
const handleForward = () => {
|
|
|
|
close();
|
|
|
|
const mediaItem = media[selectedIndex];
|
|
|
|
onForward?.(mediaItem.message.id);
|
|
|
|
};
|
2019-10-03 19:03:46 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
const onKeyDown = useCallback(
|
|
|
|
(event: KeyboardEvent) => {
|
|
|
|
switch (event.key) {
|
|
|
|
case 'Escape':
|
|
|
|
if (zoomed) {
|
|
|
|
setZoomed(false);
|
|
|
|
} else {
|
|
|
|
close();
|
|
|
|
}
|
2019-11-07 21:36:16 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
2018-04-15 04:50:18 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
break;
|
2019-11-07 21:36:16 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
case 'ArrowLeft':
|
|
|
|
if (onPrevious) {
|
|
|
|
onPrevious();
|
2019-10-03 19:03:46 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
}
|
|
|
|
break;
|
2019-10-03 19:03:46 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
case 'ArrowRight':
|
|
|
|
if (onNext) {
|
|
|
|
onNext();
|
2018-04-15 04:50:18 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
}
|
|
|
|
break;
|
2018-07-18 23:02:10 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[close, onNext, onPrevious, zoomed]
|
|
|
|
);
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
const stopPropagationAndClose = (event: MouseEvent<HTMLElement>) => {
|
|
|
|
event.stopPropagation();
|
|
|
|
close();
|
|
|
|
};
|
2019-10-03 19:03:46 +00:00
|
|
|
|
2021-08-24 21:47:14 +00:00
|
|
|
const playVideo = useCallback(() => {
|
|
|
|
if (!videoElement) {
|
2019-10-03 19:03:46 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-08-24 21:47:14 +00:00
|
|
|
if (videoElement.paused) {
|
|
|
|
videoElement.play();
|
2018-07-18 23:02:10 +00:00
|
|
|
} else {
|
2021-08-24 21:47:14 +00:00
|
|
|
videoElement.pause();
|
2018-07-18 23:02:10 +00:00
|
|
|
}
|
2021-08-24 21:47:14 +00:00
|
|
|
}, [videoElement]);
|
2018-04-25 22:15:57 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
useEffect(() => {
|
|
|
|
const div = document.createElement('div');
|
|
|
|
document.body.appendChild(div);
|
|
|
|
setRoot(div);
|
2018-04-25 22:15:57 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
return () => {
|
|
|
|
document.body.removeChild(div);
|
|
|
|
setRoot(undefined);
|
|
|
|
};
|
|
|
|
}, []);
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (!previousFocus) {
|
|
|
|
setPreviousFocus(document.activeElement as HTMLElement);
|
2018-05-08 01:20:39 +00:00
|
|
|
}
|
2021-08-23 23:14:53 +00:00
|
|
|
}, [previousFocus]);
|
2018-05-08 01:20:39 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
useEffect(() => {
|
|
|
|
return () => {
|
|
|
|
restorePreviousFocus();
|
|
|
|
};
|
|
|
|
}, [restorePreviousFocus]);
|
2018-05-22 19:31:43 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
useEffect(() => {
|
|
|
|
const useCapture = true;
|
|
|
|
document.addEventListener('keydown', onKeyDown, useCapture);
|
2020-11-03 00:47:46 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
return () => {
|
|
|
|
document.removeEventListener('keydown', onKeyDown, useCapture);
|
|
|
|
};
|
|
|
|
}, [onKeyDown]);
|
2018-04-24 20:12:11 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
useEffect(() => {
|
2021-08-24 21:47:14 +00:00
|
|
|
playVideo();
|
2018-04-15 05:48:21 +00:00
|
|
|
|
2021-08-24 21:47:14 +00:00
|
|
|
if (focusRef && focusRef.current) {
|
|
|
|
focusRef.current.focus();
|
|
|
|
}
|
2018-04-26 21:25:16 +00:00
|
|
|
|
2021-08-24 21:47:14 +00:00
|
|
|
if (videoElement && isViewOnce) {
|
|
|
|
videoElement.addEventListener('timeupdate', onTimeUpdate);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
videoElement.removeEventListener('timeupdate', onTimeUpdate);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return noop;
|
|
|
|
}, [isViewOnce, onTimeUpdate, playVideo, videoElement]);
|
2018-04-26 21:25:16 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
const { attachment, contentType, loop = false, objectURL, message } =
|
|
|
|
media[selectedIndex] || {};
|
|
|
|
const caption = attachment?.caption;
|
2019-11-07 21:36:16 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
let content: JSX.Element;
|
|
|
|
if (!contentType) {
|
|
|
|
content = <>{children}</>;
|
|
|
|
} else {
|
|
|
|
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
|
|
|
|
const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);
|
|
|
|
const isUnsupportedImageType =
|
|
|
|
!isImageTypeSupported && isImage(contentType);
|
|
|
|
const isUnsupportedVideoType =
|
|
|
|
!isVideoTypeSupported && isVideo(contentType);
|
2018-04-26 21:25:16 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
if (isImageTypeSupported) {
|
|
|
|
if (objectURL) {
|
|
|
|
content = (
|
|
|
|
<button
|
|
|
|
className="Lightbox__zoom-button"
|
|
|
|
onClick={() => setZoomed(!zoomed)}
|
|
|
|
type="button"
|
|
|
|
>
|
|
|
|
<img
|
|
|
|
alt={i18n('lightboxImageAlt')}
|
|
|
|
className="Lightbox__object"
|
|
|
|
onContextMenu={(event: MouseEvent<HTMLImageElement>) => {
|
|
|
|
// These are the only image types supported by Electron's NativeImage
|
|
|
|
if (
|
|
|
|
event &&
|
|
|
|
contentType !== IMAGE_PNG &&
|
|
|
|
!/image\/jpe?g/g.test(contentType)
|
|
|
|
) {
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
src={objectURL}
|
|
|
|
/>
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
content = (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('lightboxImageAlt')}
|
|
|
|
className={classNames({
|
|
|
|
Lightbox__object: true,
|
|
|
|
Lightbox__unsupported: true,
|
|
|
|
'Lightbox__unsupported--missing': true,
|
|
|
|
})}
|
|
|
|
onClick={stopPropagationAndClose}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else if (isVideoTypeSupported) {
|
2021-08-24 21:47:14 +00:00
|
|
|
const shouldLoop = loop || isGIF([attachment]) || isViewOnce;
|
2021-08-23 23:14:53 +00:00
|
|
|
content = (
|
|
|
|
<video
|
|
|
|
className="Lightbox__object"
|
|
|
|
controls={!shouldLoop}
|
|
|
|
key={objectURL}
|
|
|
|
loop={shouldLoop}
|
2021-08-24 21:47:14 +00:00
|
|
|
ref={setVideoElement}
|
2021-08-23 23:14:53 +00:00
|
|
|
>
|
|
|
|
<source src={objectURL} />
|
|
|
|
</video>
|
|
|
|
);
|
|
|
|
} else if (isUnsupportedImageType || isUnsupportedVideoType) {
|
|
|
|
content = (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('unsupportedAttachment')}
|
|
|
|
className={classNames({
|
|
|
|
Lightbox__object: true,
|
|
|
|
Lightbox__unsupported: true,
|
|
|
|
'Lightbox__unsupported--image': isUnsupportedImageType,
|
|
|
|
'Lightbox__unsupported--video': isUnsupportedVideoType,
|
|
|
|
})}
|
|
|
|
onClick={stopPropagationAndClose}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
window.log.info('Lightbox: Unexpected content type', { contentType });
|
2018-04-15 05:48:21 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
content = (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('unsupportedAttachment')}
|
|
|
|
className="Lightbox__object Lightbox__unsupported Lightbox__unsupported--file"
|
|
|
|
onClick={stopPropagationAndClose}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
);
|
2018-04-15 05:48:21 +00:00
|
|
|
}
|
2021-08-23 23:14:53 +00:00
|
|
|
}
|
2018-04-15 05:48:21 +00:00
|
|
|
|
2021-09-01 01:19:18 +00:00
|
|
|
const hasNext = !zoomed && selectedIndex < media.length - 1;
|
|
|
|
const hasPrevious = !zoomed && selectedIndex > 0;
|
2021-08-23 23:14:53 +00:00
|
|
|
|
|
|
|
return root
|
|
|
|
? createPortal(
|
|
|
|
<div
|
|
|
|
className="Lightbox Lightbox__container"
|
|
|
|
onClick={(event: MouseEvent<HTMLDivElement>) => {
|
|
|
|
if (containerRef && event.target !== containerRef.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
close();
|
|
|
|
}}
|
|
|
|
onKeyUp={(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
|
|
if (
|
|
|
|
(containerRef && event.target !== containerRef.current) ||
|
|
|
|
event.keyCode !== 27
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
close();
|
|
|
|
}}
|
|
|
|
ref={containerRef}
|
|
|
|
role="presentation"
|
|
|
|
>
|
|
|
|
<div
|
|
|
|
className="Lightbox__main-container"
|
|
|
|
tabIndex={-1}
|
|
|
|
ref={focusRef}
|
|
|
|
>
|
|
|
|
{!zoomed && (
|
|
|
|
<div className="Lightbox__header">
|
|
|
|
{getConversation ? (
|
|
|
|
<LightboxHeader
|
|
|
|
getConversation={getConversation}
|
|
|
|
i18n={i18n}
|
|
|
|
message={message}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<div />
|
|
|
|
)}
|
|
|
|
<div className="Lightbox__controls">
|
|
|
|
{onForward ? (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('forwardMessage')}
|
|
|
|
className="Lightbox__button Lightbox__button--forward"
|
|
|
|
onClick={handleForward}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
) : null}
|
|
|
|
{onSave ? (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('save')}
|
|
|
|
className="Lightbox__button Lightbox__button--save"
|
|
|
|
onClick={handleSave}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
) : null}
|
|
|
|
<button
|
|
|
|
aria-label={i18n('close')}
|
|
|
|
className="Lightbox__button Lightbox__button--close"
|
|
|
|
onClick={close}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
<div
|
|
|
|
className={classNames('Lightbox__object--container', {
|
|
|
|
'Lightbox__object--container--zoomed': zoomed,
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
{content}
|
|
|
|
</div>
|
|
|
|
{hasPrevious && (
|
|
|
|
<div className="Lightbox__nav-prev">
|
|
|
|
<button
|
|
|
|
aria-label={i18n('previous')}
|
|
|
|
className="Lightbox__button Lightbox__button--previous"
|
|
|
|
onClick={onPrevious}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{hasNext && (
|
|
|
|
<div className="Lightbox__nav-next">
|
|
|
|
<button
|
|
|
|
aria-label={i18n('next')}
|
|
|
|
className="Lightbox__button Lightbox__button--next"
|
|
|
|
onClick={onNext}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
{!zoomed && (
|
|
|
|
<div className="Lightbox__footer">
|
2021-08-24 21:47:14 +00:00
|
|
|
{isViewOnce && videoTime ? (
|
|
|
|
<div className="Lightbox__timestamp">
|
|
|
|
{formatDuration(videoTime)}
|
|
|
|
</div>
|
|
|
|
) : null}
|
2021-08-23 23:14:53 +00:00
|
|
|
{caption ? (
|
|
|
|
<div className="Lightbox__caption">{caption}</div>
|
|
|
|
) : null}
|
|
|
|
{media.length > 1 && (
|
|
|
|
<div className="Lightbox__thumbnails--container">
|
|
|
|
<div
|
|
|
|
className="Lightbox__thumbnails"
|
|
|
|
style={{
|
|
|
|
marginLeft:
|
|
|
|
0 - (selectedIndex * 64 + selectedIndex * 8 + 32),
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{media.map((item, index) => (
|
|
|
|
<button
|
|
|
|
className={classNames({
|
|
|
|
Lightbox__thumbnail: true,
|
|
|
|
'Lightbox__thumbnail--selected':
|
|
|
|
index === selectedIndex,
|
|
|
|
})}
|
|
|
|
key={item.thumbnailObjectUrl}
|
|
|
|
type="button"
|
|
|
|
onClick={() => setSelectedIndex(index)}
|
|
|
|
>
|
|
|
|
{item.thumbnailObjectUrl ? (
|
|
|
|
<img
|
|
|
|
alt={i18n('lightboxImageAlt')}
|
|
|
|
src={item.thumbnailObjectUrl}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<div className="Lightbox__thumbnail--unavailable" />
|
|
|
|
)}
|
|
|
|
</button>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>,
|
|
|
|
root
|
|
|
|
)
|
|
|
|
: null;
|
|
|
|
}
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
function LightboxHeader({
|
|
|
|
getConversation,
|
|
|
|
i18n,
|
|
|
|
message,
|
|
|
|
}: {
|
|
|
|
getConversation: (id: string) => ConversationType;
|
|
|
|
i18n: LocalizerType;
|
|
|
|
message: MessageAttributesType;
|
|
|
|
}): JSX.Element {
|
|
|
|
const conversation = getConversation(message.conversationId);
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
return (
|
|
|
|
<div className="Lightbox__header--container">
|
|
|
|
<div className="Lightbox__header--avatar">
|
|
|
|
<Avatar
|
|
|
|
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
|
|
|
avatarPath={conversation.avatarPath}
|
|
|
|
color={conversation.color}
|
|
|
|
conversationType={conversation.type}
|
|
|
|
i18n={i18n}
|
|
|
|
isMe={conversation.isMe}
|
|
|
|
name={conversation.name}
|
|
|
|
phoneNumber={conversation.e164}
|
|
|
|
profileName={conversation.profileName}
|
|
|
|
sharedGroupNames={conversation.sharedGroupNames}
|
|
|
|
size={AvatarSize.THIRTY_TWO}
|
|
|
|
title={conversation.title}
|
|
|
|
unblurredAvatarPath={conversation.unblurredAvatarPath}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div className="Lightbox__header--content">
|
|
|
|
<div className="Lightbox__header--name">{conversation.title}</div>
|
|
|
|
<div className="Lightbox__header--timestamp">
|
|
|
|
{moment(message.received_at_ms).format('L LT')}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
2018-04-15 03:27:03 +00:00
|
|
|
}
|