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
|
@ -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() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue