Merge signal-master.
commit
654b0dac84
@ -1,16 +1,43 @@
|
||||
const addUnhandledErrorHandler = require('electron-unhandled');
|
||||
const electron = require('electron');
|
||||
|
||||
const Errors = require('../js/modules/types/errors');
|
||||
|
||||
// addHandler :: Unit -> Unit
|
||||
const { app, dialog, clipboard } = electron;
|
||||
|
||||
// We're using hard-coded strings in this file because it needs to be ready
|
||||
// to report errors before we do anything in the app. Also, we expect users to directly
|
||||
// paste this text into search engines to find the bugs on GitHub.
|
||||
|
||||
function handleError(prefix, error) {
|
||||
console.error(`${prefix}:`, Errors.toLogFormat(error));
|
||||
|
||||
if (app.isReady()) {
|
||||
// title field is not shown on macOS, so we don't use it
|
||||
const buttonIndex = dialog.showMessageBox({
|
||||
buttons: ['OK', 'Copy error'],
|
||||
defaultId: 0,
|
||||
detail: error.stack,
|
||||
message: prefix,
|
||||
noLink: true,
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
if (buttonIndex === 1) {
|
||||
clipboard.writeText(`${prefix}\n${error.stack}`);
|
||||
}
|
||||
} else {
|
||||
dialog.showErrorBox(prefix, error.stack);
|
||||
}
|
||||
|
||||
app.quit();
|
||||
}
|
||||
|
||||
exports.addHandler = () => {
|
||||
addUnhandledErrorHandler({
|
||||
logger: error => {
|
||||
console.error(
|
||||
'Uncaught error or unhandled promise rejection:',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
},
|
||||
showDialog: false,
|
||||
process.on('uncaughtException', error => {
|
||||
handleError('Unhandled Error', error);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', error => {
|
||||
handleError('Unhandled Promise Rejection', error);
|
||||
});
|
||||
};
|
||||
|
Binary file not shown.
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.3 (67297) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>caption-shadow-24</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<rect id="path-1" x="0" y="3" width="18" height="2"></rect>
|
||||
<filter x="-19.4%" y="-125.0%" width="138.9%" height="450.0%" filterUnits="objectBoundingBox" id="filter-2">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<rect id="path-3" x="0" y="0" width="18" height="2"></rect>
|
||||
<filter x="-19.4%" y="-125.0%" width="138.9%" height="450.0%" filterUnits="objectBoundingBox" id="filter-4">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<rect id="path-5" x="0" y="6" width="12" height="2"></rect>
|
||||
<filter x="-29.2%" y="-125.0%" width="158.3%" height="450.0%" filterUnits="objectBoundingBox" id="filter-6">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<g id="caption-shadow-24" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="caption-24" transform="translate(3.000000, 8.000000)">
|
||||
<g id="Rectangle">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
|
||||
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
|
||||
</g>
|
||||
<g id="Rectangle">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-3"></use>
|
||||
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-3"></use>
|
||||
</g>
|
||||
<g id="Rectangle">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-6)" xlink:href="#path-5"></use>
|
||||
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-5"></use>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
@ -1,65 +0,0 @@
|
||||
const is = require('@sindresorhus/is');
|
||||
|
||||
const Errors = require('./types/errors');
|
||||
const Settings = require('./settings');
|
||||
|
||||
exports.syncReadReceiptConfiguration = async ({
|
||||
ourNumber,
|
||||
deviceId,
|
||||
sendRequestConfigurationSyncMessage,
|
||||
storage,
|
||||
prepareForSend,
|
||||
}) => {
|
||||
if (!is.string(deviceId)) {
|
||||
throw new TypeError('deviceId is required');
|
||||
}
|
||||
if (!is.function(sendRequestConfigurationSyncMessage)) {
|
||||
throw new TypeError('sendRequestConfigurationSyncMessage is required');
|
||||
}
|
||||
if (!is.function(prepareForSend)) {
|
||||
throw new TypeError('prepareForSend is required');
|
||||
}
|
||||
|
||||
if (!is.string(ourNumber)) {
|
||||
throw new TypeError('ourNumber is required');
|
||||
}
|
||||
|
||||
if (!is.object(storage)) {
|
||||
throw new TypeError('storage is required');
|
||||
}
|
||||
|
||||
const isPrimaryDevice = deviceId === '1';
|
||||
if (isPrimaryDevice) {
|
||||
return {
|
||||
status: 'skipped',
|
||||
reason: 'isPrimaryDevice',
|
||||
};
|
||||
}
|
||||
|
||||
const settingName = Settings.READ_RECEIPT_CONFIGURATION_SYNC;
|
||||
const hasPreviouslySynced = Boolean(storage.get(settingName));
|
||||
if (hasPreviouslySynced) {
|
||||
return {
|
||||
status: 'skipped',
|
||||
reason: 'hasPreviouslySynced',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { wrap, sendOptions } = prepareForSend(ourNumber, {
|
||||
syncMessage: true,
|
||||
});
|
||||
await wrap(sendRequestConfigurationSyncMessage(sendOptions));
|
||||
storage.put(settingName, true);
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
reason: 'failedToSendSyncMessage',
|
||||
error: Errors.toLogFormat(error),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'complete',
|
||||
};
|
||||
};
|
@ -1,44 +1,70 @@
|
||||
```js
|
||||
const noop = () => {};
|
||||
|
||||
const messages = [
|
||||
const mediaItems = [
|
||||
{
|
||||
objectURL: 'https://placekitten.com/799/600',
|
||||
attachments: [{ contentType: 'image/jpeg' }],
|
||||
contentType: 'image/jpeg',
|
||||
message: { id: 1 },
|
||||
attachment: {
|
||||
contentType: 'image/jpeg',
|
||||
caption:
|
||||
"This is a really long caption. Because the user had a lot to say. You know, it's very important to provide full context when sending an image. You don't want to make the wrong impression.",
|
||||
},
|
||||
},
|
||||
{
|
||||
objectURL: 'https://placekitten.com/900/600',
|
||||
attachments: [{ contentType: 'image/jpeg' }],
|
||||
contentType: 'image/jpeg',
|
||||
message: { id: 2 },
|
||||
attachment: { contentType: 'image/jpeg' },
|
||||
},
|
||||
// Unsupported image type
|
||||
{
|
||||
objectURL: 'foo.tif',
|
||||
attachments: [{ contentType: 'image/tiff' }],
|
||||
contentType: 'image/tiff',
|
||||
message: { id: 3 },
|
||||
attachment: { contentType: 'image/tiff' },
|
||||
},
|
||||
// Video
|
||||
{
|
||||
objectURL: util.mp4ObjectUrl,
|
||||
attachments: [{ contentType: 'video/mp4' }],
|
||||
contentType: 'video/mp4',
|
||||
message: { id: 4 },
|
||||
attachment: { contentType: 'video/mp4' },
|
||||
},
|
||||
{
|
||||
objectURL: util.mp4ObjectUrlV2,
|
||||
contentType: 'video/mp4',
|
||||
message: { id: 5 },
|
||||
attachment: { contentType: 'video/mp4' },
|
||||
},
|
||||
{
|
||||
objectURL: 'https://placekitten.com/980/800',
|
||||
attachments: [{ contentType: 'image/jpeg' }],
|
||||
contentType: 'image/jpeg',
|
||||
message: { id: 6 },
|
||||
attachment: { contentType: 'image/jpeg' },
|
||||
},
|
||||
{
|
||||
objectURL: 'https://placekitten.com/656/540',
|
||||
attachments: [{ contentType: 'image/jpeg' }],
|
||||
contentType: 'image/jpeg',
|
||||
message: { id: 7 },
|
||||
attachment: { contentType: 'image/jpeg' },
|
||||
},
|
||||
{
|
||||
objectURL: 'https://placekitten.com/762/400',
|
||||
attachments: [{ contentType: 'image/jpeg' }],
|
||||
contentType: 'image/jpeg',
|
||||
message: { id: 8 },
|
||||
attachment: { contentType: 'image/jpeg' },
|
||||
},
|
||||
{
|
||||
objectURL: 'https://placekitten.com/920/620',
|
||||
attachments: [{ contentType: 'image/jpeg' }],
|
||||
contentType: 'image/jpeg',
|
||||
message: { id: 9 },
|
||||
attachment: { contentType: 'image/jpeg' },
|
||||
},
|
||||
];
|
||||
|
||||
<div style={{ position: 'relative', width: '100%', height: 500 }}>
|
||||
<LightboxGallery messages={messages} onSave={noop} i18n={util.i18n} />
|
||||
<LightboxGallery media={mediaItems} onSave={noop} i18n={util.i18n} />
|
||||
</div>;
|
||||
```
|
||||
|
@ -0,0 +1,122 @@
|
||||
### Various sizes
|
||||
|
||||
```jsx
|
||||
<Image height='200' width='199' url={util.pngObjectUrl} />
|
||||
<Image height='149' width='149' url={util.pngObjectUrl} />
|
||||
<Image height='99' width='99' url={util.pngObjectUrl} />
|
||||
```
|
||||
|
||||
### Various curved corners
|
||||
|
||||
```jsx
|
||||
<Image height='149' width='149' curveTopLeft url={util.pngObjectUrl} />
|
||||
<Image height='149' width='149' curveTopRight url={util.pngObjectUrl} />
|
||||
<Image height='149' width='149' curveBottomLeft url={util.pngObjectUrl} />
|
||||
<Image height='149' width='149' curveBottomRight url={util.pngObjectUrl} />
|
||||
```
|
||||
|
||||
### With bottom overlay
|
||||
|
||||
```jsx
|
||||
<Image height='149' width='149' bottomOverlay url={util.pngObjectUrl} />
|
||||
<Image height='149' width='149' bottomOverlay curveBottomRight url={util.pngObjectUrl} />
|
||||
<Image height='149' width='149' bottomOverlay curveBottomLeft url={util.pngObjectUrl} />
|
||||
```
|
||||
|
||||
### With play icon
|
||||
|
||||
```jsx
|
||||
<Image height='200' width='199' playIconOverlay url={util.pngObjectUrl} />
|
||||
<Image height='149' width='149' playIconOverlay url={util.pngObjectUrl} />
|
||||
<Image height='99' width='99' playIconOverlay url={util.pngObjectUrl} />
|
||||
```
|
||||
|
||||
### With dark overlay and text
|
||||
|
||||
```jsx
|
||||
<div>
|
||||
<div>
|
||||
<Image height="200" width="199" darkOverlay url={util.pngObjectUrl} />
|
||||
<Image height="149" width="149" darkOverlay url={util.pngObjectUrl} />
|
||||
<Image height="99" width="99" darkOverlay url={util.pngObjectUrl} />
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<Image
|
||||
height="200"
|
||||
width="199"
|
||||
darkOverlay
|
||||
overlayText="+3"
|
||||
url={util.pngObjectUrl}
|
||||
/>
|
||||
<Image
|
||||
height="149"
|
||||
width="149"
|
||||
darkOverlay
|
||||
overlayText="+3"
|
||||
url={util.pngObjectUrl}
|
||||
/>
|
||||
<Image
|
||||
height="99"
|
||||
width="99"
|
||||
darkOverlay
|
||||
overlayText="+3"
|
||||
url={util.pngObjectUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### With caption
|
||||
|
||||
```jsx
|
||||
<div>
|
||||
<div>
|
||||
<Image
|
||||
height="200"
|
||||
width="199"
|
||||
attachment={{ caption: 'dogs playing' }}
|
||||
url={util.pngObjectUrl}
|
||||
/>
|
||||
<Image
|
||||
height="149"
|
||||
width="149"
|
||||
attachment={{ caption: 'dogs playing' }}
|
||||
url={util.pngObjectUrl}
|
||||
/>
|
||||
<Image
|
||||
height="99"
|
||||
width="99"
|
||||
attachment={{ caption: 'dogs playing' }}
|
||||
url={util.pngObjectUrl}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<Image
|
||||
height="200"
|
||||
width="199"
|
||||
attachment={{ caption: 'dogs playing' }}
|
||||
darkOverlay
|
||||
overlayText="+3"
|
||||
url={util.pngObjectUrl}
|
||||
/>
|
||||
<Image
|
||||
height="149"
|
||||
width="149"
|
||||
attachment={{ caption: 'dogs playing' }}
|
||||
darkOverlay
|
||||
overlayText="+3"
|
||||
url={util.pngObjectUrl}
|
||||
/>
|
||||
<Image
|
||||
height="99"
|
||||
width="99"
|
||||
attachment={{ caption: 'dogs playing' }}
|
||||
darkOverlay
|
||||
overlayText="+3"
|
||||
url={util.pngObjectUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
@ -0,0 +1,119 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { AttachmentType } from './types';
|
||||
|
||||
interface Props {
|
||||
alt: string;
|
||||
attachment: AttachmentType;
|
||||
url: string;
|
||||
|
||||
height?: number;
|
||||
width?: number;
|
||||
|
||||
overlayText?: string;
|
||||
|
||||
bottomOverlay?: boolean;
|
||||
curveBottomLeft?: boolean;
|
||||
curveBottomRight?: boolean;
|
||||
curveTopLeft?: boolean;
|
||||
curveTopRight?: boolean;
|
||||
darkOverlay?: boolean;
|
||||
playIconOverlay?: boolean;
|
||||
|
||||
i18n: Localizer;
|
||||
onClick?: (attachment: AttachmentType) => void;
|
||||
onError?: () => void;
|
||||
}
|
||||
|
||||
export class Image extends React.Component<Props> {
|
||||
public render() {
|
||||
const {
|
||||
alt,
|
||||
attachment,
|
||||
bottomOverlay,
|
||||
curveBottomLeft,
|
||||
curveBottomRight,
|
||||
curveTopLeft,
|
||||
curveTopRight,
|
||||
darkOverlay,
|
||||
height,
|
||||
i18n,
|
||||
onClick,
|
||||
onError,
|
||||
overlayText,
|
||||
playIconOverlay,
|
||||
url,
|
||||
width,
|
||||
} = this.props;
|
||||
|
||||
const { caption } = attachment || { caption: null };
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (onClick) {
|
||||
onClick(attachment);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
className={classNames(
|
||||
'module-image',
|
||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||
curveTopLeft ? 'module-image--curved-top-left' : null,
|
||||
curveTopRight ? 'module-image--curved-top-right' : null
|
||||
)}
|
||||
>
|
||||
<img
|
||||
onError={onError}
|
||||
className="module-image__image"
|
||||
alt={alt}
|
||||
height={height}
|
||||
width={width}
|
||||
src={url}
|
||||
/>
|
||||
{caption ? (
|
||||
<img
|
||||
className="module-image__caption-icon"
|
||||
src="images/caption-shadow.svg"
|
||||
alt={i18n('imageCaptionIconAlt')}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames(
|
||||
'module-image__border-overlay',
|
||||
curveTopLeft ? 'module-image--curved-top-left' : null,
|
||||
curveTopRight ? 'module-image--curved-top-right' : null,
|
||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||
darkOverlay ? 'module-image__border-overlay--dark' : null
|
||||
)}
|
||||
/>
|
||||
{bottomOverlay ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-image__bottom-overlay',
|
||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||
curveBottomRight ? 'module-image--curved-bottom-right' : null
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
{playIconOverlay ? (
|
||||
<div className="module-image__play-overlay__circle">
|
||||
<div className="module-image__play-overlay__icon" />
|
||||
</div>
|
||||
) : null}
|
||||
{overlayText ? (
|
||||
<div
|
||||
className="module-image__text-container"
|
||||
style={{ lineHeight: `${height}px` }}
|
||||
>
|
||||
{overlayText}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,354 @@
|
||||
### One image
|
||||
|
||||
```jsx
|
||||
const attachments = [
|
||||
{
|
||||
url: util.gifObjectUrl,
|
||||
contentType: 'image/gif',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
];
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<ImageGrid attachments={attachments} i18n={util.i18n} />
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<ImageGrid
|
||||
withContentAbove
|
||||
withContentBelow
|
||||
attachments={attachments}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
```
|
||||
|
||||
### One image, various aspect ratios
|
||||
|
||||
```jsx
|
||||
<div>
|
||||
<ImageGrid
|
||||
attachments={[
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 800,
|
||||
height: 1200,
|
||||
},
|
||||
]}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<hr />
|
||||
<ImageGrid
|
||||
attachments={[
|
||||
{
|
||||
url: util.gifObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
]}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<hr />
|
||||
<ImageGrid
|
||||
attachments={[
|
||||
{
|
||||
url: util.landscapeObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 4496,
|
||||
height: 3000,
|
||||
},
|
||||
]}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<hr />
|
||||
<ImageGrid
|
||||
attachments={[
|
||||
{
|
||||
url: util.landscapeGreenObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 1000,
|
||||
height: 50,
|
||||
},
|
||||
]}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<hr />
|
||||
<ImageGrid
|
||||
attachments={[
|
||||
{
|
||||
url: util.landscapePurpleObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 200,
|
||||
height: 50,
|
||||
},
|
||||
]}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<hr />
|
||||
<ImageGrid
|
||||
attachments={[
|
||||
{
|
||||
url: util.portraitYellowObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 20,
|
||||
height: 200,
|
||||
},
|
||||
]}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<hr />
|
||||
<ImageGrid
|
||||
attachments={[
|
||||
{
|
||||
url: util.landscapeRedObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 300,
|
||||
height: 1,
|
||||
},
|
||||
]}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<hr />
|
||||
<ImageGrid
|
||||
attachments={[
|
||||
{
|
||||
url: util.portraitTealObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 50,
|
||||
height: 1000,
|
||||
},
|
||||
]}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Two images
|
||||
|
||||
```jsx
|
||||
const attachments = [
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
];
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<ImageGrid attachments={attachments} i18n={util.i18n} />
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<ImageGrid
|
||||
withContentAbove
|
||||
withContentBelow
|
||||
attachments={attachments}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
```
|
||||
|
||||
### Three images
|
||||
|
||||
```jsx
|
||||
const attachments = [
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
];
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<ImageGrid attachments={attachments} i18n={util.i18n} />
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<ImageGrid
|
||||
withContentAbove
|
||||
withContentBelow
|
||||
attachments={attachments}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
```
|
||||
|
||||
### Four images
|
||||
|
||||
```jsx
|
||||
const attachments = [
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
];
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<ImageGrid attachments={attachments} i18n={util.i18n} />
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<ImageGrid
|
||||
withContentAbove
|
||||
withContentBelow
|
||||
attachments={attachments}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
```
|
||||
|
||||
### Five images
|
||||
|
||||
```jsx
|
||||
const attachments = [
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
];
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<ImageGrid attachments={attachments} i18n={util.i18n} />
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<ImageGrid
|
||||
withContentAbove
|
||||
withContentBelow
|
||||
attachments={attachments}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
```
|
||||
|
||||
### Six images
|
||||
|
||||
```
|
||||
const attachments = [
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
];
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<ImageGrid attachments={attachments} i18n={util.i18n} />
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<ImageGrid withContentAbove withContentBelow attachments={attachments} i18n={util.i18n} />
|
||||
</div>
|
||||
</div>;
|
||||
```
|
@ -0,0 +1,416 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {
|
||||
isImageTypeSupported,
|
||||
isVideoTypeSupported,
|
||||
} from '../../util/GoogleChrome';
|
||||
import { AttachmentType } from './types';
|
||||
import { Image } from './Image';
|
||||
import { Localizer } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
attachments: Array<AttachmentType>;
|
||||
withContentAbove: boolean;
|
||||
withContentBelow: boolean;
|
||||
bottomOverlay?: boolean;
|
||||
|
||||
i18n: Localizer;
|
||||
|
||||
onError: () => void;
|
||||
onClickAttachment?: (attachment: AttachmentType) => void;
|
||||
}
|
||||
|
||||
const MAX_WIDTH = 300;
|
||||
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
||||
const MIN_WIDTH = 200;
|
||||
const MIN_HEIGHT = 25;
|
||||
|
||||
export class ImageGrid extends React.Component<Props> {
|
||||
// tslint:disable-next-line max-func-body-length */
|
||||
public render() {
|
||||
const {
|
||||
attachments,
|
||||
bottomOverlay,
|
||||
i18n,
|
||||
onError,
|
||||
onClickAttachment,
|
||||
withContentAbove,
|
||||
withContentBelow,
|
||||
} = this.props;
|
||||
|
||||
const curveTopLeft = !Boolean(withContentAbove);
|
||||
const curveTopRight = curveTopLeft;
|
||||
|
||||
const curveBottom = !Boolean(withContentBelow);
|
||||
const curveBottomLeft = curveBottom;
|
||||
const curveBottomRight = curveBottom;
|
||||
|
||||
if (!attachments || !attachments.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (attachments.length === 1) {
|
||||
const { height, width } = getImageDimensions(attachments[0]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-image-grid',
|
||||
'module-image-grid--one-image'
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
curveTopLeft={curveTopLeft}
|
||||
curveTopRight={curveTopRight}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
curveBottomRight={curveBottomRight}
|
||||
attachment={attachments[0]}
|
||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
height={height}
|
||||
width={width}
|
||||
url={getUrl(attachments[0])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (attachments.length === 2) {
|
||||
return (
|
||||
<div className="module-image-grid">
|
||||
<Image
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
attachment={attachments[0]}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
curveTopLeft={curveTopLeft}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
height={149}
|
||||
width={149}
|
||||
url={getUrl(attachments[0])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
alt={getAlt(attachments[1], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
curveTopRight={curveTopRight}
|
||||
curveBottomRight={curveBottomRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||
height={149}
|
||||
width={149}
|
||||
attachment={attachments[1]}
|
||||
url={getUrl(attachments[1])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (attachments.length === 3) {
|
||||
return (
|
||||
<div className="module-image-grid">
|
||||
<Image
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
curveTopLeft={curveTopLeft}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
attachment={attachments[0]}
|
||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
height={200}
|
||||
width={199}
|
||||
url={getUrl(attachments[0])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
<div className="module-image-grid__column">
|
||||
<Image
|
||||
alt={getAlt(attachments[1], i18n)}
|
||||
i18n={i18n}
|
||||
curveTopRight={curveTopRight}
|
||||
height={99}
|
||||
width={99}
|
||||
attachment={attachments[1]}
|
||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||
url={getUrl(attachments[1])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
alt={getAlt(attachments[2], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
curveBottomRight={curveBottomRight}
|
||||
height={99}
|
||||
width={99}
|
||||
attachment={attachments[2]}
|
||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||
url={getUrl(attachments[2])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (attachments.length === 4) {
|
||||
return (
|
||||
<div className="module-image-grid">
|
||||
<div className="module-image-grid__column">
|
||||
<div className="module-image-grid__row">
|
||||
<Image
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
curveTopLeft={curveTopLeft}
|
||||
attachment={attachments[0]}
|
||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
height={149}
|
||||
width={149}
|
||||
url={getUrl(attachments[0])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
alt={getAlt(attachments[1], i18n)}
|
||||
i18n={i18n}
|
||||
curveTopRight={curveTopRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||
height={149}
|
||||
width={149}
|
||||
attachment={attachments[1]}
|
||||
url={getUrl(attachments[1])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
<div className="module-image-grid__row">
|
||||
<Image
|
||||
alt={getAlt(attachments[2], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||
height={149}
|
||||
width={149}
|
||||
attachment={attachments[2]}
|
||||
url={getUrl(attachments[2])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
alt={getAlt(attachments[3], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
curveBottomRight={curveBottomRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[3])}
|
||||
height={149}
|
||||
width={149}
|
||||
attachment={attachments[3]}
|
||||
url={getUrl(attachments[3])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-image-grid">
|
||||
<div className="module-image-grid__column">
|
||||
<div className="module-image-grid__row">
|
||||
<Image
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
curveTopLeft={curveTopLeft}
|
||||
attachment={attachments[0]}
|
||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
height={149}
|
||||
width={149}
|
||||
url={getUrl(attachments[0])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
alt={getAlt(attachments[1], i18n)}
|
||||
i18n={i18n}
|
||||
curveTopRight={curveTopRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||
height={149}
|
||||
width={149}
|
||||
attachment={attachments[1]}
|
||||
url={getUrl(attachments[1])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
<div className="module-image-grid__row">
|
||||
<Image
|
||||
alt={getAlt(attachments[2], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||
height={99}
|
||||
width={99}
|
||||
attachment={attachments[2]}
|
||||
url={getUrl(attachments[2])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
alt={getAlt(attachments[3], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
playIconOverlay={isVideoAttachment(attachments[3])}
|
||||
height={99}
|
||||
width={98}
|
||||
attachment={attachments[3]}
|
||||
url={getUrl(attachments[3])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
<Image
|
||||
alt={getAlt(attachments[4], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
curveBottomRight={curveBottomRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[4])}
|
||||
height={99}
|
||||
width={99}
|
||||
darkOverlay={attachments.length > 5}
|
||||
overlayText={
|
||||
attachments.length > 5
|
||||
? `+${attachments.length - 5}`
|
||||
: undefined
|
||||
}
|
||||
attachment={attachments[4]}
|
||||
url={getUrl(attachments[4])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getUrl(attachment: AttachmentType) {
|
||||
if (attachment.screenshot) {
|
||||
return attachment.screenshot.url;
|
||||
}
|
||||
|
||||
return attachment.url;
|
||||
}
|
||||
|
||||
export function isImage(attachments?: Array<AttachmentType>) {
|
||||
return (
|
||||
attachments &&
|
||||
attachments[0] &&
|
||||
attachments[0].contentType &&
|
||||
isImageTypeSupported(attachments[0].contentType)
|
||||
);
|
||||
}
|
||||
|
||||
export function hasImage(attachments?: Array<AttachmentType>) {
|
||||
return attachments && attachments[0] && attachments[0].url;
|
||||
}
|
||||
|
||||
export function isVideo(attachments?: Array<AttachmentType>) {
|
||||
return attachments && isVideoAttachment(attachments[0]);
|
||||
}
|
||||
|
||||
export function isVideoAttachment(attachment?: AttachmentType) {
|
||||
return (
|
||||
attachment &&
|
||||
attachment.contentType &&
|
||||
isVideoTypeSupported(attachment.contentType)
|
||||
);
|
||||
}
|
||||
|
||||
export function hasVideoScreenshot(attachments?: Array<AttachmentType>) {
|
||||
const firstAttachment = attachments ? attachments[0] : null;
|
||||
|
||||
return (
|
||||
firstAttachment &&
|
||||
firstAttachment.screenshot &&
|
||||
firstAttachment.screenshot.url
|
||||
);
|
||||
}
|
||||
|
||||
type DimensionsType = {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
|
||||
function getImageDimensions(attachment: AttachmentType): DimensionsType {
|
||||
const { height, width } = attachment;
|
||||
if (!height || !width) {
|
||||
return {
|
||||
height: MIN_HEIGHT,
|
||||
width: MIN_WIDTH,
|
||||
};
|
||||
}
|
||||
|
||||
const aspectRatio = height / width;
|
||||
const targetWidth = Math.max(Math.min(MAX_WIDTH, width), MIN_WIDTH);
|
||||
const candidateHeight = Math.round(targetWidth * aspectRatio);
|
||||
|
||||
return {
|
||||
width: targetWidth,
|
||||
height: Math.max(Math.min(MAX_HEIGHT, candidateHeight), MIN_HEIGHT),
|
||||
};
|
||||
}
|
||||
|
||||
export function getGridDimensions(
|
||||
attachments?: Array<AttachmentType>
|
||||
): null | DimensionsType {
|
||||
if (!attachments || !attachments.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isImage(attachments) && !isVideo(attachments)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (attachments.length === 1) {
|
||||
return getImageDimensions(attachments[0]);
|
||||
}
|
||||
|
||||
if (attachments.length === 2) {
|
||||
return {
|
||||
height: 150,
|
||||
width: 300,
|
||||
};
|
||||
}
|
||||
|
||||
if (attachments.length === 4) {
|
||||
return {
|
||||
height: 300,
|
||||
width: 300,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
height: 200,
|
||||
width: 300,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAlt(attachment: AttachmentType, i18n: Localizer): string {
|
||||
return isVideoAttachment(attachment)
|
||||
? i18n('videoAttachmentAlt')
|
||||
: i18n('imageAttachmentAlt');
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,22 @@
|
||||
### Conversation List
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<TypingAnimation i18n={util.i18n} />
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### Dark background
|
||||
|
||||
Note: background color is 'steel'
|
||||
|
||||
```jsx
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#6b6b78',
|
||||
padding: '2em',
|
||||
}}
|
||||
>
|
||||
<TypingAnimation color="light" i18n={util.i18n} />
|
||||
</div>
|
||||
```
|
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
i18n: Localizer;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export class TypingAnimation extends React.Component<Props> {
|
||||
public render() {
|
||||
const { i18n, color } = this.props;
|
||||
|
||||
return (
|
||||
<div className="module-typing-animation" title={i18n('typingAlt')}>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-typing-animation__dot',
|
||||
'module-typing-animation__dot--first',
|
||||
color ? `module-typing-animation__dot--${color}` : null
|
||||
)}
|
||||
/>
|
||||
<div className="module-typing-animation__spacer" />
|
||||
<div
|
||||
className={classNames(
|
||||
'module-typing-animation__dot',
|
||||
'module-typing-animation__dot--second',
|
||||
color ? `module-typing-animation__dot--${color}` : null
|
||||
)}
|
||||
/>
|
||||
<div className="module-typing-animation__spacer" />
|
||||
<div
|
||||
className={classNames(
|
||||
'module-typing-animation__dot',
|
||||
'module-typing-animation__dot--third',
|
||||
color ? `module-typing-animation__dot--${color}` : null
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
### In message bubble
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<li>
|
||||
<TypingBubble conversationType="direct" i18n={util.i18n} />
|
||||
</li>
|
||||
<li>
|
||||
<TypingBubble color="teal" conversationType="direct" i18n={util.i18n} />
|
||||
</li>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### In message bubble, group conversation
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<li>
|
||||
<TypingBubble color="red" conversationType="group" i18n={util.i18n} />
|
||||
</li>
|
||||
<li>
|
||||
<TypingBubble
|
||||
color="purple"
|
||||
authorName="First Last"
|
||||
conversationType="group"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<TypingBubble
|
||||
avatarPath={util.gifObjectUrl}
|
||||
color="blue"
|
||||
conversationType="group"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
</util.ConversationContext>
|
||||
```
|
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { TypingAnimation } from './TypingAnimation';
|
||||
import { Avatar } from '../Avatar';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
avatarPath?: string;
|
||||
color: string;
|
||||
name: string;
|
||||
phoneNumber: string;
|
||||
profileName: string;
|
||||
conversationType: string;
|
||||
i18n: Localizer;
|
||||
}
|
||||
|
||||
export class TypingBubble extends React.Component<Props> {
|
||||
public renderAvatar() {
|
||||
const {
|
||||
avatarPath,
|
||||
color,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
conversationType,
|
||||
i18n,
|
||||
} = this.props;
|
||||
|
||||
if (conversationType !== 'group') {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-message__author-avatar">
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
size={36}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { i18n, color } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classNames('module-message', 'module-message--incoming')}>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__container',
|
||||
'module-message__container--incoming',
|
||||
`module-message__container--incoming-${color}`
|
||||
)}
|
||||
>
|
||||
<div className="module-message__typing-container">
|
||||
<TypingAnimation color="light" i18n={i18n} />
|
||||
</div>
|
||||
{this.renderAvatar()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,31 +1,33 @@
|
||||
```jsx
|
||||
const messages = [
|
||||
const mediaItems = [
|
||||
{
|
||||
id: '1',
|
||||
attachments: [
|
||||
{
|
||||
fileName: 'foo.json',
|
||||
contentType: 'application/json',
|
||||
size: 53313,
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
message: {
|
||||
id: '1',
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'foo.json',
|
||||
contentType: 'application/json',
|
||||
size: 53313,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
attachments: [
|
||||
{
|
||||
fileName: 'bar.txt',
|
||||
contentType: 'text/plain',
|
||||
size: 10323,
|
||||
},
|
||||
],
|
||||
index: 1,
|
||||
message: {
|
||||
id: '2',
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'bar.txt',
|
||||
contentType: 'text/plain',
|
||||
size: 10323,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
<AttachmentSection
|
||||
header="Today"
|
||||
type="documents"
|
||||
messages={messages}
|
||||
mediaItems={mediaItems}
|
||||
i18n={util.i18n}
|
||||
/>;
|
||||
```
|
||||
|
@ -1,108 +1,94 @@
|
||||
#### With image
|
||||
|
||||
```jsx
|
||||
const message = {
|
||||
id: '1',
|
||||
const mediaItem = {
|
||||
thumbnailObjectUrl: 'https://placekitten.com/76/67',
|
||||
attachments: [
|
||||
{
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
contentType: 'image/jpeg',
|
||||
attachment: {
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
},
|
||||
};
|
||||
<MediaGridItem i18n={util.i18n} message={message} />;
|
||||
<MediaGridItem i18n={util.i18n} mediaItem={mediaItem} />;
|
||||
```
|
||||
|
||||
#### With video
|
||||
|
||||
```jsx
|
||||
const message = {
|
||||
id: '1',
|
||||
const mediaItem = {
|
||||
thumbnailObjectUrl: 'https://placekitten.com/76/67',
|
||||
attachments: [
|
||||
{
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'video/mp4',
|
||||
},
|
||||
],
|
||||
contentType: 'video/mp4',
|
||||
attachment: {
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'video/mp4',
|
||||
},
|
||||
};
|
||||
<MediaGridItem i18n={util.i18n} message={message} />;
|
||||
<MediaGridItem i18n={util.i18n} mediaItem={mediaItem} />;
|
||||
```
|
||||
|
||||
#### Missing image
|
||||
|
||||
```jsx
|
||||
const message = {
|
||||
id: '1',
|
||||
attachments: [
|
||||
{
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
const mediaItem = {
|
||||
contentType: 'image/jpeg',
|
||||
attachment: {
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
},
|
||||
};
|
||||
<MediaGridItem i18n={util.i18n} message={message} />;
|
||||
<MediaGridItem i18n={util.i18n} mediaItem={mediaItem} />;
|
||||
```
|
||||
|
||||
#### Missing video
|
||||
|
||||
```jsx
|
||||
const message = {
|
||||
id: '1',
|
||||
attachments: [
|
||||
{
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'video/mp4',
|
||||
},
|
||||
],
|
||||
const mediaItem = {
|
||||
contentType: 'video/mp4',
|
||||
attachment: {
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'video/mp4',
|
||||
},
|
||||
};
|
||||
<MediaGridItem i18n={util.i18n} message={message} />;
|
||||
<MediaGridItem i18n={util.i18n} mediaItem={mediaItem} />;
|
||||
```
|
||||
|
||||
#### Image thumbnail failed to load
|
||||
|
||||
```jsx
|
||||
const message = {
|
||||
id: '1',
|
||||
const mediaItem = {
|
||||
thumbnailObjectUrl: 'nonexistent',
|
||||
attachments: [
|
||||
{
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
contentType: 'image/jpeg',
|
||||
attachment: {
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
},
|
||||
};
|
||||
<MediaGridItem i18n={util.i18n} message={message} />;
|
||||
<MediaGridItem i18n={util.i18n} mediaItem={mediaItem} />;
|
||||
```
|
||||
|
||||
#### Video thumbnail failed to load
|
||||
|
||||
```jsx
|
||||
const message = {
|
||||
id: '1',
|
||||
const mediaItem = {
|
||||
thumbnailObjectUrl: 'nonexistent',
|
||||
attachments: [
|
||||
{
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'video/mp4',
|
||||
},
|
||||
],
|
||||
contentType: 'video/mp4',
|
||||
attachment: {
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'video/mp4',
|
||||
},
|
||||
};
|
||||
<MediaGridItem i18n={util.i18n} message={message} />;
|
||||
<MediaGridItem i18n={util.i18n} mediaItem={mediaItem} />;
|
||||
```
|
||||
|
||||
#### Other contentType
|
||||
|
||||
```jsx
|
||||
const message = {
|
||||
id: '1',
|
||||
attachments: [
|
||||
{
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'application/json',
|
||||
},
|
||||
],
|
||||
const mediaItem = {
|
||||
contentType: 'application/json',
|
||||
attachment: {
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'application/json',
|
||||
},
|
||||
};
|
||||
<MediaGridItem i18n={util.i18n} message={message} />;
|
||||
<MediaGridItem i18n={util.i18n} mediaItem={mediaItem} />;
|
||||
```
|
||||
|
@ -0,0 +1,159 @@
|
||||
import moment from 'moment';
|
||||
import { compact, groupBy, sortBy } from 'lodash';
|
||||
|
||||
import { MediaItemType } from '../../LightboxGallery';
|
||||
|
||||
// import { missingCaseError } from '../../../util/missingCaseError';
|
||||
|
||||
type StaticSectionType = 'today' | 'yesterday' | 'thisWeek' | 'thisMonth';
|
||||
type YearMonthSectionType = 'yearMonth';
|
||||
|
||||
interface GenericSection<T> {
|
||||
type: T;
|
||||
mediaItems: Array<MediaItemType>;
|
||||
}
|
||||
type StaticSection = GenericSection<StaticSectionType>;
|
||||
type YearMonthSection = GenericSection<YearMonthSectionType> & {
|
||||
year: number;
|
||||
month: number;
|
||||
};
|
||||
export type Section = StaticSection | YearMonthSection;
|
||||
export const groupMediaItemsByDate = (
|
||||
timestamp: number,
|
||||
mediaItems: Array<MediaItemType>
|
||||
): Array<Section> => {
|
||||
const referenceDateTime = moment.utc(timestamp);
|
||||
|
||||
const sortedMediaItem = sortBy(mediaItems, mediaItem => {
|
||||
const { message } = mediaItem;
|
||||
|
||||
return -message.received_at;
|
||||
});
|
||||
const messagesWithSection = sortedMediaItem.map(
|
||||
withSection(referenceDateTime)
|
||||
);
|
||||
const groupedMediaItem = groupBy(messagesWithSection, 'type');
|
||||
const yearMonthMediaItem = Object.values(
|
||||
groupBy(groupedMediaItem.yearMonth, 'order')
|
||||
).reverse();
|
||||
|
||||
return compact([
|
||||
toSection(groupedMediaItem.today),
|
||||
toSection(groupedMediaItem.yesterday),
|
||||
toSection(groupedMediaItem.thisWeek),
|
||||
toSection(groupedMediaItem.thisMonth),
|
||||
...yearMonthMediaItem.map(toSection),
|
||||
]);
|
||||
};
|
||||
|
||||
const toSection = (
|
||||
messagesWithSection: Array<MediaItemWithSection> | undefined
|
||||
): Section | null => {
|
||||
if (!messagesWithSection || messagesWithSection.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstMediaItemWithSection: MediaItemWithSection =
|
||||
messagesWithSection[0];
|
||||
if (!firstMediaItemWithSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mediaItems = messagesWithSection.map(
|
||||
messageWithSection => messageWithSection.mediaItem
|
||||
);
|
||||
switch (firstMediaItemWithSection.type) {
|
||||
case 'today':
|
||||
case 'yesterday':
|
||||
case 'thisWeek':
|
||||
case 'thisMonth':
|
||||
return {
|
||||
type: firstMediaItemWithSection.type,
|
||||
mediaItems,
|
||||
};
|
||||
case 'yearMonth':
|
||||
return {
|
||||
type: firstMediaItemWithSection.type,
|
||||
year: firstMediaItemWithSection.year,
|
||||
month: firstMediaItemWithSection.month,
|
||||
mediaItems,
|
||||
};
|
||||
default:
|
||||
// NOTE: Investigate why we get the following error:
|
||||
// error TS2345: Argument of type 'any' is not assignable to parameter
|
||||
// of type 'never'.
|
||||
// return missingCaseError(firstMediaItemWithSection.type);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
interface GenericMediaItemWithSection<T> {
|
||||
order: number;
|
||||
type: T;
|
||||
mediaItem: MediaItemType;
|
||||
}
|
||||
type MediaItemWithStaticSection = GenericMediaItemWithSection<
|
||||
StaticSectionType
|
||||
>;
|
||||
type MediaItemWithYearMonthSection = GenericMediaItemWithSection<
|
||||
YearMonthSectionType
|
||||
> & {
|
||||
year: number;
|
||||
month: number;
|
||||
};
|
||||
type MediaItemWithSection =
|
||||
| MediaItemWithStaticSection
|
||||
| MediaItemWithYearMonthSection;
|
||||
|
||||
const withSection = (referenceDateTime: moment.Moment) => (
|
||||
mediaItem: MediaItemType
|
||||
): MediaItemWithSection => {
|
||||
const today = moment(referenceDateTime).startOf('day');
|
||||
const yesterday = moment(referenceDateTime)
|
||||
.subtract(1, 'day')
|
||||
.startOf('day');
|
||||
const thisWeek = moment(referenceDateTime).startOf('isoWeek');
|
||||
const thisMonth = moment(referenceDateTime).startOf('month');
|
||||
|
||||
const { message } = mediaItem;
|
||||
const mediaItemReceivedDate = moment.utc(message.received_at);
|
||||
if (mediaItemReceivedDate.isAfter(today)) {
|
||||
return {
|
||||
order: 0,
|
||||
type: 'today',
|
||||
mediaItem,
|
||||
};
|
||||
}
|
||||
if (mediaItemReceivedDate.isAfter(yesterday)) {
|
||||
return {
|
||||
order: 1,
|
||||
type: 'yesterday',
|
||||
mediaItem,
|
||||
};
|
||||
}
|
||||
if (mediaItemReceivedDate.isAfter(thisWeek)) {
|
||||
return {
|
||||
order: 2,
|
||||
type: 'thisWeek',
|
||||
mediaItem,
|
||||
};
|
||||
}
|
||||
if (mediaItemReceivedDate.isAfter(thisMonth)) {
|
||||
return {
|
||||
order: 3,
|
||||
type: 'thisMonth',
|
||||
mediaItem,
|
||||
};
|
||||
}
|
||||
|
||||
const month: number = mediaItemReceivedDate.month();
|
||||
const year: number = mediaItemReceivedDate.year();
|
||||
|
||||
return {
|
||||
order: year * 100 + month,
|
||||
type: 'yearMonth',
|
||||
month,
|
||||
year,
|
||||
mediaItem,
|
||||
};
|
||||
};
|
@ -1,150 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import { compact, groupBy, sortBy } from 'lodash';
|
||||
|
||||
import { Message } from './types/Message';
|
||||
// import { missingCaseError } from '../../../util/missingCaseError';
|
||||
|
||||
type StaticSectionType = 'today' | 'yesterday' | 'thisWeek' | 'thisMonth';
|
||||
type YearMonthSectionType = 'yearMonth';
|
||||
|
||||
interface GenericSection<T> {
|
||||
type: T;
|
||||
messages: Array<Message>;
|
||||
}
|
||||
type StaticSection = GenericSection<StaticSectionType>;
|
||||
type YearMonthSection = GenericSection<YearMonthSectionType> & {
|
||||
year: number;
|
||||
month: number;
|
||||
};
|
||||
export type Section = StaticSection | YearMonthSection;
|
||||
export const groupMessagesByDate = (
|
||||
timestamp: number,
|
||||
messages: Array<Message>
|
||||
): Array<Section> => {
|
||||
const referenceDateTime = moment.utc(timestamp);
|
||||
|
||||
const sortedMessages = sortBy(messages, message => -message.received_at);
|
||||
const messagesWithSection = sortedMessages.map(
|
||||
withSection(referenceDateTime)
|
||||
);
|
||||
const groupedMessages = groupBy(messagesWithSection, 'type');
|
||||
const yearMonthMessages = Object.values(
|
||||
groupBy(groupedMessages.yearMonth, 'order')
|
||||
).reverse();
|
||||
|
||||
return compact([
|
||||
toSection(groupedMessages.today),
|
||||
toSection(groupedMessages.yesterday),
|
||||
toSection(groupedMessages.thisWeek),
|
||||
toSection(groupedMessages.thisMonth),
|
||||
...yearMonthMessages.map(toSection),
|
||||
]);
|
||||
};
|
||||
|
||||
const toSection = (
|
||||
messagesWithSection: Array<MessageWithSection> | undefined
|
||||
): Section | null => {
|
||||
if (!messagesWithSection || messagesWithSection.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstMessageWithSection: MessageWithSection = messagesWithSection[0];
|
||||
if (!firstMessageWithSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const messages = messagesWithSection.map(
|
||||
messageWithSection => messageWithSection.message
|
||||
);
|
||||
switch (firstMessageWithSection.type) {
|
||||
case 'today':
|
||||
case 'yesterday':
|
||||
case 'thisWeek':
|
||||
case 'thisMonth':
|
||||
return {
|
||||
type: firstMessageWithSection.type,
|
||||
messages,
|
||||
};
|
||||
case 'yearMonth':
|
||||
return {
|
||||
type: firstMessageWithSection.type,
|
||||
year: firstMessageWithSection.year,
|
||||
month: firstMessageWithSection.month,
|
||||
messages,
|
||||
};
|
||||
default:
|
||||
// NOTE: Investigate why we get the following error:
|
||||
// error TS2345: Argument of type 'any' is not assignable to parameter
|
||||
// of type 'never'.
|
||||
// return missingCaseError(firstMessageWithSection.type);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
interface GenericMessageWithSection<T> {
|
||||
order: number;
|
||||
type: T;
|
||||
message: Message;
|
||||
}
|
||||
type MessageWithStaticSection = GenericMessageWithSection<StaticSectionType>;
|
||||
type MessageWithYearMonthSection = GenericMessageWithSection<
|
||||
YearMonthSectionType
|
||||
> & {
|
||||
year: number;
|
||||
month: number;
|
||||
};
|
||||
type MessageWithSection =
|
||||
| MessageWithStaticSection
|
||||
| MessageWithYearMonthSection;
|
||||
|
||||
const withSection = (referenceDateTime: moment.Moment) => (
|
||||
message: Message
|
||||
): MessageWithSection => {
|
||||
const today = moment(referenceDateTime).startOf('day');
|
||||
const yesterday = moment(referenceDateTime)
|
||||
.subtract(1, 'day')
|
||||
.startOf('day');
|
||||
const thisWeek = moment(referenceDateTime).startOf('isoWeek');
|
||||
const thisMonth = moment(referenceDateTime).startOf('month');
|
||||
|
||||
const messageReceivedDate = moment.utc(message.received_at);
|
||||
if (messageReceivedDate.isAfter(today)) {
|
||||
return {
|
||||
order: 0,
|
||||
type: 'today',
|
||||
message,
|
||||
};
|
||||
}
|
||||
if (messageReceivedDate.isAfter(yesterday)) {
|
||||
return {
|
||||
order: 1,
|
||||
type: 'yesterday',
|
||||
message,
|
||||
};
|
||||
}
|
||||
if (messageReceivedDate.isAfter(thisWeek)) {
|
||||
return {
|
||||
order: 2,
|
||||
type: 'thisWeek',
|
||||
message,
|
||||
};
|
||||
}
|
||||
if (messageReceivedDate.isAfter(thisMonth)) {
|
||||
return {
|
||||
order: 3,
|
||||
type: 'thisMonth',
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
const month: number = messageReceivedDate.month();
|
||||
const year: number = messageReceivedDate.year();
|
||||
|
||||
return {
|
||||
order: year * 100 + month,
|
||||
type: 'yearMonth',
|
||||
month,
|
||||
year,
|
||||
message,
|
||||
};
|
||||
};
|
@ -1 +0,0 @@
|
||||
export type AttachmentType = 'media' | 'documents';
|
@ -1,7 +1,8 @@
|
||||
import { AttachmentType } from './AttachmentType';
|
||||
import { AttachmentType } from '../../types';
|
||||
import { Message } from './Message';
|
||||
|
||||
export interface ItemClickEvent {
|
||||
message: Message;
|
||||
type: AttachmentType;
|
||||
attachment: AttachmentType;
|
||||
type: 'media' | 'documents';
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
import { MIMEType } from '../../../ts/types/MIME';
|
||||
|
||||
export interface AttachmentType {
|
||||
caption?: string;
|
||||
contentType: MIMEType;
|
||||
fileName: string;
|
||||
/** Not included in protobuf, needs to be pulled from flags */
|
||||
isVoiceMessage?: boolean;
|
||||
/** For messages not already on disk, this will be a data url */
|
||||
url: string;
|
||||
size?: number;
|
||||
fileSize?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
screenshot?: {
|
||||
height: number;
|
||||
width: number;
|
||||
url: string;
|
||||
contentType: MIMEType;
|
||||
};
|
||||
thumbnail?: {
|
||||
height: number;
|
||||
width: number;
|
||||
url: string;
|
||||
contentType: MIMEType;
|
||||
};
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue