From 2988da098132d5e5c47c6fa1e9f84afe559f369c Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Tue, 22 May 2018 12:31:43 -0700 Subject: [PATCH] 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 --- _locales/en/messages.json | 26 +++++++++ package.json | 2 +- ts/backbone/views/Lightbox.ts | 2 + ts/components/Lightbox.md | 17 +++++- ts/components/Lightbox.tsx | 32 ++++++++--- ts/components/LightboxGallery.md | 2 +- ts/components/LightboxGallery.tsx | 6 ++- ts/components/conversation/AddNewLines.tsx | 2 +- ts/components/conversation/ContactDetail.tsx | 4 +- ts/components/conversation/ContactName.tsx | 11 ++-- .../conversation/ConversationTitle.tsx | 6 +-- .../conversation/EmbeddedContact.tsx | 17 +++--- ts/components/conversation/Emojify.md | 26 +++++---- ts/components/conversation/Emojify.tsx | 16 +++--- ts/components/conversation/Linkify.tsx | 6 +-- ts/components/conversation/Message.tsx | 4 +- ts/components/conversation/MessageBody.md | 21 ++++---- ts/components/conversation/MessageBody.tsx | 8 +-- ts/components/conversation/Quote.tsx | 45 +++++++++------- .../media-gallery/AttachmentSection.md | 7 ++- .../media-gallery/AttachmentSection.tsx | 2 +- .../media-gallery/DocumentListItem.md | 3 ++ .../media-gallery/DocumentListItem.tsx | 17 ++++-- .../conversation/media-gallery/EmptyState.tsx | 3 +- .../media-gallery/MediaGallery.md | 5 +- .../media-gallery/MediaGallery.tsx | 9 +++- .../media-gallery/MediaGridItem.tsx | 4 +- .../media-gallery/groupMessagesByDate.ts | 2 + .../media-gallery/types/Message.tsx | 1 + ts/components/utility/BackboneWrapper.tsx | 7 ++- ts/html/index.ts | 1 + ts/styleguide/ConversationContext.tsx | 6 +-- ts/styleguide/StyleGuideUtil.ts | 13 ++--- ts/styleguide/tslint.json | 11 ++++ ...sByDate.ts => groupMessagesByDate_test.ts} | 2 - ts/test/html/index_test.ts | 2 +- ts/test/tslint.json | 11 ++++ ts/test/types/Attachment_test.ts | 4 -- ts/test/types/Contact_test.ts | 1 - ts/test/types/Conversation_test.ts | 1 - ts/test/types/Settings_test.ts | 4 +- .../initializeAttachmentMetadata_test.ts | 1 - ts/types/Attachment.ts | 2 +- ts/types/Contact.ts | 1 + ts/types/Message.ts | 1 + ts/types/backbone/Collection.ts | 2 +- ts/util/arrayBufferToObjectURL.ts | 1 + ts/util/emoji.ts | 3 ++ tslint.json | 54 ++++++++++++++++++- 49 files changed, 311 insertions(+), 123 deletions(-) create mode 100644 ts/styleguide/tslint.json rename ts/test/components/media-gallery/{groupMessagesByDate.ts => groupMessagesByDate_test.ts} (99%) create mode 100644 ts/test/tslint.json diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 8f641d683..cb9380c20 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" diff --git a/package.json b/package.json index 9d0d193cc..904b7d784 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/ts/backbone/views/Lightbox.ts b/ts/backbone/views/Lightbox.ts index b245e5253..011fec513 100644 --- a/ts/backbone/views/Lightbox.ts +++ b/ts/backbone/views/Lightbox.ts @@ -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'; }; diff --git a/ts/components/Lightbox.md b/ts/components/Lightbox.md index 0840244f1..b776e5750 100644 --- a/ts/components/Lightbox.md +++ b/ts/components/Lightbox.md @@ -8,6 +8,7 @@ const noop = () => {}; objectURL="https://placekitten.com/800/600" contentType="image/jpeg" onSave={noop} + i18n={util.i18n} /> ; ``` @@ -18,7 +19,12 @@ const noop = () => {}; const noop = () => {};
- +
; ``` @@ -32,6 +38,7 @@ const noop = () => {}; objectURL="fixtures/pixabay-Soap-Bubble-7141.mp4" contentType="video/mp4" onSave={noop} + i18n={util.i18n} /> ; ``` @@ -42,7 +49,12 @@ const noop = () => {}; const noop = () => {};
- +
; ``` @@ -56,6 +68,7 @@ const noop = () => {}; objectURL="tsconfig.json" contentType="application/json" onSave={noop} + i18n={util.i18n} /> ; ``` diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index 73a8ebf7e..33f0bd55f 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -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 { +export class Lightbox extends React.Component { private containerRef: HTMLDivElement | null = null; public componentDidMount() { @@ -145,18 +152,27 @@ export class Lightbox extends React.Component { } public render() { - const { contentType, objectURL, onNext, onPrevious, onSave } = this.props; + const { + contentType, + objectURL, + onNext, + onPrevious, + onSave, + i18n, + } = this.props; + return (
{!is.undefined(contentType) - ? this.renderObject({ objectURL, contentType }) + ? this.renderObject({ objectURL, contentType, i18n }) : null}
@@ -189,14 +205,17 @@ export class Lightbox extends React.Component { private renderObject = ({ objectURL, contentType, + i18n, }: { objectURL: string; contentType: MIME.MIMEType; + i18n: Localizer; }) => { const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType); if (isImageTypeSupported) { return ( {i18n('lightboxImageAlt')} { // tslint:disable-next-line no-console console.log('Lightbox: Unexpected content type', { contentType }); + return ; }; @@ -245,11 +265,10 @@ export class Lightbox extends React.Component { }; 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 { break; default: - break; } }; diff --git a/ts/components/LightboxGallery.md b/ts/components/LightboxGallery.md index 4c5f71063..7fff37fca 100644 --- a/ts/components/LightboxGallery.md +++ b/ts/components/LightboxGallery.md @@ -33,6 +33,6 @@ const messages = [ ];
- +
; ``` diff --git a/ts/components/LightboxGallery.tsx b/ts/components/LightboxGallery.tsx index 39b7b4eb0..7d83c3492 100644 --- a/ts/components/LightboxGallery.tsx +++ b/ts/components/LightboxGallery.tsx @@ -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; onSave?: ({ message }: { message: Message }) => void; selectedIndex: number; @@ -42,7 +45,7 @@ export class LightboxGallery extends React.Component { } 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 { onSave={onSave ? this.handleSave : undefined} objectURL={objectURL} contentType={selectedItem.contentType} + i18n={i18n} /> ); } diff --git a/ts/components/conversation/AddNewLines.tsx b/ts/components/conversation/AddNewLines.tsx index b4bdfe135..12996d059 100644 --- a/ts/components/conversation/AddNewLines.tsx +++ b/ts/components/conversation/AddNewLines.tsx @@ -8,7 +8,7 @@ interface Props { renderNonNewLine?: RenderTextCallback; } -export class AddNewLines extends React.Component { +export class AddNewLines extends React.Component { public static defaultProps: Partial = { renderNonNewLine: ({ text, key }) => {text}, }; diff --git a/ts/components/conversation/ContactDetail.tsx b/ts/components/conversation/ContactDetail.tsx index 1ea63b65a..fced9c340 100644 --- a/ts/components/conversation/ContactDetail.tsx +++ b/ts/components/conversation/ContactDetail.tsx @@ -69,7 +69,7 @@ function getLabelForAddress(address: PostalAddress, i18n: Localizer): string { } } -export class ContactDetail extends React.Component { +export class ContactDetail extends React.Component { public renderEmail(items: Array | undefined, i18n: Localizer) { if (!items || items.length === 0) { return; @@ -159,7 +159,7 @@ export class ContactDetail extends React.Component { return (
- {renderAvatar(contact)} + {renderAvatar(contact, i18n)} {renderName(contact)} {renderContactShorthand(contact)} {renderSendMessage({ hasSignalAccount, i18n, onSendMessage })} diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index d73df7c06..880d86506 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -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 { +export class ContactName extends React.Component { public render() { - const { phoneNumber, name, profileName } = this.props; + const { phoneNumber, name, profileName, i18n } = this.props; const title = name ? name : phoneNumber; const profileElement = profileName && !name ? ( - ~ + ~ ) : null; return ( - {profileElement} + {profileElement} ); } diff --git a/ts/components/conversation/ConversationTitle.tsx b/ts/components/conversation/ConversationTitle.tsx index 7cf6ee941..905435fea 100644 --- a/ts/components/conversation/ConversationTitle.tsx +++ b/ts/components/conversation/ConversationTitle.tsx @@ -11,7 +11,7 @@ interface Props { profileName?: string; } -export class ConversationTitle extends React.Component { +export class ConversationTitle extends React.Component { public render() { const { name, phoneNumber, i18n, profileName, isVerified } = this.props; @@ -19,7 +19,7 @@ export class ConversationTitle extends React.Component { {name ? ( - + ) : null} {phoneNumber ? ( @@ -27,7 +27,7 @@ export class ConversationTitle extends React.Component { ) : null}{' '} {profileName ? ( - + ) : null} {isVerified ? ( diff --git a/ts/components/conversation/EmbeddedContact.tsx b/ts/components/conversation/EmbeddedContact.tsx index 2c2246e99..d6d788c7d 100644 --- a/ts/components/conversation/EmbeddedContact.tsx +++ b/ts/components/conversation/EmbeddedContact.tsx @@ -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; + i18n: Localizer; onSendMessage: () => void; onOpenContact: () => void; } -export class EmbeddedContact extends React.Component { +export class EmbeddedContact extends React.Component { public render() { const { contact, @@ -20,9 +22,9 @@ export class EmbeddedContact extends React.Component { } = this.props; return ( -
+
- {renderAvatar(contact)} + {renderAvatar(contact, i18n)}
{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 (
{initials}
@@ -56,7 +59,7 @@ export function renderAvatar(contact: Contact) { return (
- + {i18n('contactAvatarAlt')}
); } @@ -92,7 +95,7 @@ export function renderSendMessage(props: { }; return ( -
+