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:
parent
23586be6b0
commit
2988da0981
49 changed files with 311 additions and 123 deletions
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
```
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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>;
|
||||
```
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
};
|
||||
|
|
|
@ -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 })}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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}
|
||||
/>;
|
||||
```
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>,
|
||||
};
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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} />
|
||||
```
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -20,5 +20,10 @@ const messages = [
|
|||
},
|
||||
];
|
||||
|
||||
<AttachmentSection header="Today" type="documents" messages={messages} />;
|
||||
<AttachmentSection
|
||||
header="Today"
|
||||
type="documents"
|
||||
messages={messages}
|
||||
i18n={util.i18n}
|
||||
/>;
|
||||
```
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
```
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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} />;
|
||||
```
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -66,6 +66,7 @@ export const withObjectURL = (message: Message): Message => {
|
|||
data: attachment.data,
|
||||
type: attachment.contentType,
|
||||
});
|
||||
|
||||
return {
|
||||
...message,
|
||||
objectURL,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
11
ts/styleguide/tslint.json
Normal 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
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
import 'mocha';
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { shuffle } from 'lodash';
|
||||
|
|
@ -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
11
ts/test/tslint.json
Normal 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
|
||||
}
|
||||
}
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* @prettier
|
||||
*/
|
||||
import 'mocha';
|
||||
import { assert } from 'chai';
|
||||
|
||||
import * as Attachment from '../../types/Attachment';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'mocha';
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { getName } from '../../types/Contact';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'mocha';
|
||||
import { assert } from 'chai';
|
||||
|
||||
import * as Conversation from '../../types/Conversation';
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'mocha';
|
||||
import { assert } from 'chai';
|
||||
|
||||
import * as Message from '../../../../ts/types/message/initializeAttachmentMetadata';
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -85,6 +85,7 @@ export function contactSelector(
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...contact,
|
||||
avatar,
|
||||
|
|
|
@ -100,5 +100,6 @@ export const hasExpiration = (message: Message): boolean => {
|
|||
}
|
||||
|
||||
const { expireTimer } = message;
|
||||
|
||||
return typeof expireTimer === 'number' && expireTimer > 0;
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -14,5 +14,6 @@ export const arrayBufferToObjectURL = ({
|
|||
}
|
||||
|
||||
const blob = new Blob([data], { type });
|
||||
|
||||
return URL.createObjectURL(blob);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
54
tslint.json
54
tslint.json
|
@ -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"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue