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": { "selectAContact": {
"message": "Select a contact or group to start chatting." "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": { "sendMessageToContact": {
"message": "Send Message", "message": "Send Message",
"description": "Shown when you are sent a contact and that contact has a signal account" "description": "Shown when you are sent a contact and that contact has a signal account"
@ -618,6 +622,28 @@
"message": "Secure session reset", "message": "Secure session reset",
"description": "This is a past tense, informational message. In other words, your secure session has been 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": { "noContents": {
"message": "No message contents", "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" "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", "jshint": "yarn grunt jshint",
"lint": "yarn format --list-different && yarn lint-windows", "lint": "yarn format --list-different && yarn lint-windows",
"lint-windows": "yarn eslint && yarn grunt lint && yarn tslint", "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}\"", "format": "prettier --write \"*.{css,js,json,md,scss,ts,tsx}\" \"./**/*.{css,js,json,md,scss,ts,tsx}\"",
"transpile": "tsc", "transpile": "tsc",
"clean-transpile": "rimraf ts/**/*.js ts/*.js", "clean-transpile": "rimraf ts/**/*.js ts/*.js",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ interface Props {
renderNonNewLine?: RenderTextCallback; renderNonNewLine?: RenderTextCallback;
} }
export class AddNewLines extends React.Component<Props, {}> { export class AddNewLines extends React.Component<Props> {
public static defaultProps: Partial<Props> = { public static defaultProps: Partial<Props> = {
renderNonNewLine: ({ text, key }) => <span key={key}>{text}</span>, 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) { public renderEmail(items: Array<Email> | undefined, i18n: Localizer) {
if (!items || items.length === 0) { if (!items || items.length === 0) {
return; return;
@ -159,7 +159,7 @@ export class ContactDetail extends React.Component<Props, {}> {
return ( return (
<div className="contact-detail"> <div className="contact-detail">
{renderAvatar(contact)} {renderAvatar(contact, i18n)}
{renderName(contact)} {renderName(contact)}
{renderContactShorthand(contact)} {renderContactShorthand(contact)}
{renderSendMessage({ hasSignalAccount, i18n, onSendMessage })} {renderSendMessage({ hasSignalAccount, i18n, onSendMessage })}

View file

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

View file

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

View file

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

View file

@ -1,53 +1,53 @@
### All emoji ### All emoji
```jsx ```jsx
<Emojify text="🔥🔥🔥" /> <Emojify text="🔥🔥🔥" i18n={util.i18n} />
``` ```
### With skin color modifier ### With skin color modifier
```jsx ```jsx
<Emojify text="👍🏾" /> <Emojify text="👍🏾" i18n={util.i18n} />
``` ```
### With `sizeClass` provided ### With `sizeClass` provided
```jsx ```jsx
<Emojify text="🔥" sizeClass="jumbo" /> <Emojify text="🔥" sizeClass="jumbo" i18n={util.i18n} />
``` ```
```jsx ```jsx
<Emojify text="🔥" sizeClass="large" /> <Emojify text="🔥" sizeClass="large" i18n={util.i18n} />
``` ```
```jsx ```jsx
<Emojify text="🔥" sizeClass="medium" /> <Emojify text="🔥" sizeClass="medium" i18n={util.i18n} />
``` ```
```jsx ```jsx
<Emojify text="🔥" sizeClass="small" /> <Emojify text="🔥" sizeClass="small" i18n={util.i18n} />
``` ```
```jsx ```jsx
<Emojify text="🔥" sizeClass="" /> <Emojify text="🔥" sizeClass="" i18n={util.i18n} />
``` ```
### Starting and ending with emoji ### Starting and ending with emoji
```jsx ```jsx
<Emojify text="🔥in between🔥" /> <Emojify text="🔥in between🔥" i18n={util.i18n} />
``` ```
### With emoji in the middle ### With emoji in the middle
```jsx ```jsx
<Emojify text="Before 🔥🔥 after" /> <Emojify text="Before 🔥🔥 after" i18n={util.i18n} />
``` ```
### No emoji ### No emoji
```jsx ```jsx
<Emojify text="This is the text" /> <Emojify text="This is the text" i18n={util.i18n} />
``` ```
### Providing custom non-link render function ### Providing custom non-link render function
@ -56,5 +56,9 @@
const renderNonEmoji = ({ text, key }) => ( const renderNonEmoji = ({ text, key }) => (
<span key={key}>This is my custom content</span> <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 React from 'react';
import classnames from 'classnames'; import classNames from 'classnames';
import is from '@sindresorhus/is'; import is from '@sindresorhus/is';
import { import {
@ -10,17 +10,19 @@ import {
getTitle, getTitle,
} from '../../util/emoji'; } from '../../util/emoji';
import { RenderTextCallback } from '../../types/Util'; import { Localizer, RenderTextCallback } from '../../types/Util';
// Some of this logic taken from emoji-js/replacement // Some of this logic taken from emoji-js/replacement
function getImageTag({ function getImageTag({
match, match,
sizeClass, sizeClass,
key, key,
i18n,
}: { }: {
match: any; match: any;
sizeClass: string | undefined; sizeClass: string | undefined;
key: string | number; key: string | number;
i18n: Localizer;
}) { }) {
const result = getReplacementData(match[0], match[1], match[2]); const result = getReplacementData(match[0], match[1], match[2]);
@ -35,7 +37,8 @@ function getImageTag({
<img <img
key={key} key={key}
src={img.path} src={img.path}
className={classnames('emoji', sizeClass)} alt={i18n('emojiAlt', [title || ''])}
className={classNames('emoji', sizeClass)}
data-codepoints={img.full_idx} data-codepoints={img.full_idx}
title={`:${title}:`} title={`:${title}:`}
/> />
@ -48,15 +51,16 @@ interface Props {
sizeClass?: '' | 'small' | 'medium' | 'large' | 'jumbo'; sizeClass?: '' | 'small' | 'medium' | 'large' | 'jumbo';
/** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */ /** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
renderNonEmoji?: RenderTextCallback; renderNonEmoji?: RenderTextCallback;
i18n: Localizer;
} }
export class Emojify extends React.Component<Props, {}> { export class Emojify extends React.Component<Props> {
public static defaultProps: Partial<Props> = { public static defaultProps: Partial<Props> = {
renderNonEmoji: ({ text, key }) => <span key={key}>{text}</span>, renderNonEmoji: ({ text, key }) => <span key={key}>{text}</span>,
}; };
public render() { public render() {
const { text, sizeClass, renderNonEmoji } = this.props; const { text, sizeClass, renderNonEmoji, i18n } = this.props;
const results: Array<any> = []; const results: Array<any> = [];
const regex = getRegex(); const regex = getRegex();
@ -80,7 +84,7 @@ export class Emojify extends React.Component<Props, {}> {
results.push(renderNonEmoji({ text: textWithNoEmoji, key: count++ })); 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; last = regex.lastIndex;
match = regex.exec(text); match = regex.exec(text);

View file

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

View file

@ -1,3 +1,5 @@
// tslint:disable:newline-before-return
import React from 'react'; 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 * 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. * permutations before we start moving all message functionality to React.
*/ */
export class Message extends React.Component<{}, {}> { export class Message extends React.Component {
public render() { public render() {
return ( return (
<li className="entry outgoing sent delivered"> <li className="entry outgoing sent delivered">

View file

@ -1,43 +1,46 @@
### All components: emoji, links, newline ### All components: emoji, links, newline
```jsx ```jsx
<MessageBody text="Fire 🔥 http://somewhere.com\nSecond Line" /> <MessageBody
text="Fire 🔥 http://somewhere.com\nSecond Line"
i18n={util.i18n}
/>
``` ```
### Jumbo emoji ### Jumbo emoji
```jsx ```jsx
<MessageBody text="🔥" /> <MessageBody text="🔥" i18n={util.i18n} />
``` ```
```jsx ```jsx
<MessageBody text="🔥🔥" /> <MessageBody text="🔥🔥" i18n={util.i18n} />
``` ```
```jsx ```jsx
<MessageBody text="🔥🔥🔥🔥" /> <MessageBody text="🔥🔥🔥🔥" i18n={util.i18n} />
``` ```
```jsx ```jsx
<MessageBody text="🔥🔥🔥🔥🔥🔥🔥🔥" /> <MessageBody text="🔥🔥🔥🔥🔥🔥🔥🔥" i18n={util.i18n} />
``` ```
```jsx ```jsx
<MessageBody text="🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥" /> <MessageBody text="🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥" i18n={util.i18n} />
``` ```
```jsx ```jsx
<MessageBody text="🔥 text disables jumbomoji" /> <MessageBody text="🔥 text disables jumbomoji" i18n={util.i18n} />
``` ```
### Jumbomoji disabled ### Jumbomoji disabled
```jsx ```jsx
<MessageBody text="🔥" disableJumbomoji /> <MessageBody text="🔥" disableJumbomoji i18n={util.i18n} />
``` ```
### Links disabled ### Links disabled
```jsx ```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 { AddNewLines } from './AddNewLines';
import { Linkify } from './Linkify'; import { Linkify } from './Linkify';
import { RenderTextCallback } from '../../types/Util'; import { Localizer, RenderTextCallback } from '../../types/Util';
interface Props { interface Props {
text: string; text: string;
@ -13,6 +13,7 @@ interface Props {
disableJumbomoji?: boolean; disableJumbomoji?: boolean;
/** If set, links will be left alone instead of turned into clickable `<a>` tags. */ /** If set, links will be left alone instead of turned into clickable `<a>` tags. */
disableLinks?: boolean; disableLinks?: boolean;
i18n: Localizer;
} }
const renderNewLines: RenderTextCallback = ({ 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 * configurable with their `renderXXX` props, this component will assemble all three of
* them for you. * them for you.
*/ */
export class MessageBody extends React.Component<Props, {}> { export class MessageBody extends React.Component<Props> {
public render() { public render() {
const { text, disableJumbomoji, disableLinks } = this.props; const { text, disableJumbomoji, disableLinks, i18n } = this.props;
const sizeClass = disableJumbomoji ? '' : getSizeClass(text); const sizeClass = disableJumbomoji ? '' : getSizeClass(text);
return ( return (
@ -40,6 +41,7 @@ export class MessageBody extends React.Component<Props, {}> {
text={text} text={text}
sizeClass={sizeClass} sizeClass={sizeClass}
renderNonEmoji={disableLinks ? renderNewLines : renderLinks} renderNonEmoji={disableLinks ? renderNewLines : renderLinks}
i18n={i18n}
/> />
); );
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@
i18n={window.i18n} i18n={window.i18n}
media={[]} media={[]}
documents={[]} documents={[]}
i18n={util.i18n}
/> />
</div> </div>
``` ```
@ -72,7 +73,7 @@ const messages = _.sortBy(
message => -message.received_at 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 ## Media gallery with one document
@ -83,5 +84,5 @@ const messages = [
attachments: [{ fileName: 'foo.jpg', contentType: 'application/json' }], 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; onSelect?: (event: TabSelectEvent) => void;
type: AttachmentType; type: AttachmentType;
}) => { }) => {
const handleClick = onSelect ? () => onSelect({ type }) : undefined; const handleClick = onSelect
? () => {
onSelect({ type });
}
: undefined;
return ( return (
<div <div
style={isSelected ? styles.tab.active : styles.tab.default} style={isSelected ? styles.tab.active : styles.tab.default}
onClick={handleClick} onClick={handleClick}
role="tab"
> >
{label} {label}
</div> </div>
@ -146,6 +151,7 @@ export class MediaGallery extends React.Component<Props, State> {
throw missingCaseError(type); throw missingCaseError(type);
} }
})(); })();
return <EmptyState data-test="EmptyState" label={label} />; return <EmptyState data-test="EmptyState" label={label} />;
} }
@ -157,6 +163,7 @@ export class MediaGallery extends React.Component<Props, State> {
section.type === 'yearMonth' section.type === 'yearMonth'
? date.format(MONTH_FORMAT) ? date.format(MONTH_FORMAT)
: i18n(section.type); : i18n(section.type);
return ( return (
<AttachmentSection <AttachmentSection
key={header} 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() { public renderContent() {
const { message } = this.props; const { message } = this.props;
@ -46,7 +46,7 @@ export class MediaGridItem extends React.Component<Props, {}> {
public render() { public render() {
return ( return (
<div style={styles.container} onClick={this.props.onClick}> <div style={styles.container} role="button" onClick={this.props.onClick}>
{this.renderContent()} {this.renderContent()}
</div> </div>
); );

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,5 +4,5 @@ export interface Collection<T> {
models: Array<Model<T>>; models: Array<Model<T>>;
// tslint:disable-next-line no-misused-new // tslint:disable-next-line no-misused-new
new (): Collection<T>; 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 }); const blob = new Blob([data], { type });
return URL.createObjectURL(blob); return URL.createObjectURL(blob);
}; };

View file

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

View file

@ -1,6 +1,6 @@
{ {
"defaultSeverity": "error", "defaultSeverity": "error",
"extends": ["tslint:recommended", "tslint-react"], "extends": ["tslint:recommended", "tslint-react", "tslint-microsoft-contrib"],
"jsRules": {}, "jsRules": {},
"rules": { "rules": {
"align": [ "align": [
@ -35,6 +35,9 @@
"mocha-no-side-effect-code": false, "mocha-no-side-effect-code": false,
"mocha-unneeded-done": true, "mocha-unneeded-done": true,
// We always want 'as Type'
"no-angle-bracket-type-assertion": true,
"no-consecutive-blank-lines": [true, 2], "no-consecutive-blank-lines": [true, 2],
"object-literal-key-quotes": [true, "as-needed"], "object-literal-key-quotes": [true, "as-needed"],
"object-literal-sort-keys": false, "object-literal-sort-keys": false,
@ -73,7 +76,54 @@
}, },
"esSpecCompliant": true "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"] "rulesDirectory": ["node_modules/tslint-microsoft-contrib"]
} }