Turn on all of Microsoft's recommend lint rules

Biggest changes forced by this: alt tags for all images, resulting in
new strings added to messages.json, and a new i18n paramter/prop added
in a plot of places.

Another change of note is that there are two new tslint.json files under
ts/test and ts/styleguide to relax our rules a bit there. This required
a change to our package.json script, as manually specifying the config
file there made it ignore our tslint.json files in subdirectories
This commit is contained in:
Scott Nonnenberg 2018-05-22 12:31:43 -07:00
parent 23586be6b0
commit 2988da0981
49 changed files with 311 additions and 123 deletions

View File

@ -460,6 +460,10 @@
"selectAContact": {
"message": "Select a contact or group to start chatting."
},
"contactAvatarAlt": {
"message": "Contact avatar",
"description": "Used in the alt tag for the image avatar of a contact"
},
"sendMessageToContact": {
"message": "Send Message",
"description": "Shown when you are sent a contact and that contact has a signal account"
@ -618,6 +622,28 @@
"message": "Secure session reset",
"description": "This is a past tense, informational message. In other words, your secure session has been reset."
},
"quoteThumbnailAlt": {
"message": "Thumbnail of image from quoted message",
"description": "Used in alt tag of thumbnail images inside of an embedded message quote"
},
"lightboxImageAlt": {
"message": "Image sent in conversation",
"description": "Used in the alt tag for the image shown in a full-screen lightbox view"
},
"fileIconAlt": {
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"noContents": {
"message": "No message contents",
"description": "Shown in a message bubble if we have nothing in the message to display, or a quote and nothing else"

View File

@ -34,7 +34,7 @@
"jshint": "yarn grunt jshint",
"lint": "yarn format --list-different && yarn lint-windows",
"lint-windows": "yarn eslint && yarn grunt lint && yarn tslint",
"tslint": "tslint --config tslint.json --format stylish --project .",
"tslint": "tslint --format stylish --project .",
"format": "prettier --write \"*.{css,js,json,md,scss,ts,tsx}\" \"./**/*.{css,js,json,md,scss,ts,tsx}\"",
"transpile": "tsc",
"clean-transpile": "rimraf ts/**/*.js ts/*.js",

View File

@ -5,6 +5,7 @@ export const show = (element: HTMLElement): void => {
if (container === null) {
throw new TypeError("'.lightbox-container' is required");
}
// tslint:disable-next-line:no-inner-html
container.innerHTML = '';
container.style.display = 'block';
container.appendChild(element);
@ -17,6 +18,7 @@ export const hide = (): void => {
if (container === null) {
return;
}
// tslint:disable-next-line:no-inner-html
container.innerHTML = '';
container.style.display = 'none';
};

View File

@ -8,6 +8,7 @@ const noop = () => {};
objectURL="https://placekitten.com/800/600"
contentType="image/jpeg"
onSave={noop}
i18n={util.i18n}
/>
</div>;
```
@ -18,7 +19,12 @@ const noop = () => {};
const noop = () => {};
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<Lightbox objectURL="foo.tif" contentType="image/tiff" onSave={noop} />
<Lightbox
objectURL="foo.tif"
contentType="image/tiff"
onSave={noop}
i18n={util.i18n}
/>
</div>;
```
@ -32,6 +38,7 @@ const noop = () => {};
objectURL="fixtures/pixabay-Soap-Bubble-7141.mp4"
contentType="video/mp4"
onSave={noop}
i18n={util.i18n}
/>
</div>;
```
@ -42,7 +49,12 @@ const noop = () => {};
const noop = () => {};
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<Lightbox objectURL="foo.mov" contentType="video/quicktime" onSave={noop} />
<Lightbox
objectURL="foo.mov"
contentType="video/quicktime"
onSave={noop}
i18n={util.i18n}
/>
</div>;
```
@ -56,6 +68,7 @@ const noop = () => {};
objectURL="tsconfig.json"
contentType="application/json"
onSave={noop}
i18n={util.i18n}
/>
</div>;
```

View File

@ -1,3 +1,5 @@
// tslint:disable:react-a11y-anchors
import React from 'react';
import classNames from 'classnames';
@ -8,10 +10,13 @@ import * as GoogleChrome from '../util/GoogleChrome';
import * as MIME from '../types/MIME';
import { colorSVG } from '../styles/colorSVG';
import { Localizer } from '../types/Util';
interface Props {
close: () => void;
objectURL: string;
contentType: MIME.MIMEType | undefined;
i18n: Localizer;
objectURL: string;
onNext?: () => void;
onPrevious?: () => void;
onSave?: () => void;
@ -103,6 +108,7 @@ const IconButton = ({ onClick, style, type }: IconButtonProps) => {
href="#"
onClick={clickHandler}
className={classNames('iconButton', type)}
role="button"
style={style}
/>
);
@ -128,10 +134,11 @@ const Icon = ({
maxWidth: 200,
}}
onClick={onClick}
role="button"
/>
);
export class Lightbox extends React.Component<Props, {}> {
export class Lightbox extends React.Component<Props> {
private containerRef: HTMLDivElement | null = null;
public componentDidMount() {
@ -145,18 +152,27 @@ export class Lightbox extends React.Component<Props, {}> {
}
public render() {
const { contentType, objectURL, onNext, onPrevious, onSave } = this.props;
const {
contentType,
objectURL,
onNext,
onPrevious,
onSave,
i18n,
} = this.props;
return (
<div
style={styles.container}
onClick={this.onContainerClick}
ref={this.setContainerRef}
role="dialog"
>
<div style={styles.mainContainer}>
<div style={styles.controlsOffsetPlaceholder} />
<div style={styles.objectContainer}>
{!is.undefined(contentType)
? this.renderObject({ objectURL, contentType })
? this.renderObject({ objectURL, contentType, i18n })
: null}
</div>
<div style={styles.controls}>
@ -189,14 +205,17 @@ export class Lightbox extends React.Component<Props, {}> {
private renderObject = ({
objectURL,
contentType,
i18n,
}: {
objectURL: string;
contentType: MIME.MIMEType;
i18n: Localizer;
}) => {
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
if (isImageTypeSupported) {
return (
<img
alt={i18n('lightboxImageAlt')}
style={styles.object}
src={objectURL}
onClick={this.onObjectClick}
@ -228,6 +247,7 @@ export class Lightbox extends React.Component<Props, {}> {
// tslint:disable-next-line no-console
console.log('Lightbox: Unexpected content type', { contentType });
return <Icon onClick={this.onObjectClick} url="images/file.svg" />;
};
@ -245,11 +265,10 @@ export class Lightbox extends React.Component<Props, {}> {
};
private onKeyUp = (event: KeyboardEvent) => {
const { onClose } = this;
const { onNext, onPrevious } = this.props;
switch (event.key) {
case 'Escape':
onClose();
this.onClose();
break;
case 'ArrowLeft':
@ -265,7 +284,6 @@ export class Lightbox extends React.Component<Props, {}> {
break;
default:
break;
}
};

View File

@ -33,6 +33,6 @@ const messages = [
];
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<LightboxGallery messages={messages} onSave={noop} />
<LightboxGallery messages={messages} onSave={noop} i18n={util.i18n} />
</div>;
```

View File

@ -7,6 +7,8 @@ import * as MIME from '../types/MIME';
import { Lightbox } from './Lightbox';
import { Message } from './conversation/media-gallery/types/Message';
import { Localizer } from '../types/Util';
interface Item {
objectURL?: string;
contentType: MIME.MIMEType | undefined;
@ -14,6 +16,7 @@ interface Item {
interface Props {
close: () => void;
i18n: Localizer;
messages: Array<Message>;
onSave?: ({ message }: { message: Message }) => void;
selectedIndex: number;
@ -42,7 +45,7 @@ export class LightboxGallery extends React.Component<Props, State> {
}
public render() {
const { close, messages, onSave } = this.props;
const { close, messages, onSave, i18n } = this.props;
const { selectedIndex } = this.state;
const selectedMessage: Message = messages[selectedIndex];
@ -65,6 +68,7 @@ export class LightboxGallery extends React.Component<Props, State> {
onSave={onSave ? this.handleSave : undefined}
objectURL={objectURL}
contentType={selectedItem.contentType}
i18n={i18n}
/>
);
}

View File

@ -8,7 +8,7 @@ interface Props {
renderNonNewLine?: RenderTextCallback;
}
export class AddNewLines extends React.Component<Props, {}> {
export class AddNewLines extends React.Component<Props> {
public static defaultProps: Partial<Props> = {
renderNonNewLine: ({ text, key }) => <span key={key}>{text}</span>,
};

View File

@ -69,7 +69,7 @@ function getLabelForAddress(address: PostalAddress, i18n: Localizer): string {
}
}
export class ContactDetail extends React.Component<Props, {}> {
export class ContactDetail extends React.Component<Props> {
public renderEmail(items: Array<Email> | undefined, i18n: Localizer) {
if (!items || items.length === 0) {
return;
@ -159,7 +159,7 @@ export class ContactDetail extends React.Component<Props, {}> {
return (
<div className="contact-detail">
{renderAvatar(contact)}
{renderAvatar(contact, i18n)}
{renderName(contact)}
{renderContactShorthand(contact)}
{renderSendMessage({ hasSignalAccount, i18n, onSendMessage })}

View File

@ -2,27 +2,30 @@ import React from 'react';
import { Emojify } from './Emojify';
import { Localizer } from '../../types/Util';
interface Props {
phoneNumber: string;
name?: string;
profileName?: string;
i18n: Localizer;
}
export class ContactName extends React.Component<Props, {}> {
export class ContactName extends React.Component<Props> {
public render() {
const { phoneNumber, name, profileName } = this.props;
const { phoneNumber, name, profileName, i18n } = this.props;
const title = name ? name : phoneNumber;
const profileElement =
profileName && !name ? (
<span className="profile-name">
~<Emojify text={profileName} />
~<Emojify text={profileName} i18n={i18n} />
</span>
) : null;
return (
<span>
<Emojify text={title} /> {profileElement}
<Emojify text={title} i18n={i18n} /> {profileElement}
</span>
);
}

View File

@ -11,7 +11,7 @@ interface Props {
profileName?: string;
}
export class ConversationTitle extends React.Component<Props, {}> {
export class ConversationTitle extends React.Component<Props> {
public render() {
const { name, phoneNumber, i18n, profileName, isVerified } = this.props;
@ -19,7 +19,7 @@ export class ConversationTitle extends React.Component<Props, {}> {
<span className="conversation-title">
{name ? (
<span className="conversation-name" dir="auto">
<Emojify text={name} />
<Emojify text={name} i18n={i18n} />
</span>
) : null}
{phoneNumber ? (
@ -27,7 +27,7 @@ export class ConversationTitle extends React.Component<Props, {}> {
) : null}{' '}
{profileName ? (
<span className="profileName">
<Emojify text={profileName} />
<Emojify text={profileName} i18n={i18n} />
</span>
) : null}
{isVerified ? (

View File

@ -1,15 +1,17 @@
import React from 'react';
import { Contact, getName } from '../../types/Contact';
import { Localizer } from '../../types/Util';
interface Props {
contact: Contact;
hasSignalAccount: boolean;
i18n: (key: string, values?: Array<string>) => string;
i18n: Localizer;
onSendMessage: () => void;
onOpenContact: () => void;
}
export class EmbeddedContact extends React.Component<Props, {}> {
export class EmbeddedContact extends React.Component<Props> {
public render() {
const {
contact,
@ -20,9 +22,9 @@ export class EmbeddedContact extends React.Component<Props, {}> {
} = this.props;
return (
<div className="embedded-contact" onClick={onOpenContact}>
<div className="embedded-contact" role="button" onClick={onOpenContact}>
<div className="first-line">
{renderAvatar(contact)}
{renderAvatar(contact, i18n)}
<div className="text-container">
{renderName(contact)}
{renderContactShorthand(contact)}
@ -40,13 +42,14 @@ function getInitials(name: string): string {
return name.trim()[0] || '#';
}
export function renderAvatar(contact: Contact) {
export function renderAvatar(contact: Contact, i18n: Localizer) {
const { avatar } = contact;
const path = avatar && avatar.avatar && avatar.avatar.path;
if (!path) {
const name = getName(contact);
const initials = getInitials(name || '');
return (
<div className="image-container">
<div className="default-avatar">{initials}</div>
@ -56,7 +59,7 @@ export function renderAvatar(contact: Contact) {
return (
<div className="image-container">
<img src={path} />
<img src={path} alt={i18n('contactAvatarAlt')} />
</div>
);
}
@ -92,7 +95,7 @@ export function renderSendMessage(props: {
};
return (
<div className="send-message" onClick={onClick}>
<div className="send-message" role="button" onClick={onClick}>
<button className="inner">
<div className="icon bubble-icon" />
{i18n('sendMessageToContact')}

View File

@ -1,53 +1,53 @@
### All emoji
```jsx
<Emojify text="🔥🔥🔥" />
<Emojify text="🔥🔥🔥" i18n={util.i18n} />
```
### With skin color modifier
```jsx
<Emojify text="👍🏾" />
<Emojify text="👍🏾" i18n={util.i18n} />
```
### With `sizeClass` provided
```jsx
<Emojify text="🔥" sizeClass="jumbo" />
<Emojify text="🔥" sizeClass="jumbo" i18n={util.i18n} />
```
```jsx
<Emojify text="🔥" sizeClass="large" />
<Emojify text="🔥" sizeClass="large" i18n={util.i18n} />
```
```jsx
<Emojify text="🔥" sizeClass="medium" />
<Emojify text="🔥" sizeClass="medium" i18n={util.i18n} />
```
```jsx
<Emojify text="🔥" sizeClass="small" />
<Emojify text="🔥" sizeClass="small" i18n={util.i18n} />
```
```jsx
<Emojify text="🔥" sizeClass="" />
<Emojify text="🔥" sizeClass="" i18n={util.i18n} />
```
### Starting and ending with emoji
```jsx
<Emojify text="🔥in between🔥" />
<Emojify text="🔥in between🔥" i18n={util.i18n} />
```
### With emoji in the middle
```jsx
<Emojify text="Before 🔥🔥 after" />
<Emojify text="Before 🔥🔥 after" i18n={util.i18n} />
```
### No emoji
```jsx
<Emojify text="This is the text" />
<Emojify text="This is the text" i18n={util.i18n} />
```
### Providing custom non-link render function
@ -56,5 +56,9 @@
const renderNonEmoji = ({ text, key }) => (
<span key={key}>This is my custom content</span>
);
<Emojify text="Before 🔥🔥 after" renderNonEmoji={renderNonEmoji} />;
<Emojify
text="Before 🔥🔥 after"
renderNonEmoji={renderNonEmoji}
i18n={util.i18n}
/>;
```

View File

@ -1,6 +1,6 @@
import React from 'react';
import classnames from 'classnames';
import classNames from 'classnames';
import is from '@sindresorhus/is';
import {
@ -10,17 +10,19 @@ import {
getTitle,
} from '../../util/emoji';
import { RenderTextCallback } from '../../types/Util';
import { Localizer, RenderTextCallback } from '../../types/Util';
// Some of this logic taken from emoji-js/replacement
function getImageTag({
match,
sizeClass,
key,
i18n,
}: {
match: any;
sizeClass: string | undefined;
key: string | number;
i18n: Localizer;
}) {
const result = getReplacementData(match[0], match[1], match[2]);
@ -35,7 +37,8 @@ function getImageTag({
<img
key={key}
src={img.path}
className={classnames('emoji', sizeClass)}
alt={i18n('emojiAlt', [title || ''])}
className={classNames('emoji', sizeClass)}
data-codepoints={img.full_idx}
title={`:${title}:`}
/>
@ -48,15 +51,16 @@ interface Props {
sizeClass?: '' | 'small' | 'medium' | 'large' | 'jumbo';
/** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
renderNonEmoji?: RenderTextCallback;
i18n: Localizer;
}
export class Emojify extends React.Component<Props, {}> {
export class Emojify extends React.Component<Props> {
public static defaultProps: Partial<Props> = {
renderNonEmoji: ({ text, key }) => <span key={key}>{text}</span>,
};
public render() {
const { text, sizeClass, renderNonEmoji } = this.props;
const { text, sizeClass, renderNonEmoji, i18n } = this.props;
const results: Array<any> = [];
const regex = getRegex();
@ -80,7 +84,7 @@ export class Emojify extends React.Component<Props, {}> {
results.push(renderNonEmoji({ text: textWithNoEmoji, key: count++ }));
}
results.push(getImageTag({ match, sizeClass, key: count++ }));
results.push(getImageTag({ match, sizeClass, key: count++, i18n }));
last = regex.lastIndex;
match = regex.exec(text);

View File

@ -1,10 +1,10 @@
import React from 'react';
import createLinkify from 'linkify-it';
import LinkifyIt from 'linkify-it';
import { RenderTextCallback } from '../../types/Util';
const linkify = createLinkify();
const linkify = LinkifyIt();
interface Props {
text: string;
@ -14,7 +14,7 @@ interface Props {
const SUPPORTED_PROTOCOLS = /^(http|https):/i;
export class Linkify extends React.Component<Props, {}> {
export class Linkify extends React.Component<Props> {
public static defaultProps: Partial<Props> = {
renderNonLink: ({ text, key }) => <span key={key}>{text}</span>,
};

View File

@ -1,3 +1,5 @@
// tslint:disable:newline-before-return
import React from 'react';
/**
@ -5,7 +7,7 @@ import React from 'react';
* none of the dynamic functionality. This page will be used to build up our corpus of
* permutations before we start moving all message functionality to React.
*/
export class Message extends React.Component<{}, {}> {
export class Message extends React.Component {
public render() {
return (
<li className="entry outgoing sent delivered">

View File

@ -1,43 +1,46 @@
### All components: emoji, links, newline
```jsx
<MessageBody text="Fire 🔥 http://somewhere.com\nSecond Line" />
<MessageBody
text="Fire 🔥 http://somewhere.com\nSecond Line"
i18n={util.i18n}
/>
```
### Jumbo emoji
```jsx
<MessageBody text="🔥" />
<MessageBody text="🔥" i18n={util.i18n} />
```
```jsx
<MessageBody text="🔥🔥" />
<MessageBody text="🔥🔥" i18n={util.i18n} />
```
```jsx
<MessageBody text="🔥🔥🔥🔥" />
<MessageBody text="🔥🔥🔥🔥" i18n={util.i18n} />
```
```jsx
<MessageBody text="🔥🔥🔥🔥🔥🔥🔥🔥" />
<MessageBody text="🔥🔥🔥🔥🔥🔥🔥🔥" i18n={util.i18n} />
```
```jsx
<MessageBody text="🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥" />
<MessageBody text="🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥" i18n={util.i18n} />
```
```jsx
<MessageBody text="🔥 text disables jumbomoji" />
<MessageBody text="🔥 text disables jumbomoji" i18n={util.i18n} />
```
### Jumbomoji disabled
```jsx
<MessageBody text="🔥" disableJumbomoji />
<MessageBody text="🔥" disableJumbomoji i18n={util.i18n} />
```
### Links disabled
```jsx
<MessageBody text="http://somewhere.com" disableLinks />
<MessageBody text="http://somewhere.com" disableLinks i18n={util.i18n} />
```

View File

@ -5,7 +5,7 @@ import { Emojify } from './Emojify';
import { AddNewLines } from './AddNewLines';
import { Linkify } from './Linkify';
import { RenderTextCallback } from '../../types/Util';
import { Localizer, RenderTextCallback } from '../../types/Util';
interface Props {
text: string;
@ -13,6 +13,7 @@ interface Props {
disableJumbomoji?: boolean;
/** If set, links will be left alone instead of turned into clickable `<a>` tags. */
disableLinks?: boolean;
i18n: Localizer;
}
const renderNewLines: RenderTextCallback = ({
@ -30,9 +31,9 @@ const renderLinks: RenderTextCallback = ({ text: textWithLinks, key }) => (
* configurable with their `renderXXX` props, this component will assemble all three of
* them for you.
*/
export class MessageBody extends React.Component<Props, {}> {
export class MessageBody extends React.Component<Props> {
public render() {
const { text, disableJumbomoji, disableLinks } = this.props;
const { text, disableJumbomoji, disableLinks, i18n } = this.props;
const sizeClass = disableJumbomoji ? '' : getSizeClass(text);
return (
@ -40,6 +41,7 @@ export class MessageBody extends React.Component<Props, {}> {
text={text}
sizeClass={sizeClass}
renderNonEmoji={disableLinks ? renderNewLines : renderLinks}
i18n={i18n}
/>
);
}

View File

@ -1,18 +1,21 @@
// tslint:disable:react-this-binding-issue
import React from 'react';
import classnames from 'classnames';
import classNames from 'classnames';
import * as MIME from '../../../ts/types/MIME';
import * as GoogleChrome from '../../../ts/util/GoogleChrome';
import { Emojify } from './Emojify';
import { MessageBody } from './MessageBody';
import { Localizer } from '../../types/Util';
interface Props {
attachments: Array<QuotedAttachment>;
authorColor: string;
authorProfileName?: string;
authorTitle: string;
i18n: (key: string, values?: Array<string>) => string;
i18n: Localizer;
isFromMe: string;
isIncoming: boolean;
onClick?: () => void;
@ -23,14 +26,14 @@ interface Props {
interface QuotedAttachment {
contentType: MIME.MIMEType;
fileName: string;
/* Not included in protobuf */
/** Not included in protobuf */
isVoiceMessage: boolean;
thumbnail?: Attachment;
}
interface Attachment {
contentType: MIME.MIMEType;
/* Not included in protobuf, and is loaded asynchronously */
/** Not included in protobuf, and is loaded asynchronously */
objectUrl?: string;
}
@ -54,16 +57,16 @@ function getObjectUrl(thumbnail: Attachment | undefined): string | null {
return null;
}
export class Quote extends React.Component<Props, {}> {
public renderImage(url: string, icon?: string) {
export class Quote extends React.Component<Props> {
public renderImage(url: string, i18n: Localizer, icon?: string) {
const iconElement = icon ? (
<div className={classnames('icon', 'with-image', icon)} />
<div className={classNames('icon', 'with-image', icon)} />
) : null;
return (
<div className="icon-container">
<div className="inner">
<img src={url} />
<img src={url} alt={i18n('quoteThumbnailAlt')} />
{iconElement}
</div>
</div>
@ -78,14 +81,14 @@ export class Quote extends React.Component<Props, {}> {
return (
<div className="icon-container">
<div className={classnames('circle-background', backgroundColor)} />
<div className={classnames('icon', icon, iconColor)} />
<div className={classNames('circle-background', backgroundColor)} />
<div className={classNames('icon', icon, iconColor)} />
</div>
);
}
public renderIconContainer() {
const { attachments } = this.props;
const { attachments, i18n } = this.props;
if (!attachments || attachments.length === 0) {
return null;
}
@ -96,11 +99,13 @@ export class Quote extends React.Component<Props, {}> {
if (GoogleChrome.isVideoTypeSupported(contentType)) {
return objectUrl
? this.renderImage(objectUrl, 'play')
? this.renderImage(objectUrl, i18n, 'play')
: this.renderIcon('movie');
}
if (GoogleChrome.isImageTypeSupported(contentType)) {
return objectUrl ? this.renderImage(objectUrl) : this.renderIcon('image');
return objectUrl
? this.renderImage(objectUrl, i18n)
: this.renderIcon('image');
}
if (MIME.isAudio(contentType)) {
return this.renderIcon('microphone');
@ -115,7 +120,7 @@ export class Quote extends React.Component<Props, {}> {
if (text) {
return (
<div className="text">
<MessageBody text={text} />
<MessageBody text={text} i18n={i18n} />
</div>
);
}
@ -181,7 +186,7 @@ export class Quote extends React.Component<Props, {}> {
// We need the container to give us the flexibility to implement the iOS design.
return (
<div className="close-container">
<div className="close-button" onClick={onClick} />
<div className="close-button" role="button" onClick={onClick} />
</div>
);
}
@ -197,17 +202,17 @@ export class Quote extends React.Component<Props, {}> {
const authorProfileElement = authorProfileName ? (
<span className="profile-name">
~<Emojify text={authorProfileName} />
~<Emojify text={authorProfileName} i18n={i18n} />
</span>
) : null;
return (
<div className={classnames(authorColor, 'author')}>
<div className={classNames(authorColor, 'author')}>
{isFromMe ? (
i18n('you')
) : (
<span>
<Emojify text={authorTitle} /> {authorProfileElement}
<Emojify text={authorTitle} i18n={i18n} /> {authorProfileElement}
</span>
)}
</div>
@ -221,7 +226,7 @@ export class Quote extends React.Component<Props, {}> {
return null;
}
const classes = classnames(
const classes = classNames(
authorColor,
'quoted-message',
isFromMe ? 'from-me' : null,
@ -229,7 +234,7 @@ export class Quote extends React.Component<Props, {}> {
);
return (
<div onClick={onClick} className={classes}>
<div onClick={onClick} role="button" className={classes}>
<div className="primary">
{this.renderIOSLabel()}
{this.renderAuthor()}

View File

@ -20,5 +20,10 @@ const messages = [
},
];
<AttachmentSection header="Today" type="documents" messages={messages} />;
<AttachmentSection
header="Today"
type="documents"
messages={messages}
i18n={util.i18n}
/>;
```

View File

@ -33,7 +33,7 @@ interface Props {
onItemClick?: (event: ItemClickEvent) => void;
}
export class AttachmentSection extends React.Component<Props, {}> {
export class AttachmentSection extends React.Component<Props> {
public render() {
const { header } = this.props;

View File

@ -4,17 +4,20 @@
fileName="meow.jpg"
fileSize={1024 * 1000 * 2}
timestamp={Date.now()}
i18n={util.i18n}
/>
<DocumentListItem
fileName="rickroll.wmv"
fileSize={1024 * 1000 * 8}
timestamp={Date.now() - 24 * 60 * 1000}
i18n={util.i18n}
/>
<DocumentListItem
fileName="kitten.gif"
fileSize={1024 * 1000 * 1.2}
timestamp={Date.now() - 14 * 24 * 60 * 1000}
shouldShowSeparator={false}
i18n={util.i18n}
/>
</div>
```

View File

@ -1,11 +1,14 @@
import React from 'react';
import moment from 'moment';
// tslint:disable-next-line:match-default-export-name
import formatFileSize from 'filesize';
import { Localizer } from '../../../types/Util';
interface Props {
// Required
i18n: (key: string, values?: Array<string>) => string;
i18n: Localizer;
timestamp: number;
// Optional
@ -58,13 +61,14 @@ const styles = {
},
};
export class DocumentListItem extends React.Component<Props, {}> {
export class DocumentListItem extends React.Component<Props> {
public static defaultProps: Partial<Props> = {
shouldShowSeparator: true,
};
public render() {
const { shouldShowSeparator } = this.props;
return (
<div
style={{
@ -78,11 +82,16 @@ export class DocumentListItem extends React.Component<Props, {}> {
}
private renderContent() {
const { fileName, fileSize, timestamp } = this.props;
const { fileName, fileSize, timestamp, i18n } = this.props;
return (
<div style={styles.itemContainer} onClick={this.props.onClick}>
<div
style={styles.itemContainer}
role="button"
onClick={this.props.onClick}
>
<img
alt={i18n('fileIconAlt')}
src="images/file.svg"
width="48"
height="48"

View File

@ -21,9 +21,10 @@ const styles = {
} as React.CSSProperties,
};
export class EmptyState extends React.Component<Props, {}> {
export class EmptyState extends React.Component<Props> {
public render() {
const { label } = this.props;
return <div style={styles.container}>{label}</div>;
}
}

View File

@ -6,6 +6,7 @@
i18n={window.i18n}
media={[]}
documents={[]}
i18n={util.i18n}
/>
</div>
```
@ -72,7 +73,7 @@ const messages = _.sortBy(
message => -message.received_at
);
<MediaGallery i18n={window.i18n} media={messages} documents={messages} />;
<MediaGallery i18n={util.i18n} media={messages} documents={messages} />;
```
## Media gallery with one document
@ -83,5 +84,5 @@ const messages = [
attachments: [{ fileName: 'foo.jpg', contentType: 'application/json' }],
},
];
<MediaGallery i18n={window.i18n} media={messages} documents={messages} />;
<MediaGallery i18n={util.i18n} media={messages} documents={messages} />;
```

View File

@ -81,12 +81,17 @@ const Tab = ({
onSelect?: (event: TabSelectEvent) => void;
type: AttachmentType;
}) => {
const handleClick = onSelect ? () => onSelect({ type }) : undefined;
const handleClick = onSelect
? () => {
onSelect({ type });
}
: undefined;
return (
<div
style={isSelected ? styles.tab.active : styles.tab.default}
onClick={handleClick}
role="tab"
>
{label}
</div>
@ -146,6 +151,7 @@ export class MediaGallery extends React.Component<Props, State> {
throw missingCaseError(type);
}
})();
return <EmptyState data-test="EmptyState" label={label} />;
}
@ -157,6 +163,7 @@ export class MediaGallery extends React.Component<Props, State> {
section.type === 'yearMonth'
? date.format(MONTH_FORMAT)
: i18n(section.type);
return (
<AttachmentSection
key={header}

View File

@ -25,7 +25,7 @@ const styles = {
},
};
export class MediaGridItem extends React.Component<Props, {}> {
export class MediaGridItem extends React.Component<Props> {
public renderContent() {
const { message } = this.props;
@ -46,7 +46,7 @@ export class MediaGridItem extends React.Component<Props, {}> {
public render() {
return (
<div style={styles.container} onClick={this.props.onClick}>
<div style={styles.container} role="button" onClick={this.props.onClick}>
{this.renderContent()}
</div>
);

View File

@ -31,6 +31,7 @@ export const groupMessagesByDate = (
const yearMonthMessages = Object.values(
groupBy(groupedMessages.yearMonth, 'order')
).reverse();
return compact([
toSection(groupedMessages.today),
toSection(groupedMessages.yesterday),
@ -138,6 +139,7 @@ const withSection = (referenceDateTime: moment.Moment) => (
const month: number = messageReceivedDate.month();
const year: number = messageReceivedDate.year();
return {
order: year * 100 + month,
type: 'yearMonth',

View File

@ -66,6 +66,7 @@ export const withObjectURL = (message: Message): Message => {
data: attachment.data,
type: attachment.contentType,
});
return {
...message,
objectURL,

View File

@ -21,7 +21,7 @@ interface BackboneViewConstructor {
* Allows Backbone Views to be rendered inside of React (primarily for the Style Guide)
* while we slowly replace the internals of a given Backbone view with React.
*/
export class BackboneWrapper extends React.Component<Props, {}> {
export class BackboneWrapper extends React.Component<Props> {
protected el: HTMLElement | null = null;
protected view: BackboneView | null = null;
@ -44,10 +44,9 @@ export class BackboneWrapper extends React.Component<Props, {}> {
};
protected setup = () => {
const { el } = this;
const { View, options } = this.props;
if (!el) {
if (!this.el) {
return;
}
this.view = new View(options);
@ -55,7 +54,7 @@ export class BackboneWrapper extends React.Component<Props, {}> {
// It's important to let the view create its own root DOM element. This ensures that
// its tagName property actually takes effect.
el.appendChild(this.view.el);
this.el.appendChild(this.view.el);
};
protected teardown() {

View File

@ -1,3 +1,4 @@
// tslint:disable-next-line: match-default-export-name
import linkTextInternal from '../../js/modules/link_text';
export const linkText = (value: string): string =>

View File

@ -1,5 +1,5 @@
import React from 'react';
import classnames from 'classnames';
import classNames from 'classnames';
interface Props {
/**
@ -13,13 +13,13 @@ interface Props {
* Provides the parent elements necessary to allow the main Signal Desktop stylesheet to
* apply (with no changes) to messages in the Style Guide.
*/
export class ConversationContext extends React.Component<Props, {}> {
export class ConversationContext extends React.Component<Props> {
public render() {
const { theme, type } = this.props;
return (
<div className={theme || 'android'}>
<div className={classnames('conversation', type || 'private')}>
<div className={classNames('conversation', type || 'private')}>
<div className="discussion-container" style={{ padding: '0.5em' }}>
<ul className="message-list">{this.props.children}</ul>
</div>

View File

@ -1,11 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { padStart, sample } from 'lodash';
import { default as _, padStart, sample } from 'lodash';
import libphonenumber from 'google-libphonenumber';
import _ from 'lodash';
import moment from 'moment';
import qs from 'qs';
import QueryString from 'qs';
export { _ };
@ -57,6 +56,7 @@ function makeObjectUrl(data: ArrayBuffer, contentType: string): string {
const blob = new Blob([data], {
type: contentType,
});
return URL.createObjectURL(blob);
}
@ -90,7 +90,7 @@ export {
const parent = window as any;
const query = window.location.search.replace(/^\?/, '');
const urlOptions = qs.parse(query);
const urlOptions = QueryString.parse(query);
const theme = urlOptions.theme || 'android';
const locale = urlOptions.locale || 'en';
@ -99,11 +99,11 @@ import localeMessages from '../../_locales/en/messages.json';
// @ts-ignore
import { setup } from '../../js/modules/i18n';
import filesize from 'filesize';
import fileSize from 'filesize';
const i18n = setup(locale, localeMessages);
parent.filesize = filesize;
parent.filesize = fileSize;
parent.i18n = i18n;
parent.React = React;
@ -137,6 +137,7 @@ const Attachments = {
parent.Signal = Signal.setup({
Attachments,
userDataPath: '/',
// tslint:disable-next-line:no-backbone-get-set-outside-model
getRegionCode: () => parent.storage.get('regionCode'),
});
parent.SignalService = SignalService;

11
ts/styleguide/tslint.json Normal file
View File

@ -0,0 +1,11 @@
{
"defaultSeverity": "error",
"extends": ["../../tslint.json"],
"rules": {
// To allow the use of devDependencies here
"no-implicit-dependencies": false,
// All tests use arrow functions, and they can be long
"max-func-body-length": false
}
}

View File

@ -1,5 +1,3 @@
import 'mocha';
import { assert } from 'chai';
import { shuffle } from 'lodash';

View File

@ -1,4 +1,3 @@
import 'mocha';
import { assert } from 'chai';
import * as HTML from '../../html';
@ -52,6 +51,7 @@ describe('HTML', () => {
{
name: 'URLs without protocols',
input: 'github.com',
// tslint:disable-next-line:no-http-string
outputHref: 'http://github.com',
outputLabel: 'github.com',
},

11
ts/test/tslint.json Normal file
View File

@ -0,0 +1,11 @@
{
"defaultSeverity": "error",
"extends": ["../../tslint.json"],
"rules": {
// To allow the use of devDependencies here
"no-implicit-dependencies": false,
// All tests use arrow functions, and they can be long
"max-func-body-length": false
}
}

View File

@ -1,7 +1,3 @@
/**
* @prettier
*/
import 'mocha';
import { assert } from 'chai';
import * as Attachment from '../../types/Attachment';

View File

@ -1,4 +1,3 @@
import 'mocha';
import { assert } from 'chai';
import { getName } from '../../types/Contact';

View File

@ -1,4 +1,3 @@
import 'mocha';
import { assert } from 'chai';
import * as Conversation from '../../types/Conversation';

View File

@ -1,11 +1,11 @@
import os from 'os';
import sinon from 'sinon';
import Sinon from 'sinon';
import { assert } from 'chai';
import * as Settings from '../../../ts/types/Settings';
describe('Settings', () => {
const sandbox = sinon.createSandbox();
const sandbox = Sinon.createSandbox();
describe('isAudioNotificationSupported', () => {
context('on macOS', () => {

View File

@ -1,4 +1,3 @@
import 'mocha';
import { assert } from 'chai';
import * as Message from '../../../../ts/types/message/initializeAttachmentMetadata';

View File

@ -121,6 +121,7 @@ export const getSuggestedFilename = ({
: '';
const fileType = getFileExtension(attachment);
const extension = fileType ? `.${fileType}` : '';
return `${prefix}${suffix}${extension}`;
};
@ -133,7 +134,6 @@ export const getFileExtension = (attachment: Attachment): string | null => {
case 'video/quicktime':
return 'mov';
default:
// TODO: Use better MIME --> file extension mapping:
return attachment.contentType.split('/')[1];
}
};

View File

@ -85,6 +85,7 @@ export function contactSelector(
},
};
}
return {
...contact,
avatar,

View File

@ -100,5 +100,6 @@ export const hasExpiration = (message: Message): boolean => {
}
const { expireTimer } = message;
return typeof expireTimer === 'number' && expireTimer > 0;
};

View File

@ -4,5 +4,5 @@ export interface Collection<T> {
models: Array<Model<T>>;
// tslint:disable-next-line no-misused-new
new (): Collection<T>;
fetch(options: object): JQuery.Deferred<any, any, any>;
fetch(options: object): JQuery.Deferred<any>;
}

View File

@ -14,5 +14,6 @@ export const arrayBufferToObjectURL = ({
}
const blob = new Blob([data], { type });
return URL.createObjectURL(blob);
};

View File

@ -29,6 +29,7 @@ export function replaceColons(str: string) {
if (code) {
return instance.data[code][0][0];
}
return m;
});
}
@ -51,6 +52,7 @@ function getCountOfAllMatches(str: string, regex: RegExp) {
function hasNormalCharacters(str: string) {
const noEmoji = str.replace(instance.rx_unified, '').trim();
return noEmoji.length > 0;
}
@ -96,6 +98,7 @@ export function getReplacementData(
variation,
};
}
return {
value: unified,
};

View File

@ -1,6 +1,6 @@
{
"defaultSeverity": "error",
"extends": ["tslint:recommended", "tslint-react"],
"extends": ["tslint:recommended", "tslint-react", "tslint-microsoft-contrib"],
"jsRules": {},
"rules": {
"align": [
@ -35,6 +35,9 @@
"mocha-no-side-effect-code": false,
"mocha-unneeded-done": true,
// We always want 'as Type'
"no-angle-bracket-type-assertion": true,
"no-consecutive-blank-lines": [true, 2],
"object-literal-key-quotes": [true, "as-needed"],
"object-literal-sort-keys": false,
@ -73,7 +76,54 @@
},
"esSpecCompliant": true
}
]
],
// Disabling a large set of Microsoft-recommended rules
// Modifying:
// React components and namespaces are Pascal case
"variable-name": [true, "allow-pascal-case"],
// Maybe will turn on:
// We're not trying to be comprehensive with JSDoc right now. We have the style guide.
"completed-docs": false,
// Today we have files with a single named export which isn't the filename. Eventually.
"export-name": false,
// We have a lot of 'any' in our code today
"no-any": false,
// We use this today, could get rid of it
"no-increment-decrement": false,
// This seems to detect false positives: any multi-level object literal, for example
"no-object-literal-type-assertion": false,
// I like relative references to the current dir, or absolute. Maybe can do this?
"no-relative-imports": false,
// We have a lot of 'any' in our code today
"no-unsafe-any": false,
// Not everything needs to be typed right now
"typedef": false,
// Probably won't turn on:
// We want to import a capitalized React, for example
"import-name": false,
// We have the styleguide for better docs
"missing-jsdoc": false,
// 'type' and 'number' are just too common
"no-reserved-keywords": false,
// The style guide needs JSDoc-style block comments to extract proptype documentation
"no-single-line-block-comment": false,
// Out-of-order functions can improve readability
"no-use-before-declare": false,
// We use Array<type> syntax
"prefer-array-literal": false,
// We prefer key: () => void syntax, because it suggests an object instead of a class
"prefer-method-signature": false,
// 'as' is nicer than angle brackets.
"prefer-type-cast": false,
// We use || and && shortcutting because we're javascript programmers
"strict-boolean-expressions": false
},
"rulesDirectory": ["node_modules/tslint-microsoft-contrib"]
}