New staged attachments UI, multiple image attachments per message

This commit is contained in:
Scott Nonnenberg 2018-12-01 17:48:53 -08:00
parent e4babdaef0
commit 985b1d6aa6
22 changed files with 1550 additions and 648 deletions

View file

@ -0,0 +1,72 @@
## Image
```js
let caption = null;
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<CaptionEditor
url={util.gifObjectUrl}
attachment={{
contentType: 'image/jpeg',
}}
onChangeCaption={caption => console.log('onChangeCaption', caption)}
i18n={util.i18n}
/>
</div>;
```
## Image with caption
```js
let caption =
"This is the user-provided caption. We show it overlaid on the image. If it's really long, then it wraps, but it doesn't get too close to the edges of the image.";
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<CaptionEditor
url="https://placekitten.com/800/600"
attachment={{
contentType: 'image/jpeg',
}}
caption={caption}
contentType="image/jpeg"
onChangeCaption={caption => console.log('onChangeCaption', caption)}
i18n={util.i18n}
/>
</div>;
```
## Video
```js
let caption = null;
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<CaptionEditor
url="fixtures/pixabay-Soap-Bubble-7141.mp4"
attachment={{
contentType: 'video/mp4',
}}
onChangeCaption={caption => console.log('onChangeCaption', caption)}
i18n={util.i18n}
/>
</div>;
```
## Video with caption
```js
let caption =
"This is the user-provided caption. We show it overlaid on the image. If it's really long, then it wraps, but it doesn't get too close to the edges of the image.";
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<CaptionEditor
url="fixtures/pixabay-Soap-Bubble-7141.mp4"
attachment={{
contentType: 'video/mp4',
}}
caption={caption}
onChangeCaption={caption => console.log('onChangeCaption', caption)}
i18n={util.i18n}
/>
</div>;
```

View file

@ -0,0 +1,78 @@
// tslint:disable:react-a11y-anchors
import React from 'react';
import * as GoogleChrome from '../util/GoogleChrome';
import { AttachmentType } from './conversation/types';
import { Localizer } from '../types/Util';
interface Props {
attachment: AttachmentType;
i18n: Localizer;
url: string;
caption?: string;
onChangeCaption?: (caption: string) => void;
close?: () => void;
}
export class CaptionEditor extends React.Component<Props> {
public renderObject() {
const { url, i18n, attachment } = this.props;
const { contentType } = attachment || { contentType: null };
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
if (isImageTypeSupported) {
return (
<img
className="module-caption-editor__image"
alt={i18n('imageAttachmentAlt')}
src={url}
/>
);
}
const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);
if (isVideoTypeSupported) {
return (
<video className="module-caption-editor__video" controls={true}>
<source src={url} />
</video>
);
}
return <div className="module-caption-editor__placeholder" />;
}
public render() {
const { caption, i18n, close, onChangeCaption } = this.props;
return (
<div className="module-caption-editor">
<div
role="button"
onClick={close}
className="module-caption-editor__close-button"
/>
<div className="module-caption-editor__media-container">
{this.renderObject()}
</div>
<div className="module-caption-editor__bottom-bar">
<div className="module-caption-editor__add-caption-button" />
<input
type="text"
value={caption || ''}
maxLength={200}
placeholder={i18n('addACaption')}
className="module-caption-editor__caption-input"
onChange={event => {
if (onChangeCaption) {
onChangeCaption(event.target.value);
}
}}
/>
</div>
</div>
);
}
}

View file

@ -0,0 +1,114 @@
### One image
```jsx
const attachments = [
{
url: util.gifObjectUrl,
contentType: 'image/gif',
width: 320,
height: 240,
},
];
<AttachmentList
attachments={attachments}
onClose={() => console.log('onClose')}
onClickAttachment={attachment => {
console.log('onClickAttachment', attachment);
}}
onCloseAttachment={attachment => {
console.log('onCloseAttachment', attachment);
}}
i18n={util.i18n}
/>;
```
### Four images
```jsx
const attachments = [
{
url: util.gifObjectUrl,
contentType: 'image/png',
width: 320,
height: 240,
},
{
url: util.pngObjectUrl,
contentType: 'image/png',
width: 800,
height: 1200,
},
{
url: util.landscapeObjectUrl,
contentType: 'image/png',
width: 4496,
height: 3000,
},
{
url: util.landscapeGreenObjectUrl,
contentType: 'image/png',
width: 1000,
height: 50,
},
];
<div>
<AttachmentList
attachments={attachments}
onClose={() => console.log('onClose')}
onClickAttachment={attachment => {
console.log('onClickAttachment', attachment);
}}
onCloseAttachment={attachment => {
console.log('onCloseAttachment', attachment);
}}
i18n={util.i18n}
/>
</div>;
```
### A mix of attachment types
```jsx
const attachments = [
{
url: util.gifObjectUrl,
contentType: 'image/gif',
width: 320,
height: 240,
},
{
contentType: 'text/plain',
fileName: 'manifesto.txt',
},
{
url: util.pngObjectUrl,
contentType: 'image/png',
width: 800,
height: 1200,
},
];
<div>
<AttachmentList
attachments={attachments}
onClose={() => console.log('onClose')}
onClickAttachment={attachment => {
console.log('onClickAttachment', attachment);
}}
onCloseAttachment={attachment => {
console.log('onCloseAttachment', attachment);
}}
i18n={util.i18n}
/>
</div>;
```
### No attachments provided
Nothing is shown if attachment list is empty.
```jsx
<AttachmentList attachments={[]} i18n={util.i18n} />
```

View file

@ -0,0 +1,106 @@
import React from 'react';
// import classNames from 'classnames';
import {
isImageTypeSupported,
isVideoTypeSupported,
} from '../../util/GoogleChrome';
import { AttachmentType } from './types';
import { Image } from './Image';
import { StagedGenericAttachment } from './StagedGenericAttachment';
import { Localizer } from '../../types/Util';
interface Props {
attachments: Array<AttachmentType>;
i18n: Localizer;
// onError: () => void;
onClickAttachment: (attachment: AttachmentType) => void;
onCloseAttachment: (attachment: AttachmentType) => void;
onClose: () => void;
}
const IMAGE_WIDTH = 120;
const IMAGE_HEIGHT = 120;
export class AttachmentList extends React.Component<Props> {
// tslint:disable-next-line max-func-body-length */
public render() {
const {
attachments,
i18n,
// onError,
onClickAttachment,
onCloseAttachment,
onClose,
} = this.props;
if (!attachments.length) {
return null;
}
return (
<div className="module-attachments">
{attachments.length > 1 ? (
<div className="module-attachments__header">
<div
role="button"
onClick={onClose}
className="module-attachments__close-button"
/>
</div>
) : null}
<div className="module-attachments__rail">
{(attachments || []).map((attachment, index) => {
const { contentType } = attachment;
if (
isImageTypeSupported(contentType) ||
isVideoTypeSupported(contentType)
) {
return (
<Image
key={getUrl(attachment) || attachment.fileName || index}
alt={`TODO: attachment number ${index}`}
i18n={i18n}
attachment={attachment}
softCorners={true}
playIconOverlay={isVideoAttachment(attachment)}
height={IMAGE_HEIGHT}
width={IMAGE_WIDTH}
url={getUrl(attachment)}
closeButton={true}
onClick={onClickAttachment}
onClickClose={onCloseAttachment}
/>
);
}
return (
<StagedGenericAttachment
key={getUrl(attachment) || attachment.fileName || index}
attachment={attachment}
i18n={i18n}
onClose={onCloseAttachment}
/>
);
})}
</div>
</div>
);
}
}
export function isVideoAttachment(attachment?: AttachmentType) {
return (
attachment &&
attachment.contentType &&
isVideoTypeSupported(attachment.contentType)
);
}
function getUrl(attachment: AttachmentType) {
if (attachment.screenshot) {
return attachment.screenshot.url;
}
return attachment.url;
}

View file

@ -77,18 +77,21 @@
width="199"
attachment={{ caption: 'dogs playing' }}
url={util.pngObjectUrl}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
attachment={{ caption: 'dogs playing' }}
url={util.pngObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
attachment={{ caption: 'dogs playing' }}
url={util.pngObjectUrl}
i18n={util.i18n}
/>
</div>
<hr />
@ -100,6 +103,7 @@
darkOverlay
overlayText="+3"
url={util.pngObjectUrl}
i18n={util.i18n}
/>
<Image
height="149"
@ -108,6 +112,7 @@
darkOverlay
overlayText="+3"
url={util.pngObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
@ -116,6 +121,82 @@
darkOverlay
overlayText="+3"
url={util.pngObjectUrl}
i18n={util.i18n}
/>
</div>
</div>
```
### With top-right X and soft corners
```jsx
<div>
<div>
<Image
height="200"
width="199"
closeButton={true}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
closeButton={true}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
closeButton={true}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
</div>
<br />
<div>
<Image
height="200"
width="199"
closeButton={true}
attachment={{ caption: 'dogs playing' }}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
closeButton={true}
attachment={{ caption: 'dogs playing' }}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
closeButton={true}
attachment={{ caption: 'dogs playing' }}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
</div>
</div>

View file

@ -15,24 +15,29 @@ interface Props {
overlayText?: string;
bottomOverlay?: boolean;
closeButton?: boolean;
curveBottomLeft?: boolean;
curveBottomRight?: boolean;
curveTopLeft?: boolean;
curveTopRight?: boolean;
darkOverlay?: boolean;
playIconOverlay?: boolean;
softCorners?: boolean;
i18n: Localizer;
onClick?: (attachment: AttachmentType) => void;
onClickClose?: (attachment: AttachmentType) => void;
onError?: () => void;
}
export class Image extends React.Component<Props> {
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
public render() {
const {
alt,
attachment,
bottomOverlay,
closeButton,
curveBottomLeft,
curveBottomRight,
curveTopLeft,
@ -41,9 +46,11 @@ export class Image extends React.Component<Props> {
height,
i18n,
onClick,
onClickClose,
onError,
overlayText,
playIconOverlay,
softCorners,
url,
width,
} = this.props;
@ -52,18 +59,20 @@ export class Image extends React.Component<Props> {
return (
<div
role={onClick ? 'button' : undefined}
onClick={() => {
if (onClick) {
onClick(attachment);
}
}}
role="button"
className={classNames(
'module-image',
onClick ? 'module-image__with-click-handler' : null,
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
curveTopRight ? 'module-image--curved-top-right' : null,
softCorners ? 'module-image--soft-corners' : null
)}
>
<img
@ -88,9 +97,22 @@ export class Image extends React.Component<Props> {
curveTopRight ? 'module-image--curved-top-right' : null,
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
curveBottomRight ? 'module-image--curved-bottom-right' : null,
softCorners ? 'module-image--soft-corners' : null,
darkOverlay ? 'module-image__border-overlay--dark' : null
)}
/>
{closeButton ? (
<div
role="button"
onClick={(e: React.MouseEvent<{}>) => {
e.stopPropagation();
if (onClickClose) {
onClickClose(attachment);
}
}}
className="module-image__close-button"
/>
) : null}
{bottomOverlay ? (
<div
className={classNames(

View file

@ -24,7 +24,7 @@ interface Props {
const MAX_WIDTH = 300;
const MAX_HEIGHT = MAX_WIDTH * 1.5;
const MIN_WIDTH = 200;
const MIN_HEIGHT = 25;
const MIN_HEIGHT = 50;
export class ImageGrid extends React.Component<Props> {
// tslint:disable-next-line max-func-body-length */

View file

@ -79,52 +79,6 @@ interface State {
imageBroken: boolean;
}
function isAudio(attachments?: Array<AttachmentType>) {
return (
attachments &&
attachments[0] &&
attachments[0].contentType &&
MIME.isAudio(attachments[0].contentType)
);
}
function canDisplayImage(attachments?: Array<AttachmentType>) {
const { height, width } =
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
return (
height &&
height > 0 &&
height <= 4096 &&
width &&
width > 0 &&
width <= 4096
);
}
function getExtension({
fileName,
contentType,
}: {
fileName: string;
contentType: MIME.MIMEType;
}): string | null {
if (fileName && fileName.indexOf('.') >= 0) {
const lastPeriod = fileName.lastIndexOf('.');
const extension = fileName.slice(lastPeriod + 1);
if (extension.length) {
return extension;
}
}
const slash = contentType.indexOf('/');
if (slash >= 0) {
return contentType.slice(slash + 1);
}
return null;
}
const EXPIRATION_CHECK_MINIMUM = 2000;
const EXPIRED_DELAY = 600;
@ -847,3 +801,49 @@ export class Message extends React.Component<Props, State> {
);
}
}
export function getExtension({
fileName,
contentType,
}: {
fileName: string;
contentType: MIME.MIMEType;
}): string | null {
if (fileName && fileName.indexOf('.') >= 0) {
const lastPeriod = fileName.lastIndexOf('.');
const extension = fileName.slice(lastPeriod + 1);
if (extension.length) {
return extension;
}
}
const slash = contentType.indexOf('/');
if (slash >= 0) {
return contentType.slice(slash + 1);
}
return null;
}
function isAudio(attachments?: Array<AttachmentType>) {
return (
attachments &&
attachments[0] &&
attachments[0].contentType &&
MIME.isAudio(attachments[0].contentType)
);
}
function canDisplayImage(attachments?: Array<AttachmentType>) {
const { height, width } =
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
return (
height &&
height > 0 &&
height <= 4096 &&
width &&
width > 0 &&
width <= 4096
);
}

View file

@ -0,0 +1,44 @@
Text file
```js
const attachment = {
contentType: 'text/plain',
fileName: 'manifesto.txt',
};
<StagedGenericAttachment
attachment={attachment}
i18n={util.i18n}
onClose={attachment => console.log('onClose', attachment)}
/>;
```
File with long name
```js
const attachment = {
contentType: 'text/plain',
fileName: 'this-is-my-very-important-manifesto-you-must-read-it.txt',
};
<StagedGenericAttachment
attachment={attachment}
i18n={util.i18n}
onClose={attachment => console.log('onClose', attachment)}
/>;
```
File with long extension
```js
const attachment = {
contentType: 'text/plain',
fileName: 'manifesto.reallylongtxt',
};
<StagedGenericAttachment
attachment={attachment}
i18n={util.i18n}
onClose={attachment => console.log('onClose', attachment)}
/>;
```

View file

@ -0,0 +1,44 @@
import React from 'react';
import { getExtension } from './Message';
import { Localizer } from '../../types/Util';
import { AttachmentType } from './types';
interface Props {
attachment: AttachmentType;
onClose: (attachment: AttachmentType) => void;
i18n: Localizer;
}
export class StagedGenericAttachment extends React.Component<Props> {
public render() {
const { attachment, onClose } = this.props;
const { fileName, contentType } = attachment;
const extension = getExtension({ contentType, fileName });
return (
<div className="module-staged-generic-attachment">
<div
className="module-staged-generic-attachment__close-button"
role="button"
onClick={() => {
if (onClose) {
onClose(attachment);
}
}}
/>
<div className="module-staged-generic-attachment__icon">
{extension ? (
<div className="module-staged-generic-attachment__icon__extension">
{extension}
</div>
) : null}
</div>
<div className="module-staged-generic-attachment__filename">
{fileName}
</div>
</div>
);
}
}

View file

@ -659,468 +659,495 @@
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " template: $('#conversation').html(),",
"lineNumber": 73,
"lineNumber": 78,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-html(",
"path": "js/views/conversation_view.js",
"line": " template: $('#conversation').html(),",
"lineNumber": 73,
"lineNumber": 78,
"reasonCategory": "usageTrusted",
"updated": "2018-09-15T00:38:04.183Z",
"reasonDetail": "Getting the value, not setting it"
"updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.loadingScreen.$el.prependTo(this.$('.discussion-container'));",
"lineNumber": 143,
"lineNumber": 148,
"reasonCategory": "usageTrusted",
"updated": "2018-11-14T19:09:08.182Z",
"updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-prependTo(",
"path": "js/views/conversation_view.js",
"line": " this.loadingScreen.$el.prependTo(this.$('.discussion-container'));",
"lineNumber": 143,
"lineNumber": 148,
"reasonCategory": "usageTrusted",
"updated": "2018-11-14T19:07:46.079Z",
"updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " el: this.$('form.send'),",
"lineNumber": 147,
"line": " el: this.$('.attachment-list'),",
"lineNumber": 152,
"reasonCategory": "usageTrusted",
"updated": "2018-11-14T19:07:46.079Z",
"updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.conversation-header').append(this.titleView.el);",
"lineNumber": 205,
"lineNumber": 209,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-append(",
"path": "js/views/conversation_view.js",
"line": " this.$('.conversation-header').append(this.titleView.el);",
"lineNumber": 205,
"lineNumber": 209,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
"updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.discussion-container').append(this.view.el);",
"lineNumber": 211,
"lineNumber": 215,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-append(",
"path": "js/views/conversation_view.js",
"line": " this.$('.discussion-container').append(this.view.el);",
"lineNumber": 211,
"lineNumber": 215,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
"updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$messageField = this.$('.send-message');",
"lineNumber": 214,
"lineNumber": 218,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.send-message').focus(this.focusBottomBar.bind(this));",
"lineNumber": 232,
"lineNumber": 236,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$emojiPanelContainer = this.$('.emoji-panel-container');",
"lineNumber": 235,
"lineNumber": 239,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:26:45.287Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('input.file-input').click();",
"lineNumber": 276,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " const fileField = this.$('input.file-input');",
"lineNumber": 279,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " const container = this.$('.discussion-container');",
"lineNumber": 421,
"lineNumber": 451,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-append(",
"path": "js/views/conversation_view.js",
"line": " container.append(this.banner.el);",
"lineNumber": 422,
"lineNumber": 452,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.typingBubbleView.$el.appendTo(this.$('.typing-container'));",
"lineNumber": 459,
"lineNumber": 489,
"reasonCategory": "usageTrusted",
"updated": "2018-11-14T18:51:15.180Z",
"reasonDetail": "$() parameter is a hard-coded string"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " this.typingBubbleView.$el.appendTo(this.$('.typing-container'));",
"lineNumber": 459,
"lineNumber": 489,
"reasonCategory": "usageTrusted",
"updated": "2018-11-14T18:51:15.180Z",
"reasonDetail": "Both parameters are known elements from the DOM"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.send-message').val().length > 0 ||",
"lineNumber": 468,
"lineNumber": 498,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.capture-audio').hide();",
"lineNumber": 471,
"lineNumber": 501,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.capture-audio').show();",
"lineNumber": 473,
"lineNumber": 503,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " if (this.$('.send-message').val().length > 2000) {",
"lineNumber": 477,
"lineNumber": 507,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.android-length-warning').hide();",
"lineNumber": 480,
"lineNumber": 510,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 518,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " view.$el.appendTo(this.$('.capture-audio'));",
"lineNumber": 500,
"lineNumber": 537,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " view.$el.appendTo(this.$('.capture-audio'));",
"lineNumber": 500,
"lineNumber": 537,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.send-message').attr('disabled', true);",
"lineNumber": 502,
"lineNumber": 539,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.bottom-bar form').submit();",
"lineNumber": 509,
"lineNumber": 548,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.send-message').removeAttr('disabled');",
"lineNumber": 512,
"lineNumber": 551,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.bottom-bar form').removeClass('active');",
"lineNumber": 518,
"lineNumber": 557,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.bottom-bar form').addClass('active');",
"lineNumber": 521,
"lineNumber": 560,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " const container = this.$('.discussion-container');",
"lineNumber": 609,
"lineNumber": 648,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-append(",
"path": "js/views/conversation_view.js",
"line": " container.append(this.scrollDownButton.el);",
"lineNumber": 610,
"lineNumber": 649,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 637,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 670,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 674,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " const el = this.$(`#${databaseId}`);",
"lineNumber": 681,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 684,
"lineNumber": 676,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 709,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 713,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " const el = this.$(`#${databaseId}`);",
"lineNumber": 720,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 723,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " lastSeenEl.insertBefore(this.$(`#${oldestUnread.get('id')}`));",
"lineNumber": 861,
"lineNumber": 900,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-insertBefore(",
"path": "js/views/conversation_view.js",
"line": " lastSeenEl.insertBefore(this.$(`#${oldestUnread.get('id')}`));",
"lineNumber": 861,
"lineNumber": 900,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.bar-container').show();",
"lineNumber": 916,
"lineNumber": 955,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.bar-container').hide();",
"lineNumber": 928,
"lineNumber": 967,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " const el = this.$(`#${message.id}`);",
"lineNumber": 1025,
"lineNumber": 1064,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-prepend(",
"path": "js/views/conversation_view.js",
"line": " this.$el.prepend(dialog.el);",
"lineNumber": 1098,
"lineNumber": 1137,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 1121,
"lineNumber": 1160,
"reasonCategory": "usageTrusted",
"updated": "2018-10-11T19:22:47.331Z",
"reasonDetail": "Operating on already-existing DOM elements"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-prepend(",
"path": "js/views/conversation_view.js",
"line": " this.$el.prepend(dialog.el);",
"lineNumber": 1149,
"lineNumber": 1188,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " view.$el.insertBefore(this.$('.panel').first());",
"lineNumber": 1283,
"lineNumber": 1323,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-insertBefore(",
"path": "js/views/conversation_view.js",
"line": " view.$el.insertBefore(this.$('.panel').first());",
"lineNumber": 1283,
"lineNumber": 1323,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-prepend(",
"path": "js/views/conversation_view.js",
"line": " this.$el.prepend(dialog.el);",
"lineNumber": 1361,
"lineNumber": 1401,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.send').prepend(this.quoteView.el);",
"lineNumber": 1531,
"lineNumber": 1571,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-prepend(",
"path": "js/views/conversation_view.js",
"line": " this.$('.send').prepend(this.quoteView.el);",
"lineNumber": 1531,
"lineNumber": 1571,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/conversation_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 1555,
"lineNumber": 1595,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.bottom-bar form').submit();",
"lineNumber": 1610,
"lineNumber": 1650,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " const $attachmentPreviews = this.$('.attachment-previews');",
"lineNumber": 1619,
"lineNumber": 1659,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/conversation_view.js",
"line": " this.$('.panel').css('display') === 'none'",
"lineNumber": 1650,
"lineNumber": 1690,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T02:21:20.921Z",
"reasonDetail": "Protected from arbitrary input"
},
{
@ -1196,103 +1223,58 @@
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " this.$input = this.$('input[type=file]');",
"lineNumber": 45,
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 216,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/file_input_view.js",
"line": " this.$('.avatar').hide();",
"lineNumber": 88,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/file_input_view.js",
"line": " this.$('.attachment-previews').append(this.thumb.render().el);",
"lineNumber": 90,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-append(",
"path": "js/views/file_input_view.js",
"line": " this.$('.attachment-previews').append(this.thumb.render().el);",
"lineNumber": 90,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-$(",
"path": "js/views/file_input_view.js",
"line": " this.thumb.$('img')[0].onload = () => {",
"lineNumber": 98,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/file_input_view.js",
"line": " this.thumb.$('img')[0].onerror = () => {",
"lineNumber": 101,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T03:04:48.403Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 108,
"lineNumber": 222,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 190,
"reasonCategory": "usageTrusted",
"updated": "2018-10-11T19:22:47.331Z",
"reasonDetail": "Operating on already-existing DOM elements"
},
{
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 284,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-$(",
"path": "js/views/file_input_view.js",
"line": " this.$('.avatar').show();",
"lineNumber": 388,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2018-12-15T03:04:48.403Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-wrap(",
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " .wrap('<form>')",
"lineNumber": 398,
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 230,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Hard-coded value"
"updated": "2018-12-15T03:04:48.403Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 236,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T03:04:48.403Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 242,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T03:04:48.403Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-insertAfter(",
"path": "js/views/file_input_view.js",
"line": " toast.$el.insertAfter(this.$el);",
"lineNumber": 248,
"reasonCategory": "usageTrusted",
"updated": "2018-12-15T03:04:48.403Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
@ -3493,7 +3475,7 @@
"lineNumber": 4136,
"reasonCategory": "falseMatch",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "<optional>"
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-wrap(",
@ -4083,7 +4065,7 @@
"lineNumber": 483,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "<optional>"
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
@ -5849,7 +5831,7 @@
"lineNumber": 1699,
"reasonCategory": "falseMatch",
"updated": "2018-09-18T19:19:27.699Z",
"reasonDetail": "<optional>"
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "DOM-innerHTML",