From 372aa44e496e58d6e7b0bcf74ca28c1ff4f221fe Mon Sep 17 00:00:00 2001 From: Chris Svenningsen Date: Mon, 14 Sep 2020 12:51:27 -0700 Subject: [PATCH] Migrate conversations to ESLint --- .eslintignore | 3 +- .eslintrc.js | 2 + _locales/en/messages.json | 18 +- ts/components/conversation/AddNewLines.tsx | 21 +- .../conversation/AttachmentList.stories.tsx | 4 +- ts/components/conversation/AttachmentList.tsx | 144 +++--- .../conversation/CallingNotification.tsx | 43 +- .../conversation/ContactDetail.stories.tsx | 4 +- ts/components/conversation/ContactDetail.tsx | 48 +- .../conversation/ContactName.stories.tsx | 5 +- ts/components/conversation/ContactName.tsx | 19 +- .../ConversationHeader.stories.tsx | 6 +- .../conversation/ConversationHeader.tsx | 61 ++- .../conversation/ConversationHero.stories.tsx | 6 +- .../conversation/ConversationHero.tsx | 18 +- .../conversation/EmbeddedContact.stories.tsx | 4 +- .../conversation/EmbeddedContact.tsx | 3 +- ts/components/conversation/Emojify.tsx | 21 +- ts/components/conversation/ExpireTimer.tsx | 10 +- .../conversation/GroupNotification.tsx | 15 +- .../conversation/GroupV2Change.stories.tsx | 20 +- ts/components/conversation/GroupV2Change.tsx | 2 + ts/components/conversation/Image.stories.tsx | 8 +- ts/components/conversation/Image.tsx | 18 +- .../conversation/ImageGrid.stories.tsx | 4 +- ts/components/conversation/ImageGrid.tsx | 461 +++++++++--------- .../InlineNotificationWrapper.tsx | 18 +- .../LastSeenIndicator.stories.tsx | 4 +- .../conversation/LastSeenIndicator.tsx | 28 +- ts/components/conversation/Linkify.tsx | 21 +- .../conversation/Message.stories.tsx | 7 +- ts/components/conversation/Message.tsx | 324 ++++++------ .../conversation/MessageBody.stories.tsx | 4 +- ts/components/conversation/MessageBody.tsx | 2 +- .../conversation/MessageDetail.stories.tsx | 5 +- ts/components/conversation/MessageDetail.tsx | 31 +- .../MessageRequestActions.stories.tsx | 8 +- .../conversation/MessageRequestActions.tsx | 7 +- .../MessageRequestActionsConfirmation.tsx | 8 +- .../ProfileChangeNotification.stories.tsx | 5 +- ts/components/conversation/Quote.stories.tsx | 12 +- ts/components/conversation/Quote.tsx | 63 ++- ts/components/conversation/ReactionPicker.tsx | 2 + .../conversation/ReactionViewer.stories.tsx | 4 +- ts/components/conversation/ReactionViewer.tsx | 2 +- .../ResetSessionNotification.stories.tsx | 4 +- .../conversation/ResetSessionNotification.tsx | 16 +- .../SafetyNumberNotification.stories.tsx | 4 - .../conversation/SafetyNumberNotification.tsx | 85 ++-- .../conversation/ScrollDownButton.stories.tsx | 4 - .../conversation/ScrollDownButton.tsx | 49 +- .../StagedGenericAttachment.stories.tsx | 5 - .../conversation/StagedGenericAttachment.tsx | 59 +-- .../StagedLinkPreview.stories.tsx | 6 +- .../conversation/StagedLinkPreview.tsx | 93 ++-- .../StagedPlaceholderAttachment.stories.tsx | 4 - .../StagedPlaceholderAttachment.tsx | 28 +- .../conversation/Timeline.stories.tsx | 7 +- ts/components/conversation/Timeline.tsx | 119 +++-- .../conversation/TimelineItem.stories.tsx | 20 +- ts/components/conversation/TimelineItem.tsx | 5 +- .../conversation/TimelineLoadingRow.tsx | 11 +- .../TimerNotification.stories.tsx | 4 - .../conversation/TimerNotification.tsx | 6 +- .../conversation/Timestamp.stories.tsx | 36 +- ts/components/conversation/Timestamp.tsx | 10 +- .../conversation/TypingAnimation.stories.tsx | 4 - .../conversation/TypingAnimation.tsx | 60 +-- .../conversation/TypingBubble.stories.tsx | 4 - ts/components/conversation/TypingBubble.tsx | 6 +- .../UnsupportedMessage.stories.tsx | 4 - .../conversation/UnsupportedMessage.tsx | 109 +++-- .../VerificationNotification.stories.tsx | 6 +- .../conversation/VerificationNotification.tsx | 6 +- ts/components/conversation/_contactUtil.tsx | 6 +- .../AttachmentSection.stories.tsx | 6 +- .../media-gallery/AttachmentSection.tsx | 2 +- .../DocumentListItem.stories.tsx | 1 + .../media-gallery/DocumentListItem.tsx | 8 +- .../media-gallery/EmptyState.stories.tsx | 1 + .../conversation/media-gallery/EmptyState.tsx | 13 +- .../media-gallery/LoadingIndicator.tsx | 2 +- .../media-gallery/MediaGallery.stories.tsx | 2 - .../media-gallery/MediaGallery.tsx | 16 +- .../media-gallery/MediaGridItem.stories.tsx | 4 +- .../media-gallery/MediaGridItem.tsx | 20 +- .../media-gallery/groupMediaItemsByDate.ts | 3 + .../media-gallery/types/Message.ts | 2 + ts/util/lint/exceptions.json | 42 +- tslint.json | 1 + 90 files changed, 1261 insertions(+), 1165 deletions(-) diff --git a/.eslintignore b/.eslintignore index 9c7eff025..2a75f71bb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -33,7 +33,8 @@ webpack.config.ts sticker-creator/**/*.ts sticker-creator/**/*.tsx ts/*.ts -ts/components/conversation/** +ts/components/*.ts +ts/components/*.tsx ts/components/stickers/** ts/shims/** ts/sql/** diff --git a/.eslintrc.js b/.eslintrc.js index e6827f54e..6c355611d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -118,6 +118,8 @@ module.exports = { rules: { ...rules, 'import/no-extraneous-dependencies': 'off', + 'react/jsx-props-no-spreading': 'off', + 'react/no-array-index-key': 'off', }, }, ], diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 858f91fb1..1ea8bddc6 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -151,6 +151,10 @@ "message": "Set Up as Standalone Device", "description": "Only available on development modes, menu option to open up the standalone device setup sequence" }, + "messageContextMenuButton": { + "message": "More actions", + "description": "Label for context button next to each message" + }, "contextMenuCopyLink": { "message": "Copy Link", "description": "Shown in the context menu for a link to indicate that the user can copy the link" @@ -985,6 +989,10 @@ "theirIdentityUnknown": { "message": "You haven't exchanged any messages with this contact yet. Your safety number with them will be available after the first message." }, + "goBack": { + "message": "Go back", + "description": "Label for back button in a conversation" + }, "moreInfo": { "message": "More Info...", "description": "Shown on the drop-down menu for an individual message, takes you to message detail screen" @@ -2772,6 +2780,14 @@ "message": "Ringing...", "description": "Shown in the call screen when placing an outgoing call that is now ringing" }, + "makeOutgoingCall": { + "message": "Start a call", + "description": "Title for the call button in a conversation" + }, + "makeOutgoingVideoCall": { + "message": "Start a video call", + "description": "Title for the video call button in a conversation" + }, "callReconnecting": { "message": "Reconnecting...", "description": "Shown in the call screen when the call is reconnecting due to network issues" @@ -3574,7 +3590,7 @@ } }, "close": { - "message": "close", + "message": "Close", "description": "Generic close label" }, "previous": { diff --git a/ts/components/conversation/AddNewLines.tsx b/ts/components/conversation/AddNewLines.tsx index ca745b8fb..1caf956fb 100644 --- a/ts/components/conversation/AddNewLines.tsx +++ b/ts/components/conversation/AddNewLines.tsx @@ -13,15 +13,20 @@ export class AddNewLines extends React.Component { renderNonNewLine: ({ text }) => text, }; - public render() { + public render(): + | JSX.Element + | string + | null + | Array { const { text, renderNonNewLine } = this.props; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const results: Array = []; const FIND_NEWLINES = /\n/g; // We have to do this, because renderNonNewLine is not required in our Props object, // but it is always provided via defaultProps. if (!renderNonNewLine) { - return; + return null; } let match = FIND_NEWLINES.exec(text); @@ -35,20 +40,20 @@ export class AddNewLines extends React.Component { while (match) { if (last < match.index) { const textWithNoNewline = text.slice(last, match.index); - results.push( - renderNonNewLine({ text: textWithNoNewline, key: count++ }) - ); + count += 1; + results.push(renderNonNewLine({ text: textWithNoNewline, key: count })); } - results.push(
); + count += 1; + results.push(
); - // @ts-ignore last = FIND_NEWLINES.lastIndex; match = FIND_NEWLINES.exec(text); } if (last < text.length) { - results.push(renderNonNewLine({ text: text.slice(last), key: count++ })); + count += 1; + results.push(renderNonNewLine({ text: text.slice(last), key: count })); } return results; diff --git a/ts/components/conversation/AttachmentList.stories.tsx b/ts/components/conversation/AttachmentList.stories.tsx index 54cf923f7..d69ac93bf 100644 --- a/ts/components/conversation/AttachmentList.stories.tsx +++ b/ts/components/conversation/AttachmentList.stories.tsx @@ -11,11 +11,9 @@ import { MIMEType, VIDEO_MP4, } from '../../types/MIME'; - -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; + const i18n = setupI18n('en', enMessages); const story = storiesOf('Components/Conversation/AttachmentList', module); diff --git a/ts/components/conversation/AttachmentList.tsx b/ts/components/conversation/AttachmentList.tsx index 14f155c79..fddabcea8 100644 --- a/ts/components/conversation/AttachmentList.tsx +++ b/ts/components/conversation/AttachmentList.tsx @@ -27,89 +27,81 @@ export interface Props { const IMAGE_WIDTH = 120; const IMAGE_HEIGHT = 120; -export class AttachmentList extends React.Component { - // tslint:disable-next-line max-func-body-length */ - public render() { - const { - attachments, - i18n, - onAddAttachment, - onClickAttachment, - onCloseAttachment, - onClose, - } = this.props; +export const AttachmentList = ({ + attachments, + i18n, + onAddAttachment, + onClickAttachment, + onCloseAttachment, + onClose, +}: Props): JSX.Element | null => { + if (!attachments.length) { + return null; + } - if (!attachments.length) { - return null; - } + const allVisualAttachments = areAllAttachmentsVisual(attachments); - const allVisualAttachments = areAllAttachmentsVisual(attachments); - - return ( -
- {attachments.length > 1 ? ( -
-
- ) : null} -
- {(attachments || []).map((attachment, index) => { - const { contentType } = attachment; - if ( - isImageTypeSupported(contentType) || - isVideoTypeSupported(contentType) - ) { - const imageKey = - getUrl(attachment) || attachment.fileName || index; - const clickCallback = - attachments.length > 1 ? onClickAttachment : undefined; - - return ( - {i18n('stagedImageAttachment', { - onCloseAttachment(attachment); - }} - /> - ); - } - - const genericKey = - getUrl(attachment) || attachment.fileName || index; + return ( +
+ {attachments.length > 1 ? ( +
+
+ ) : null} +
+ {(attachments || []).map((attachment, index) => { + const { contentType } = attachment; + if ( + isImageTypeSupported(contentType) || + isVideoTypeSupported(contentType) + ) { + const imageKey = getUrl(attachment) || attachment.fileName || index; + const clickCallback = + attachments.length > 1 ? onClickAttachment : undefined; return ( - { + onCloseAttachment(attachment); + }} /> ); - })} - {allVisualAttachments ? ( - - ) : null} -
+ ); + })} + {allVisualAttachments ? ( + + ) : null}
- ); - } -} +
+ ); +}; diff --git a/ts/components/conversation/CallingNotification.tsx b/ts/components/conversation/CallingNotification.tsx index bc81ef43f..167ec8205 100644 --- a/ts/components/conversation/CallingNotification.tsx +++ b/ts/components/conversation/CallingNotification.tsx @@ -31,37 +31,30 @@ export function getCallingNotificationText( if (wasDeclined) { if (wasVideoCall) { return i18n('declinedIncomingVideoCall'); - } else { - return i18n('declinedIncomingAudioCall'); - } - } else if (wasAccepted) { - if (wasVideoCall) { - return i18n('acceptedIncomingVideoCall'); - } else { - return i18n('acceptedIncomingAudioCall'); - } - } else { - if (wasVideoCall) { - return i18n('missedIncomingVideoCall'); - } else { - return i18n('missedIncomingAudioCall'); } + return i18n('declinedIncomingAudioCall'); } - } else { if (wasAccepted) { if (wasVideoCall) { - return i18n('acceptedOutgoingVideoCall'); - } else { - return i18n('acceptedOutgoingAudioCall'); - } - } else { - if (wasVideoCall) { - return i18n('missedOrDeclinedOutgoingVideoCall'); - } else { - return i18n('missedOrDeclinedOutgoingAudioCall'); + return i18n('acceptedIncomingVideoCall'); } + return i18n('acceptedIncomingAudioCall'); } + if (wasVideoCall) { + return i18n('missedIncomingVideoCall'); + } + return i18n('missedIncomingAudioCall'); } + if (wasAccepted) { + if (wasVideoCall) { + return i18n('acceptedOutgoingVideoCall'); + } + return i18n('acceptedOutgoingAudioCall'); + } + if (wasVideoCall) { + return i18n('missedOrDeclinedOutgoingVideoCall'); + } + return i18n('missedOrDeclinedOutgoingAudioCall'); } export const CallingNotification = (props: Props): JSX.Element | null => { @@ -81,7 +74,7 @@ export const CallingNotification = (props: Props): JSX.Element | null => { { + // eslint-disable-next-line class-methods-use-this public renderSendMessage({ hasSignalAccount, i18n, @@ -80,20 +81,24 @@ export class ContactDetail extends React.Component { hasSignalAccount: boolean; i18n: (key: string, values?: Array) => string; onSendMessage: () => void; - }) { + }): JSX.Element | null { if (!hasSignalAccount) { return null; } // We don't want the overall click handler for this element to fire, so we stop // propagation before handing control to the caller's callback. - const onClick = (e: React.MouseEvent<{}>): void => { + const onClick = (e: React.MouseEvent): void => { e.stopPropagation(); onSendMessage(); }; return ( -
); + /* eslint-enable no-nested-ternary */ } } diff --git a/ts/components/conversation/ImageGrid.stories.tsx b/ts/components/conversation/ImageGrid.stories.tsx index 990b5f79c..169c33a90 100644 --- a/ts/components/conversation/ImageGrid.stories.tsx +++ b/ts/components/conversation/ImageGrid.stories.tsx @@ -13,12 +13,10 @@ import { MIMEType, VIDEO_MP4, } from '../../types/MIME'; - -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; import { pngUrl, squareStickerUrl } from '../../storybook/Fixtures'; + const i18n = setupI18n('en', enMessages); const story = storiesOf('Components/Conversation/ImageGrid', module); diff --git a/ts/components/conversation/ImageGrid.tsx b/ts/components/conversation/ImageGrid.tsx index 78819d2f2..b78f04e23 100644 --- a/ts/components/conversation/ImageGrid.tsx +++ b/ts/components/conversation/ImageGrid.tsx @@ -30,241 +30,162 @@ export interface Props { onClick?: (attachment: AttachmentType) => void; } -export class ImageGrid extends React.Component { - // tslint:disable-next-line max-func-body-length */ - public render() { - const { - attachments, - bottomOverlay, - i18n, - isSticker, - stickerSize, - onError, - onClick, - tabIndex, - withContentAbove, - withContentBelow, - } = this.props; +export const ImageGrid = ({ + attachments, + bottomOverlay, + i18n, + isSticker, + stickerSize, + onError, + onClick, + tabIndex, + withContentAbove, + withContentBelow, +}: Props): JSX.Element | null => { + const curveTopLeft = !withContentAbove; + const curveTopRight = curveTopLeft; - const curveTopLeft = !Boolean(withContentAbove); - const curveTopRight = curveTopLeft; + const curveBottom = !withContentBelow; + const curveBottomLeft = curveBottom; + const curveBottomRight = curveBottom; - const curveBottom = !Boolean(withContentBelow); - const curveBottomLeft = curveBottom; - const curveBottomRight = curveBottom; + const withBottomOverlay = Boolean(bottomOverlay && curveBottom); - const withBottomOverlay = Boolean(bottomOverlay && curveBottom); + if (!attachments || !attachments.length) { + return null; + } - if (!attachments || !attachments.length) { - return null; - } + if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) { + const { height, width } = getImageDimensions( + attachments[0], + isSticker ? stickerSize : undefined + ); - if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) { - const { height, width } = getImageDimensions( - attachments[0], - isSticker ? stickerSize : undefined - ); + return ( +
+ {getAlt(attachments[0], +
+ ); + } - return ( -
- {getAlt(attachments[0], -
- ); - } + if (attachments.length === 2) { + return ( +
+ {getAlt(attachments[0], + {getAlt(attachments[1], +
+ ); + } - if (attachments.length === 2) { - return ( -
- {getAlt(attachments[0], + if (attachments.length === 3) { + return ( +
+ {getAlt(attachments[0], +
{getAlt(attachments[1], -
- ); - } - - if (attachments.length === 3) { - return ( -
{getAlt(attachments[0], -
- {getAlt(attachments[1], - {getAlt(attachments[2], -
- ); - } - - if (attachments.length === 4) { - return ( -
-
-
- {getAlt(attachments[0], - {getAlt(attachments[1], -
-
- {getAlt(attachments[2], - {getAlt(attachments[3], -
-
-
- ); - } - - const moreMessagesOverlay = attachments.length > 5; - const moreMessagesOverlayText = moreMessagesOverlay - ? `+${attachments.length - 5}` - : undefined; +
+ ); + } + if (attachments.length === 4) { return (
@@ -274,6 +195,7 @@ export class ImageGrid extends React.Component { i18n={i18n} blurHash={attachments[0].blurHash} curveTopLeft={curveTopLeft} + noBorder={false} attachment={attachments[0]} playIconOverlay={isVideoAttachment(attachments[0])} height={149} @@ -288,6 +210,7 @@ export class ImageGrid extends React.Component { blurHash={attachments[1].blurHash} curveTopRight={curveTopRight} playIconOverlay={isVideoAttachment(attachments[1])} + noBorder={false} height={149} width={149} attachment={attachments[1]} @@ -302,11 +225,11 @@ export class ImageGrid extends React.Component { i18n={i18n} blurHash={attachments[2].blurHash} bottomOverlay={withBottomOverlay} - noBorder={isSticker} + noBorder={false} curveBottomLeft={curveBottomLeft} playIconOverlay={isVideoAttachment(attachments[2])} - height={99} - width={99} + height={149} + width={149} attachment={attachments[2]} url={getThumbnailUrl(attachments[2])} onClick={onClick} @@ -317,35 +240,107 @@ export class ImageGrid extends React.Component { i18n={i18n} blurHash={attachments[3].blurHash} bottomOverlay={withBottomOverlay} - noBorder={isSticker} + noBorder={false} + curveBottomRight={curveBottomRight} playIconOverlay={isVideoAttachment(attachments[3])} - height={99} - width={98} + height={149} + width={149} attachment={attachments[3]} url={getThumbnailUrl(attachments[3])} onClick={onClick} onError={onError} /> - {getAlt(attachments[4],
); } -} + + const moreMessagesOverlay = attachments.length > 5; + const moreMessagesOverlayText = moreMessagesOverlay + ? `+${attachments.length - 5}` + : undefined; + + return ( +
+
+
+ {getAlt(attachments[0], + {getAlt(attachments[1], +
+
+ {getAlt(attachments[2], + {getAlt(attachments[3], + {getAlt(attachments[4], +
+
+
+ ); +}; diff --git a/ts/components/conversation/InlineNotificationWrapper.tsx b/ts/components/conversation/InlineNotificationWrapper.tsx index 266423942..ac7edc3cb 100644 --- a/ts/components/conversation/InlineNotificationWrapper.tsx +++ b/ts/components/conversation/InlineNotificationWrapper.tsx @@ -10,7 +10,7 @@ export type PropsType = { export class InlineNotificationWrapper extends React.Component { public focusRef: React.RefObject = React.createRef(); - public setFocus = () => { + public setFocus = (): void => { const container = this.focusRef.current; if (container && !container.contains(document.activeElement)) { @@ -18,14 +18,15 @@ export class InlineNotificationWrapper extends React.Component { } }; - public handleFocus = () => { + public handleFocus = (): void => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (window.getInteractionMode() === 'keyboard') { this.setSelected(); } }; - public setSelected = () => { + public setSelected = (): void => { const { id, conversationId, selectMessage } = this.props; if (selectMessage) { @@ -33,25 +34,28 @@ export class InlineNotificationWrapper extends React.Component { } }; - public componentDidMount() { + public componentDidMount(): void { const { isSelected } = this.props; if (isSelected) { this.setFocus(); } } - public componentDidUpdate(prevProps: PropsType) { - if (!prevProps.isSelected && this.props.isSelected) { + public componentDidUpdate(prevProps: PropsType): void { + const { isSelected } = this.props; + + if (!prevProps.isSelected && isSelected) { this.setFocus(); } } - public render() { + public render(): JSX.Element { const { children } = this.props; return (
{ - public render() { - const { count, i18n } = this.props; +export const LastSeenIndicator = ({ count, i18n }: Props): JSX.Element => { + const message = + count === 1 + ? i18n('unreadMessage') + : i18n('unreadMessages', [String(count)]); - const message = - count === 1 - ? i18n('unreadMessage') - : i18n('unreadMessages', [String(count)]); - - return ( -
-
-
{message}
-
- ); - } -} + return ( +
+
+
{message}
+
+ ); +}; diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx index f357ac052..7d1ba51d5 100644 --- a/ts/components/conversation/Linkify.tsx +++ b/ts/components/conversation/Linkify.tsx @@ -20,17 +20,21 @@ export class Linkify extends React.Component { renderNonLink: ({ text }) => text, }; - public render() { + public render(): + | JSX.Element + | string + | null + | Array { const { text, renderNonLink } = this.props; const matchData = linkify.match(text) || []; - const results: Array = []; + const results: Array = []; let last = 0; let count = 1; // We have to do this, because renderNonLink is not required in our Props object, // but it is always provided via defaultProps. if (!renderNonLink) { - return; + return null; } if (matchData.length === 0) { @@ -46,18 +50,20 @@ export class Linkify extends React.Component { }) => { if (last < match.index) { const textWithNoLink = text.slice(last, match.index); - results.push(renderNonLink({ text: textWithNoLink, key: count++ })); + count += 1; + results.push(renderNonLink({ text: textWithNoLink, key: count })); } const { url, text: originalText } = match; + count += 1; if (SUPPORTED_PROTOCOLS.test(url) && !isLinkSneaky(url)) { results.push( - + {originalText} ); } else { - results.push(renderNonLink({ text: originalText, key: count++ })); + results.push(renderNonLink({ text: originalText, key: count })); } last = match.lastIndex; @@ -65,7 +71,8 @@ export class Linkify extends React.Component { ); if (last < text.length) { - results.push(renderNonLink({ text: text.slice(last), key: count++ })); + count += 1; + results.push(renderNonLink({ text: text.slice(last), key: count })); } return results; diff --git a/ts/components/conversation/Message.stories.tsx b/ts/components/conversation/Message.stories.tsx index b87e25a4c..c8c931232 100644 --- a/ts/components/conversation/Message.stories.tsx +++ b/ts/components/conversation/Message.stories.tsx @@ -15,12 +15,10 @@ import { MIMEType, VIDEO_MP4, } from '../../types/MIME'; - -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; import { pngUrl } from '../../storybook/Fixtures'; + const i18n = setupI18n('en', enMessages); const story = storiesOf('Components/Conversation/Message', module); @@ -75,7 +73,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ previews: overrideProps.previews || [], reactions: overrideProps.reactions, reactToMessage: action('reactToMessage'), - renderEmojiPicker: renderEmojiPicker, + renderEmojiPicker, replyToMessage: action('replyToMessage'), retrySend: action('retrySend'), scrollToQuotedMessage: action('scrollToQuotedMessage'), @@ -195,7 +193,6 @@ story.add('Older', () => { return renderBothDirections(props); }); -// tslint:disable-next-line:max-func-body-length story.add('Reactions', () => { const props = createProps({ text: 'Hello there from a pal!', diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 06fba11b2..b4a8c7cfc 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -3,6 +3,7 @@ import ReactDOM, { createPortal } from 'react-dom'; import classNames from 'classnames'; import Measure from 'react-measure'; import { drop, groupBy, orderBy, take } from 'lodash'; +import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; import { Manager, Popper, Reference } from 'react-popper'; import moment, { Moment } from 'moment'; @@ -24,6 +25,7 @@ import { Props as ReactionPickerProps, ReactionPicker } from './ReactionPicker'; import { Emoji } from '../emoji/Emoji'; import { + AttachmentType, canDisplayImage, getExtensionForDisplay, getGridDimensions, @@ -34,8 +36,7 @@ import { isImage, isImageAttachment, isVideo, -} from '../../../ts/types/Attachment'; -import { AttachmentType } from '../../types/Attachment'; +} from '../../types/Attachment'; import { ContactType } from '../../types/Contact'; import { getIncrement } from '../../util/timer'; @@ -43,7 +44,6 @@ import { isFileDangerous } from '../../util/isFileDangerous'; import { BodyRangesType, LocalizerType } from '../../types/Util'; import { ColorType } from '../../types/Colors'; import { createRefMerger } from '../_util'; -import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; interface Trigger { handleContextClick: (event: React.MouseEvent) => void; @@ -209,18 +209,24 @@ const EXPIRED_DELAY = 600; export class Message extends React.PureComponent { public menuTriggerRef: Trigger | undefined; + public audioRef: React.RefObject = React.createRef(); + public focusRef: React.RefObject = React.createRef(); + public reactionsContainerRef: React.RefObject< HTMLDivElement > = React.createRef(); + public reactionsContainerRefMerger = createRefMerger(); public wideMl: MediaQueryList; - public expirationCheckInterval: any; - public expiredTimeout: any; - public selectedTimeout: any; + public expirationCheckInterval: NodeJS.Timeout | undefined; + + public expiredTimeout: NodeJS.Timeout | undefined; + + public selectedTimeout: NodeJS.Timeout | undefined; public constructor(props: Props) { super(props); @@ -268,24 +274,23 @@ export class Message extends React.PureComponent { return state; } - public handleWideMlChange = (event: MediaQueryListEvent) => { + public handleWideMlChange = (event: MediaQueryListEvent): void => { this.setState({ isWide: event.matches }); }; - public captureMenuTrigger = (triggerRef: Trigger) => { + public captureMenuTrigger = (triggerRef: Trigger): void => { this.menuTriggerRef = triggerRef; }; - public showMenu = (event: React.MouseEvent) => { + public showMenu = (event: React.MouseEvent): void => { if (this.menuTriggerRef) { this.menuTriggerRef.handleContextClick(event); } }; - public handleImageError = () => { + public handleImageError = (): void => { const { id } = this.props; - // tslint:disable-next-line no-console - console.log( + window.log.info( `Message ${id}: Image failed to load; failing over to placeholder` ); this.setState({ @@ -293,7 +298,7 @@ export class Message extends React.PureComponent { }); }; - public handleFocus = () => { + public handleFocus = (): void => { const { interactionMode } = this.props; if (interactionMode === 'keyboard') { @@ -301,7 +306,7 @@ export class Message extends React.PureComponent { } }; - public setSelected = () => { + public setSelected = (): void => { const { id, conversationId, selectMessage } = this.props; if (selectMessage) { @@ -309,7 +314,7 @@ export class Message extends React.PureComponent { } }; - public setFocus = () => { + public setFocus = (): void => { const container = this.focusRef.current; if (container && !container.contains(document.activeElement)) { @@ -317,7 +322,7 @@ export class Message extends React.PureComponent { } }; - public componentDidMount() { + public componentDidMount(): void { this.startSelectedTimer(); const { isSelected } = this.props; @@ -340,7 +345,7 @@ export class Message extends React.PureComponent { }, checkFrequency); } - public componentWillUnmount() { + public componentWillUnmount(): void { if (this.selectedTimeout) { clearInterval(this.selectedTimeout); } @@ -356,18 +361,20 @@ export class Message extends React.PureComponent { this.wideMl.removeEventListener('change', this.handleWideMlChange); } - public componentDidUpdate(prevProps: Props) { + public componentDidUpdate(prevProps: Props): void { + const { isSelected } = this.props; + this.startSelectedTimer(); - if (!prevProps.isSelected && this.props.isSelected) { + if (!prevProps.isSelected && isSelected) { this.setFocus(); } this.checkExpired(); } - public startSelectedTimer() { - const { interactionMode } = this.props; + public startSelectedTimer(): void { + const { clearSelectedMessage, interactionMode } = this.props; const { isSelected } = this.state; if (interactionMode === 'keyboard' || !isSelected) { @@ -378,12 +385,12 @@ export class Message extends React.PureComponent { this.selectedTimeout = setTimeout(() => { this.selectedTimeout = undefined; this.setState({ isSelected: false }); - this.props.clearSelectedMessage(); + clearSelectedMessage(); }, SELECTED_TIMEOUT); } } - public checkExpired() { + public checkExpired(): void { const now = Date.now(); const { isExpired, expirationTimestamp, expirationLength } = this.props; @@ -408,7 +415,7 @@ export class Message extends React.PureComponent { } } - public renderTimestamp() { + public renderTimestamp(): JSX.Element { const { direction, i18n, @@ -442,6 +449,7 @@ export class Message extends React.PureComponent { i18n('sendFailed') ) : (
); - } else if (!firstAttachment.pending && isAudio(attachments)) { + } + if (!firstAttachment.pending && isAudio(attachments)) { return ( ); - } else { - const { pending, fileName, fileSize, contentType } = firstAttachment; - const extension = getExtensionForDisplay({ contentType, fileName }); - const isDangerous = isFileDangerous(fileName || ''); + } + const { pending, fileName, fileSize, contentType } = firstAttachment; + const extension = getExtensionForDisplay({ contentType, fileName }); + const isDangerous = isFileDangerous(fileName || ''); - return ( - - ); - } + )} +
+
+ {fileName} +
+
+ {fileSize} +
+
+ + ); } - // tslint:disable-next-line cyclomatic-complexity max-func-body-length - public renderPreview() { + public renderPreview(): JSX.Element | null { const { attachments, conversationType, @@ -809,6 +815,7 @@ export class Message extends React.PureComponent { return ( ); @@ -1808,7 +1843,7 @@ export class Message extends React.PureComponent { ); } - public renderContents() { + public renderContents(): JSX.Element | null { const { isTapToView, deletedForEveryone } = this.props; if (deletedForEveryone) { @@ -1837,10 +1872,9 @@ export class Message extends React.PureComponent { ); } - // tslint:disable-next-line cyclomatic-complexity max-func-body-length public handleOpen = ( event: React.KeyboardEvent | React.MouseEvent - ) => { + ): void => { const { attachments, contact, @@ -1923,10 +1957,8 @@ export class Message extends React.PureComponent { event.stopPropagation(); if (this.audioRef.current.paused) { - // tslint:disable-next-line no-floating-promises this.audioRef.current.play(); } else { - // tslint:disable-next-line no-floating-promises this.audioRef.current.pause(); } } @@ -1946,7 +1978,7 @@ export class Message extends React.PureComponent { } }; - public openGenericAttachment = (event?: React.MouseEvent) => { + public openGenericAttachment = (event?: React.MouseEvent): void => { const { attachments, downloadAttachment, timestamp } = this.props; if (event) { @@ -1969,7 +2001,7 @@ export class Message extends React.PureComponent { }); }; - public handleKeyDown = (event: React.KeyboardEvent) => { + public handleKeyDown = (event: React.KeyboardEvent): void => { // Do not allow reactions to error messages const { canReply } = this.props; @@ -1989,7 +2021,7 @@ export class Message extends React.PureComponent { this.handleOpen(event); }; - public handleClick = (event: React.MouseEvent) => { + public handleClick = (event: React.MouseEvent): void => { // We don't want clicks on body text to result in the 'default action' for the message const { text } = this.props; if (text && text.length > 0) { @@ -2008,8 +2040,7 @@ export class Message extends React.PureComponent { this.handleOpen(event); }; - // tslint:disable-next-line: cyclomatic-complexity - public renderContainer() { + public renderContainer(): JSX.Element { const { authorColor, deletedForEveryone, @@ -2061,7 +2092,7 @@ export class Message extends React.PureComponent { return ( { this.setState({ containerWidth: bounds.width }); }} @@ -2081,8 +2112,7 @@ export class Message extends React.PureComponent { ); } - // tslint:disable-next-line cyclomatic-complexity - public render() { + public render(): JSX.Element | null { const { authorPhoneNumber, attachments, diff --git a/ts/components/conversation/MessageBody.stories.tsx b/ts/components/conversation/MessageBody.stories.tsx index f000b4017..dd8cad776 100644 --- a/ts/components/conversation/MessageBody.stories.tsx +++ b/ts/components/conversation/MessageBody.stories.tsx @@ -4,11 +4,9 @@ import { boolean, text } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; import { MessageBody, Props } from './MessageBody'; - -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; + const i18n = setupI18n('en', enMessages); const story = storiesOf('Components/Conversation/MessageBody', module); diff --git a/ts/components/conversation/MessageBody.tsx b/ts/components/conversation/MessageBody.tsx index 178c97658..780037c05 100644 --- a/ts/components/conversation/MessageBody.tsx +++ b/ts/components/conversation/MessageBody.tsx @@ -94,7 +94,7 @@ export class MessageBody extends React.Component { ); } - public render() { + public render(): JSX.Element { const { bodyRanges, text, diff --git a/ts/components/conversation/MessageDetail.stories.tsx b/ts/components/conversation/MessageDetail.stories.tsx index 09098d7c1..4d933acec 100644 --- a/ts/components/conversation/MessageDetail.stories.tsx +++ b/ts/components/conversation/MessageDetail.stories.tsx @@ -6,11 +6,9 @@ import { storiesOf } from '@storybook/react'; import { Props as MessageProps } from './Message'; import { MessageDetail, Props } from './MessageDetail'; - -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; + const i18n = setupI18n('en', enMessages); const story = storiesOf('Components/Conversation/MessageDetail', module); @@ -147,6 +145,7 @@ story.add('Not Delivered', () => { text: 'A message to Max', }, }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any props.receivedAt = undefined as any; return ; diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index a9cab4c8f..a99b1f95f 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -37,10 +37,14 @@ export interface Props { i18n: LocalizerType; } +const _keyForError = (error: Error): string => { + return `${error.name}-${error.message}`; +}; + export class MessageDetail extends React.Component { private readonly focusRef = React.createRef(); - public componentDidMount() { + public componentDidMount(): void { // When this component is created, it's initially not part of the DOM, and then it's // added off-screen and animated in. This ensures that the focus takes. setTimeout(() => { @@ -50,7 +54,7 @@ export class MessageDetail extends React.Component { }); } - public renderAvatar(contact: Contact) { + public renderAvatar(contact: Contact): JSX.Element { const { i18n } = this.props; const { avatarPath, @@ -76,12 +80,13 @@ export class MessageDetail extends React.Component { ); } - public renderDeleteButton() { + public renderDeleteButton(): JSX.Element { const { i18n, message } = this.props; return (
- {errors.map((error, index) => ( -
+ {errors.map(error => ( +
{error.message}
))} @@ -151,7 +161,7 @@ export class MessageDetail extends React.Component { ); } - public renderContacts() { + public renderContacts(): JSX.Element | null { const { contacts } = this.props; if (!contacts || !contacts.length) { @@ -165,18 +175,19 @@ export class MessageDetail extends React.Component { ); } - public render() { + public render(): JSX.Element { const { errors, message, receivedAt, sentAt, i18n } = this.props; return ( + // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
- {(errors || []).map((error, index) => ( - + {(errors || []).map(error => ( + diff --git a/ts/components/conversation/MessageRequestActions.stories.tsx b/ts/components/conversation/MessageRequestActions.stories.tsx index 90fed9ae1..04d3e7fc1 100644 --- a/ts/components/conversation/MessageRequestActions.stories.tsx +++ b/ts/components/conversation/MessageRequestActions.stories.tsx @@ -2,14 +2,12 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; import { text } from '@storybook/addon-knobs'; import { action } from '@storybook/addon-actions'; + import { MessageRequestActions, Props as MessageRequestActionsProps, } from './MessageRequestActions'; - -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; const i18n = setupI18n('en', enMessages); @@ -42,7 +40,7 @@ storiesOf('Components/Conversation/MessageRequestActions', module) .add('Direct (Blocked)', () => { return (
- +
); }) @@ -56,7 +54,7 @@ storiesOf('Components/Conversation/MessageRequestActions', module) .add('Group (Blocked)', () => { return (
- +
); }); diff --git a/ts/components/conversation/MessageRequestActions.tsx b/ts/components/conversation/MessageRequestActions.tsx index c93d3b024..3d38a37d1 100644 --- a/ts/components/conversation/MessageRequestActions.tsx +++ b/ts/components/conversation/MessageRequestActions.tsx @@ -19,7 +19,6 @@ export type Props = { 'i18n' | 'state' | 'onChangeState' >; -// tslint:disable-next-line max-func-body-length export const MessageRequestActions = ({ conversationType, firstName, @@ -34,7 +33,7 @@ export const MessageRequestActions = ({ phoneNumber, profileName, title, -}: Props) => { +}: Props): JSX.Element => { const [mrState, setMrState] = React.useState(MessageRequestState.default); return ( @@ -80,6 +79,7 @@ export const MessageRequestActions = ({

{isBlocked ? ( ) : ( + return ( +
+
+
+ + + , + ]} + i18n={i18n} + />
- ); - } -} + +
+ ); +}; diff --git a/ts/components/conversation/ScrollDownButton.stories.tsx b/ts/components/conversation/ScrollDownButton.stories.tsx index 44e79440b..b5034af7f 100644 --- a/ts/components/conversation/ScrollDownButton.stories.tsx +++ b/ts/components/conversation/ScrollDownButton.stories.tsx @@ -3,12 +3,8 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { boolean } from '@storybook/addon-knobs'; -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; - -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; - import { Props, ScrollDownButton } from './ScrollDownButton'; const i18n = setupI18n('en', enMessages); diff --git a/ts/components/conversation/ScrollDownButton.tsx b/ts/components/conversation/ScrollDownButton.tsx index 7c4753cec..6a472b48b 100644 --- a/ts/components/conversation/ScrollDownButton.tsx +++ b/ts/components/conversation/ScrollDownButton.tsx @@ -12,28 +12,29 @@ export type Props = { i18n: LocalizerType; }; -export class ScrollDownButton extends React.Component { - public render() { - const { conversationId, withNewMessages, i18n, scrollDown } = this.props; - const altText = withNewMessages - ? i18n('messagesBelow') - : i18n('scrollDown'); +export const ScrollDownButton = ({ + conversationId, + withNewMessages, + i18n, + scrollDown, +}: Props): JSX.Element => { + const altText = withNewMessages ? i18n('messagesBelow') : i18n('scrollDown'); - return ( -
- -
- ); - } -} + return ( +
+ +
+ ); +}; diff --git a/ts/components/conversation/StagedGenericAttachment.stories.tsx b/ts/components/conversation/StagedGenericAttachment.stories.tsx index 907dbfaeb..62d7d4f39 100644 --- a/ts/components/conversation/StagedGenericAttachment.stories.tsx +++ b/ts/components/conversation/StagedGenericAttachment.stories.tsx @@ -5,13 +5,8 @@ import { action } from '@storybook/addon-actions'; import { AttachmentType } from '../../types/Attachment'; import { MIMEType } from '../../types/MIME'; - -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; - -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; - import { Props, StagedGenericAttachment } from './StagedGenericAttachment'; const i18n = setupI18n('en', enMessages); diff --git a/ts/components/conversation/StagedGenericAttachment.tsx b/ts/components/conversation/StagedGenericAttachment.tsx index ca95b9e4d..72569affc 100644 --- a/ts/components/conversation/StagedGenericAttachment.tsx +++ b/ts/components/conversation/StagedGenericAttachment.tsx @@ -9,33 +9,36 @@ export interface Props { i18n: LocalizerType; } -export class StagedGenericAttachment extends React.Component { - public render() { - const { attachment, onClose } = this.props; - const { fileName, contentType } = attachment; - const extension = getExtensionForDisplay({ contentType, fileName }); +export const StagedGenericAttachment = ({ + attachment, + i18n, + onClose, +}: Props): JSX.Element => { + const { fileName, contentType } = attachment; + const extension = getExtensionForDisplay({ contentType, fileName }); - return ( -
-
+ ); +}; diff --git a/ts/components/conversation/StagedLinkPreview.stories.tsx b/ts/components/conversation/StagedLinkPreview.stories.tsx index 51eaad254..cdf923442 100644 --- a/ts/components/conversation/StagedLinkPreview.stories.tsx +++ b/ts/components/conversation/StagedLinkPreview.stories.tsx @@ -5,19 +5,15 @@ import { action } from '@storybook/addon-actions'; import { AttachmentType } from '../../types/Attachment'; import { MIMEType } from '../../types/MIME'; - -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; - -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; - import { Props, StagedLinkPreview } from './StagedLinkPreview'; const i18n = setupI18n('en', enMessages); const story = storiesOf('Components/Conversation/StagedLinkPreview', module); +// eslint-disable-next-line @typescript-eslint/no-explicit-any story.addDecorator((withKnobs as any)({ escapeHTML: false })); const createAttachment = ( diff --git a/ts/components/conversation/StagedLinkPreview.tsx b/ts/components/conversation/StagedLinkPreview.tsx index ed59df2a3..4cc269a4a 100644 --- a/ts/components/conversation/StagedLinkPreview.tsx +++ b/ts/components/conversation/StagedLinkPreview.tsx @@ -16,48 +16,53 @@ export interface Props { onClose?: () => void; } -export class StagedLinkPreview extends React.Component { - public render() { - const { isLoaded, onClose, i18n, title, image, domain } = this.props; +export const StagedLinkPreview = ({ + isLoaded, + onClose, + i18n, + title, + image, + domain, +}: Props): JSX.Element => { + const isImage = image && isImageAttachment(image); - const isImage = image && isImageAttachment(image); - - return ( -
- {!isLoaded ? ( -
- {i18n('loadingPreview')} -
- ) : null} - {isLoaded && image && isImage ? ( -
- -
- ) : null} - {isLoaded ? ( -
-
{title}
-
{domain}
-
- ) : null} -
- ); - } -} + return ( +
+ {!isLoaded ? ( +
+ {i18n('loadingPreview')} +
+ ) : null} + {isLoaded && image && isImage ? ( +
+ +
+ ) : null} + {isLoaded ? ( +
+
{title}
+
{domain}
+
+ ) : null} +
+ ); +}; diff --git a/ts/components/conversation/StagedPlaceholderAttachment.stories.tsx b/ts/components/conversation/StagedPlaceholderAttachment.stories.tsx index d1cb24a2b..db9ae5717 100644 --- a/ts/components/conversation/StagedPlaceholderAttachment.stories.tsx +++ b/ts/components/conversation/StagedPlaceholderAttachment.stories.tsx @@ -2,12 +2,8 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; - -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; - import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment'; const i18n = setupI18n('en', enMessages); diff --git a/ts/components/conversation/StagedPlaceholderAttachment.tsx b/ts/components/conversation/StagedPlaceholderAttachment.tsx index 229d926ec..7f6a5dc95 100644 --- a/ts/components/conversation/StagedPlaceholderAttachment.tsx +++ b/ts/components/conversation/StagedPlaceholderAttachment.tsx @@ -6,18 +6,16 @@ interface Props { i18n: LocalizerType; } -export class StagedPlaceholderAttachment extends React.Component { - public render() { - const { i18n, onClick } = this.props; - - return ( - - ); - } -} +export const StagedPlaceholderAttachment = ({ + i18n, + onClick, +}: Props): JSX.Element => ( + +); diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index 49da99b29..3008c65d7 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -3,12 +3,8 @@ import { storiesOf } from '@storybook/react'; import { boolean, number } from '@storybook/addon-knobs'; import { action } from '@storybook/addon-actions'; -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; - -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; - import { Props, Timeline } from './Timeline'; import { TimelineItem, TimelineItemType } from './TimelineItem'; import { LastSeenIndicator } from './LastSeenIndicator'; @@ -19,7 +15,7 @@ const i18n = setupI18n('en', enMessages); const story = storiesOf('Components/Conversation/Timeline', module); -// tslint:disable-next-line +// eslint-disable-next-line const noop = () => {}; Object.assign(window, { @@ -207,6 +203,7 @@ const items: Record = { type: 'linkNotification', data: null, }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; const actions = () => ({ diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index 8ff6052db..23306f086 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -1,10 +1,11 @@ import { debounce, get, isNumber } from 'lodash'; -import React from 'react'; +import React, { CSSProperties } from 'react'; import { AutoSizer, CellMeasurer, CellMeasurerCache, List, + Grid, } from 'react-virtualized'; import { ScrollDownButton } from './ScrollDownButton'; @@ -39,7 +40,7 @@ export type PropsDataType = { type PropsHousekeepingType = { id: string; unreadCount?: number; - typingContact?: Object; + typingContact?: unknown; selectedMessageId?: string; i18n: LocalizerType; @@ -47,7 +48,7 @@ type PropsHousekeepingType = { renderItem: ( id: string, conversationId: string, - actions: Object + actions: Record ) => JSX.Element; renderLastSeenIndicator: (id: string) => JSX.Element; renderHeroRow: ( @@ -86,8 +87,8 @@ type RowRendererParamsType = { isScrolling: boolean; isVisible: boolean; key: string; - parent: Object; - style: Object; + parent: Record; + style: CSSProperties; }; type OnScrollParamsType = { scrollTop: number; @@ -134,13 +135,20 @@ export class Timeline extends React.PureComponent { defaultHeight: 64, fixedWidth: true, }); + public mostRecentWidth = 0; + public mostRecentHeight = 0; + public offsetFromBottom: number | undefined = 0; + public resizeFlag = false; - public listRef = React.createRef(); + + public listRef = React.createRef(); + public visibleRows: VisibleRowsType | undefined; - public loadCountdownTimeout: any; + + public loadCountdownTimeout: NodeJS.Timeout | null = null; constructor(props: Props) { super(props); @@ -176,9 +184,9 @@ export class Timeline extends React.PureComponent { return state; } - public getList = () => { + public getList = (): List | null => { if (!this.listRef) { - return; + return null; } const { current } = this.listRef; @@ -186,25 +194,30 @@ export class Timeline extends React.PureComponent { return current; }; - public getGrid = () => { + public getGrid = (): Grid | undefined => { const list = this.getList(); if (!list) { return; } + // eslint-disable-next-line consistent-return return list.Grid; }; - public getScrollContainer = () => { - const grid = this.getGrid(); + public getScrollContainer = (): HTMLDivElement | undefined => { + // We're using an internal variable (_scrollingContainer)) here, + // so cannot rely on the public type. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const grid: any = this.getGrid(); if (!grid) { return; } + // eslint-disable-next-line consistent-return return grid._scrollingContainer as HTMLDivElement; }; - public scrollToRow = (row: number) => { + public scrollToRow = (row: number): void => { const list = this.getList(); if (!list) { return; @@ -213,7 +226,7 @@ export class Timeline extends React.PureComponent { list.scrollToRow(row); }; - public recomputeRowHeights = (row?: number) => { + public recomputeRowHeights = (row?: number): void => { const list = this.getList(); if (!list) { return; @@ -222,7 +235,7 @@ export class Timeline extends React.PureComponent { list.recomputeRowHeights(row); }; - public onHeightOnlyChange = () => { + public onHeightOnlyChange = (): void => { const grid = this.getGrid(); const scrollContainer = this.getScrollContainer(); if (!grid || !scrollContainer) { @@ -240,13 +253,18 @@ export class Timeline extends React.PureComponent { ); const delta = newOffsetFromBottom - this.offsetFromBottom; - grid.scrollToPosition({ scrollTop: scrollContainer.scrollTop + delta }); + // TODO: DESKTOP-687 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (grid as any).scrollToPosition({ + scrollTop: scrollContainer.scrollTop + delta, + }); }; - public resize = (row?: number) => { + public resize = (row?: number): void => { this.offsetFromBottom = undefined; this.resizeFlag = false; if (isNumber(row) && row > 0) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.cellSizeCache.clearPlus(row, 0); } else { @@ -256,11 +274,11 @@ export class Timeline extends React.PureComponent { this.recomputeRowHeights(row || 0); }; - public resizeHeroRow = () => { + public resizeHeroRow = (): void => { this.resize(0); }; - public onScroll = (data: OnScrollParamsType) => { + public onScroll = (data: OnScrollParamsType): void => { // Ignore scroll events generated as react-virtualized recursively scrolls and // re-measures to get us where we want to go. if ( @@ -284,7 +302,6 @@ export class Timeline extends React.PureComponent { this.updateWithVisibleRows(); }; - // tslint:disable-next-line member-ordering public updateScrollMetrics = debounce( (data: OnScrollParamsType) => { const { clientHeight, clientWidth, scrollHeight, scrollTop } = data; @@ -337,10 +354,14 @@ export class Timeline extends React.PureComponent { ); } + // Variable collision + // eslint-disable-next-line react/destructuring-assignment if (loadCountdownStart !== this.props.loadCountdownStart) { setLoadCountdownStart(id, loadCountdownStart); } + // Variable collision + // eslint-disable-next-line react/destructuring-assignment if (isNearBottom !== this.props.isNearBottom) { setIsNearBottom(id, isNearBottom); } @@ -356,7 +377,7 @@ export class Timeline extends React.PureComponent { { maxWait: 50 } ); - public updateVisibleRows = () => { + public updateVisibleRows = (): void => { let newest; let oldest; @@ -384,6 +405,7 @@ export class Timeline extends React.PureComponent { const { id, offsetTop, offsetHeight } = child; if (!id) { + // eslint-disable-next-line no-continue continue; } @@ -403,6 +425,7 @@ export class Timeline extends React.PureComponent { const { offsetTop, id } = child; if (!id) { + // eslint-disable-next-line no-continue continue; } @@ -417,7 +440,6 @@ export class Timeline extends React.PureComponent { this.visibleRows = { newest, oldest }; }; - // tslint:disable-next-line member-ordering cyclomatic-complexity public updateWithVisibleRows = debounce( () => { const { @@ -479,7 +501,7 @@ export class Timeline extends React.PureComponent { { maxWait: 500 } ); - public loadOlderMessages = () => { + public loadOlderMessages = (): void => { const { haveOldest, isLoadingMessages, @@ -505,7 +527,7 @@ export class Timeline extends React.PureComponent { key, parent, style, - }: RowRendererParamsType) => { + }: RowRendererParamsType): JSX.Element => { const { id, haveOldest, @@ -591,7 +613,7 @@ export class Timeline extends React.PureComponent { ); }; - public fromItemIndexToRow(index: number) { + public fromItemIndexToRow(index: number): number { const { oldestUnreadIndex } = this.props; // We will always render either the hero row or the loading row @@ -604,7 +626,7 @@ export class Timeline extends React.PureComponent { return index + addition; } - public getRowCount() { + public getRowCount(): number { const { oldestUnreadIndex, typingContact } = this.props; const { items } = this.props; const itemsCount = items && items.length ? items.length : 0; @@ -639,19 +661,21 @@ export class Timeline extends React.PureComponent { return; } + // eslint-disable-next-line consistent-return return index; } - public getLastSeenIndicatorRow(props?: Props) { + public getLastSeenIndicatorRow(props?: Props): number | undefined { const { oldestUnreadIndex } = props || this.props; if (!isNumber(oldestUnreadIndex)) { return; } + // eslint-disable-next-line consistent-return return this.fromItemIndexToRow(oldestUnreadIndex) - 1; } - public getTypingBubbleRow() { + public getTypingBubbleRow(): number | undefined { const { items } = this.props; if (!items || items.length < 0) { return; @@ -659,10 +683,11 @@ export class Timeline extends React.PureComponent { const last = items.length - 1; + // eslint-disable-next-line consistent-return return this.fromItemIndexToRow(last) + 1; } - public onScrollToMessage = (messageId: string) => { + public onScrollToMessage = (messageId: string): void => { const { isLoadingMessages, items, loadAndScroll } = this.props; const index = items.findIndex(item => item === messageId); @@ -678,7 +703,7 @@ export class Timeline extends React.PureComponent { } }; - public scrollToBottom = (setFocus?: boolean) => { + public scrollToBottom = (setFocus?: boolean): void => { const { selectMessage, id, items } = this.props; if (setFocus && items && items.length > 0) { @@ -694,11 +719,11 @@ export class Timeline extends React.PureComponent { }); }; - public onClickScrollDownButton = () => { + public onClickScrollDownButton = (): void => { this.scrollDown(false); }; - public scrollDown = (setFocus?: boolean) => { + public scrollDown = (setFocus?: boolean): void => { const { haveNewest, id, @@ -746,19 +771,20 @@ export class Timeline extends React.PureComponent { } }; - public componentDidMount() { + public componentDidMount(): void { this.updateWithVisibleRows(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore window.registerForActive(this.updateWithVisibleRows); } - public componentWillUnmount() { + public componentWillUnmount(): void { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore window.unregisterForActive(this.updateWithVisibleRows); } - // tslint:disable-next-line cyclomatic-complexity max-func-body-length - public componentDidUpdate(prevProps: Props) { + public componentDidUpdate(prevProps: Props): void { const { id, clearChangedMessages, @@ -787,6 +813,8 @@ export class Timeline extends React.PureComponent { } const oneTimeScrollRow = this.getLastSeenIndicatorRow(); + // TODO: DESKTOP-688 + // eslint-disable-next-line react/no-did-update-set-state this.setState({ oneTimeScrollRow, atBottom: true, @@ -804,7 +832,9 @@ export class Timeline extends React.PureComponent { prevProps.items.length > 0 && items !== prevProps.items ) { - if (this.state.atTop) { + const { atTop } = this.state; + + if (atTop) { const oldFirstIndex = 0; const oldFirstId = prevProps.items[oldFirstIndex]; @@ -820,6 +850,8 @@ export class Timeline extends React.PureComponent { if (delta > 0) { // We're loading more new messages at the top; we want to stay at the top this.resize(); + // TODO: DESKTOP-688 + // eslint-disable-next-line react/no-did-update-set-state this.setState({ oneTimeScrollRow: newRow }); return; @@ -900,7 +932,7 @@ export class Timeline extends React.PureComponent { this.updateWithVisibleRows(); } - public getScrollTarget = () => { + public getScrollTarget = (): number | undefined => { const { oneTimeScrollRow, atBottom, propScrollToIndex } = this.state; const rowCount = this.getRowCount(); @@ -920,7 +952,7 @@ export class Timeline extends React.PureComponent { return scrollToBottom; }; - public handleBlur = (event: React.FocusEvent) => { + public handleBlur = (event: React.FocusEvent): void => { const { clearSelectedMessage } = this.props; const { currentTarget } = event; @@ -944,7 +976,7 @@ export class Timeline extends React.PureComponent { }, 0); }; - public handleKeyDown = (event: React.KeyboardEvent) => { + public handleKeyDown = (event: React.KeyboardEvent): void => { const { selectMessage, selectedMessageId, items, id } = this.props; const commandKey = get(window, 'platform') === 'darwin' && event.metaKey; const controlKey = get(window, 'platform') !== 'darwin' && event.ctrlKey; @@ -1015,12 +1047,10 @@ export class Timeline extends React.PureComponent { event.preventDefault(); event.stopPropagation(); - - return; } }; - public render() { + public render(): JSX.Element | null { const { i18n, id, items } = this.props; const { shouldShowScrollDownButton, @@ -1037,7 +1067,7 @@ export class Timeline extends React.PureComponent { return (
{ ; }) - // tslint:disable-next-line max-func-body-length .add('Notification', () => { const items = [ { @@ -173,7 +169,7 @@ storiesOf('Components/Conversation/TimelineItem', module) acceptedTime: Date.now() - 200, wasDeclined: false, wasIncoming: false, - wasVideoCall: true, + wasVideoCall: false, endedTime: Date.now(), }, }, @@ -193,8 +189,8 @@ storiesOf('Components/Conversation/TimelineItem', module) }, { type: 'callHistory', - callHistoryDetails: { - data: { + data: { + callHistoryDetails: { // declined outgoing audio wasDeclined: true, wasIncoming: false, @@ -243,20 +239,21 @@ storiesOf('Components/Conversation/TimelineItem', module) return ( <> - {items.map(item => ( - <> + {items.map((item, index) => ( +
- +
))} ); }) .add('Unknown Type', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: intentional const item = { type: 'random', @@ -268,6 +265,7 @@ storiesOf('Components/Conversation/TimelineItem', module) return ; }) .add('Missing Item', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: intentional const item = null as TimelineItemProps['item']; diff --git a/ts/components/conversation/TimelineItem.tsx b/ts/components/conversation/TimelineItem.tsx index a74b88e0c..7aa77ecb5 100644 --- a/ts/components/conversation/TimelineItem.tsx +++ b/ts/components/conversation/TimelineItem.tsx @@ -124,7 +124,7 @@ export type PropsType = PropsLocalType & Pick; export class TimelineItem extends React.PureComponent { - public render() { + public render(): JSX.Element | null { const { conversationId, id, @@ -136,8 +136,7 @@ export class TimelineItem extends React.PureComponent { } = this.props; if (!item) { - // tslint:disable-next-line:no-console - console.warn(`TimelineItem: item ${id} provided was falsey`); + window.log.warn(`TimelineItem: item ${id} provided was falsey`); return null; } diff --git a/ts/components/conversation/TimelineLoadingRow.tsx b/ts/components/conversation/TimelineLoadingRow.tsx index ea72c6044..cb4e51d3c 100644 --- a/ts/components/conversation/TimelineLoadingRow.tsx +++ b/ts/components/conversation/TimelineLoadingRow.tsx @@ -16,18 +16,15 @@ export type Props = { const FAKE_DURATION = 1000; export class TimelineLoadingRow extends React.PureComponent { - public renderContents() { + public renderContents(): JSX.Element { const { state, duration, expiresAt, onComplete } = this.props; if (state === 'idle') { const fakeExpiresAt = Date.now() - FAKE_DURATION; return ; - } else if ( - state === 'countdown' && - isNumber(duration) && - isNumber(expiresAt) - ) { + } + if (state === 'countdown' && isNumber(duration) && isNumber(expiresAt)) { return ( { return ; } - public render() { + public render(): JSX.Element { return (
{this.renderContents()}
); diff --git a/ts/components/conversation/TimerNotification.stories.tsx b/ts/components/conversation/TimerNotification.stories.tsx index 6cf98a7f9..393ce9d40 100644 --- a/ts/components/conversation/TimerNotification.stories.tsx +++ b/ts/components/conversation/TimerNotification.stories.tsx @@ -2,12 +2,8 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; import { boolean, select, text } from '@storybook/addon-knobs'; -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; - -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; - import { Props, TimerNotification } from './TimerNotification'; const i18n = setupI18n('en', enMessages); diff --git a/ts/components/conversation/TimerNotification.tsx b/ts/components/conversation/TimerNotification.tsx index adf5ac3c9..7aaea5885 100644 --- a/ts/components/conversation/TimerNotification.tsx +++ b/ts/components/conversation/TimerNotification.tsx @@ -22,7 +22,7 @@ type PropsHousekeeping = { export type Props = PropsData & PropsHousekeeping; export class TimerNotification extends React.Component { - public renderContents() { + public renderContents(): JSX.Element | string | null { const { i18n, name, @@ -71,13 +71,13 @@ export class TimerNotification extends React.Component { ? i18n('disappearingMessagesDisabledByMember') : i18n('timerSetByMember', [timespan]); default: - console.warn('TimerNotification: unsupported type provided:', type); + window.log.warn('TimerNotification: unsupported type provided:', type); return null; } } - public render() { + public render(): JSX.Element { const { timespan, disabled } = this.props; return ( diff --git a/ts/components/conversation/Timestamp.stories.tsx b/ts/components/conversation/Timestamp.stories.tsx index d965afedd..4fb5a36cb 100644 --- a/ts/components/conversation/Timestamp.stories.tsx +++ b/ts/components/conversation/Timestamp.stories.tsx @@ -2,19 +2,15 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; import { boolean, date, select, text } from '@storybook/addon-knobs'; -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; - -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; - import { Props, Timestamp } from './Timestamp'; const i18n = setupI18n('en', enMessages); const story = storiesOf('Components/Conversation/Timestamp', module); -const now = Date.now; +const { now } = Date; const seconds = (n: number) => n * 1000; const minutes = (n: number) => 60 * seconds(n); const hours = (n: number) => 60 * minutes(n); @@ -70,21 +66,23 @@ const createProps = (overrideProps: Partial = {}): Props => ({ const createTable = (overrideProps: Partial = {}) => (
{i18n('error')}
- - - - - {times().map(([description, timestamp]) => ( - - - + + + + - ))} + {times().map(([description, timestamp]) => ( + + + + + ))} +
DescriptionTimestamp
{description} - -
DescriptionTimestamp
{description} + +
); diff --git a/ts/components/conversation/Timestamp.tsx b/ts/components/conversation/Timestamp.tsx index ed72cfd54..daf82bb44 100644 --- a/ts/components/conversation/Timestamp.tsx +++ b/ts/components/conversation/Timestamp.tsx @@ -21,7 +21,7 @@ export interface Props { const UPDATE_FREQUENCY = 60 * 1000; export class Timestamp extends React.Component { - private interval: any; + private interval: NodeJS.Timeout | null; constructor(props: Props) { super(props); @@ -29,22 +29,24 @@ export class Timestamp extends React.Component { this.interval = null; } - public componentDidMount() { + public componentDidMount(): void { const update = () => { this.setState({ + // Used to trigger renders + // eslint-disable-next-line react/no-unused-state lastUpdated: Date.now(), }); }; this.interval = setInterval(update, UPDATE_FREQUENCY); } - public componentWillUnmount() { + public componentWillUnmount(): void { if (this.interval) { clearInterval(this.interval); } } - public render() { + public render(): JSX.Element | null { const { direction, i18n, diff --git a/ts/components/conversation/TypingAnimation.stories.tsx b/ts/components/conversation/TypingAnimation.stories.tsx index a577df28b..a9c106790 100644 --- a/ts/components/conversation/TypingAnimation.stories.tsx +++ b/ts/components/conversation/TypingAnimation.stories.tsx @@ -1,12 +1,8 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; - -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; - import { Props, TypingAnimation } from './TypingAnimation'; const i18n = setupI18n('en', enMessages); diff --git a/ts/components/conversation/TypingAnimation.tsx b/ts/components/conversation/TypingAnimation.tsx index 06852717a..554699468 100644 --- a/ts/components/conversation/TypingAnimation.tsx +++ b/ts/components/conversation/TypingAnimation.tsx @@ -8,36 +8,30 @@ export interface Props { color?: string; } -export class TypingAnimation extends React.Component { - public render() { - const { i18n, color } = this.props; - - return ( -
-
-
-
-
-
-
- ); - } -} +export const TypingAnimation = ({ i18n, color }: Props): JSX.Element => ( +
+
+
+
+
+
+
+); diff --git a/ts/components/conversation/TypingBubble.stories.tsx b/ts/components/conversation/TypingBubble.stories.tsx index 0ba000471..c42220d1d 100644 --- a/ts/components/conversation/TypingBubble.stories.tsx +++ b/ts/components/conversation/TypingBubble.stories.tsx @@ -2,12 +2,8 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; import { select, text } from '@storybook/addon-knobs'; -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; - -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; - import { Props, TypingBubble } from './TypingBubble'; import { Colors } from '../../types/Colors'; diff --git a/ts/components/conversation/TypingBubble.tsx b/ts/components/conversation/TypingBubble.tsx index 70919eef6..d965b7d38 100644 --- a/ts/components/conversation/TypingBubble.tsx +++ b/ts/components/conversation/TypingBubble.tsx @@ -19,7 +19,7 @@ export interface Props { } export class TypingBubble extends React.PureComponent { - public renderAvatar() { + public renderAvatar(): JSX.Element | null { const { avatarPath, color, @@ -32,7 +32,7 @@ export class TypingBubble extends React.PureComponent { } = this.props; if (conversationType !== 'group') { - return; + return null; } return ( @@ -52,7 +52,7 @@ export class TypingBubble extends React.PureComponent { ); } - public render() { + public render(): JSX.Element { const { i18n, color, conversationType } = this.props; const isGroup = conversationType === 'group'; diff --git a/ts/components/conversation/UnsupportedMessage.stories.tsx b/ts/components/conversation/UnsupportedMessage.stories.tsx index 493bfb312..7fc0de333 100644 --- a/ts/components/conversation/UnsupportedMessage.stories.tsx +++ b/ts/components/conversation/UnsupportedMessage.stories.tsx @@ -3,12 +3,8 @@ import { storiesOf } from '@storybook/react'; import { boolean, text } from '@storybook/addon-knobs'; import { action } from '@storybook/addon-actions'; -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; - -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; - import { ContactType, Props, UnsupportedMessage } from './UnsupportedMessage'; const i18n = setupI18n('en', enMessages); diff --git a/ts/components/conversation/UnsupportedMessage.tsx b/ts/components/conversation/UnsupportedMessage.tsx index 72f0c3fe5..3618fffd0 100644 --- a/ts/components/conversation/UnsupportedMessage.tsx +++ b/ts/components/conversation/UnsupportedMessage.tsx @@ -29,61 +29,62 @@ type PropsHousekeeping = { export type Props = PropsData & PropsHousekeeping & PropsActions; -export class UnsupportedMessage extends React.Component { - public render() { - const { canProcessNow, contact, i18n, downloadNewVersion } = this.props; - const { isMe } = contact; +export const UnsupportedMessage = ({ + canProcessNow, + contact, + i18n, + downloadNewVersion, +}: Props): JSX.Element => { + const { isMe } = contact; - const otherStringId = canProcessNow - ? 'Message--unsupported-message-ask-to-resend' - : 'Message--unsupported-message'; - const meStringId = canProcessNow - ? 'Message--from-me-unsupported-message-ask-to-resend' - : 'Message--from-me-unsupported-message'; - const stringId = isMe ? meStringId : otherStringId; + const otherStringId = canProcessNow + ? 'Message--unsupported-message-ask-to-resend' + : 'Message--unsupported-message'; + const meStringId = canProcessNow + ? 'Message--from-me-unsupported-message-ask-to-resend' + : 'Message--from-me-unsupported-message'; + const stringId = isMe ? meStringId : otherStringId; - return ( -
-
-
- - - , - ]} - i18n={i18n} - /> -
- {canProcessNow ? null : ( - + return ( +
+
+
+ + + , + ]} + i18n={i18n} + />
- ); - } -} + {canProcessNow ? null : ( + + )} +
+ ); +}; diff --git a/ts/components/conversation/VerificationNotification.stories.tsx b/ts/components/conversation/VerificationNotification.stories.tsx index d62ff43d6..4e8ac28f8 100644 --- a/ts/components/conversation/VerificationNotification.stories.tsx +++ b/ts/components/conversation/VerificationNotification.stories.tsx @@ -1,14 +1,10 @@ import * as React from 'react'; +import { boolean } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; -// @ts-ignore import { setup as setupI18n } from '../../../js/modules/i18n'; - -// @ts-ignore import enMessages from '../../../_locales/en/messages.json'; - import { Props, VerificationNotification } from './VerificationNotification'; -import { boolean } from '@storybook/addon-knobs'; const i18n = setupI18n('en', enMessages); diff --git a/ts/components/conversation/VerificationNotification.tsx b/ts/components/conversation/VerificationNotification.tsx index f04a1e8df..ca4454e7c 100644 --- a/ts/components/conversation/VerificationNotification.tsx +++ b/ts/components/conversation/VerificationNotification.tsx @@ -27,7 +27,7 @@ type PropsHousekeeping = { export type Props = PropsData & PropsHousekeeping; export class VerificationNotification extends React.Component { - public getStringId() { + public getStringId(): string { const { isLocal, type } = this.props; switch (type) { @@ -44,7 +44,7 @@ export class VerificationNotification extends React.Component { } } - public renderContents() { + public renderContents(): JSX.Element { const { contact, i18n } = this.props; const id = this.getStringId(); @@ -67,7 +67,7 @@ export class VerificationNotification extends React.Component { ); } - public render() { + public render(): JSX.Element { const { type } = this.props; const suffix = type === 'markVerified' ? 'mark-verified' : 'mark-not-verified'; diff --git a/ts/components/conversation/_contactUtil.tsx b/ts/components/conversation/_contactUtil.tsx index 64140493a..0f60995c2 100644 --- a/ts/components/conversation/_contactUtil.tsx +++ b/ts/components/conversation/_contactUtil.tsx @@ -19,7 +19,7 @@ export function renderAvatar({ i18n: LocalizerType; size: 28 | 52 | 80; direction?: 'outgoing' | 'incoming'; -}) { +}): JSX.Element { const { avatar } = contact; const avatarPath = avatar && avatar.avatar && avatar.avatar.path; @@ -60,7 +60,7 @@ export function renderName({ contact: ContactType; isIncoming: boolean; module: string; -}) { +}): JSX.Element { return (
{ - public render() { + public render(): JSX.Element { const { header } = this.props; return ( diff --git a/ts/components/conversation/media-gallery/DocumentListItem.stories.tsx b/ts/components/conversation/media-gallery/DocumentListItem.stories.tsx index 871809da7..406c2dc1c 100644 --- a/ts/components/conversation/media-gallery/DocumentListItem.stories.tsx +++ b/ts/components/conversation/media-gallery/DocumentListItem.stories.tsx @@ -10,6 +10,7 @@ const story = storiesOf( module ); +// eslint-disable-next-line @typescript-eslint/no-explicit-any story.addDecorator((withKnobs as any)({ escapeHTML: false })); story.add('Single', () => ( diff --git a/ts/components/conversation/media-gallery/DocumentListItem.tsx b/ts/components/conversation/media-gallery/DocumentListItem.tsx index 6d832dbf0..db9848212 100644 --- a/ts/components/conversation/media-gallery/DocumentListItem.tsx +++ b/ts/components/conversation/media-gallery/DocumentListItem.tsx @@ -2,7 +2,6 @@ import React from 'react'; import classNames from 'classnames'; import moment from 'moment'; -// tslint:disable-next-line:match-default-export-name import formatFileSize from 'filesize'; interface Props { @@ -21,7 +20,7 @@ export class DocumentListItem extends React.Component { shouldShowSeparator: true, }; - public render() { + public render(): JSX.Element { const { shouldShowSeparator } = this.props; return ( @@ -39,12 +38,13 @@ export class DocumentListItem extends React.Component { } private renderContent() { - const { fileName, fileSize, timestamp } = this.props; + const { fileName, fileSize, onClick, timestamp } = this.props; return ( ); diff --git a/ts/components/conversation/media-gallery/groupMediaItemsByDate.ts b/ts/components/conversation/media-gallery/groupMediaItemsByDate.ts index 570657992..0309d10d6 100644 --- a/ts/components/conversation/media-gallery/groupMediaItemsByDate.ts +++ b/ts/components/conversation/media-gallery/groupMediaItemsByDate.ts @@ -67,11 +67,13 @@ const toSection = ( case 'yesterday': case 'thisWeek': case 'thisMonth': + // eslint-disable-next-line consistent-return return { type: firstMediaItemWithSection.type, mediaItems, }; case 'yearMonth': + // eslint-disable-next-line consistent-return return { type: firstMediaItemWithSection.type, year: firstMediaItemWithSection.year, @@ -83,6 +85,7 @@ const toSection = ( // error TS2345: Argument of type 'any' is not assignable to parameter // of type 'never'. // return missingCaseError(firstMediaItemWithSection.type); + // eslint-disable-next-line no-useless-return return; } }; diff --git a/ts/components/conversation/media-gallery/types/Message.ts b/ts/components/conversation/media-gallery/types/Message.ts index dd773402a..565dc2c7b 100644 --- a/ts/components/conversation/media-gallery/types/Message.ts +++ b/ts/components/conversation/media-gallery/types/Message.ts @@ -3,5 +3,7 @@ import { Attachment } from '../../../../types/Attachment'; export type Message = { id: string; attachments: Array; + // Assuming this is for the API + // eslint-disable-next-line camelcase received_at: number; }; diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 732b2a0f1..eafd1569a 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -12830,18 +12830,17 @@ "path": "ts/components/CallScreen.js", "line": " this.localVideoRef = react_1.default.createRef();", "lineNumber": 98, - "reasonCategory": "usageTrusted", - "updated": "2020-05-28T17:22:06.472Z", - "reasonDetail": "Used to render local preview video" + "reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted", + "updated": "2020-09-14T23:03:44.863Z", + "reasonDetail": "" }, { "rule": "React-createRef", "path": "ts/components/CallScreen.js", "line": " this.remoteVideoRef = react_1.default.createRef();", - "lineNumber": 98, + "lineNumber": 99, "reasonCategory": "usageTrusted", - "updated": "2020-09-11T17:24:56.124Z", - "reasonDetail": "Necessary for showing call video" + "updated": "2020-09-14T23:03:44.863Z" }, { "rule": "React-createRef", @@ -12856,10 +12855,9 @@ "rule": "React-createRef", "path": "ts/components/CallScreen.tsx", "line": " this.remoteVideoRef = React.createRef();", - "lineNumber": 75, + "lineNumber": 80, "reasonCategory": "usageTrusted", - "updated": "2020-09-11T17:24:56.124Z", - "reasonDetail": "Necessary for showing call video" + "updated": "2020-09-14T23:03:44.863Z" }, { "rule": "React-createRef", @@ -12944,10 +12942,9 @@ "rule": "React-createRef", "path": "ts/components/Lightbox.js", "line": " this.videoRef = react_1.default.createRef();", - "lineNumber": 142, + "lineNumber": 149, "reasonCategory": "usageTrusted", - "updated": "2020-09-11T17:24:56.124Z", - "reasonDetail": "Used to control video" + "updated": "2020-09-14T23:03:44.863Z" }, { "rule": "React-createRef", @@ -13016,7 +13013,7 @@ "rule": "React-createRef", "path": "ts/components/conversation/ConversationHeader.tsx", "line": " this.menuTriggerRef = React.createRef();", - "lineNumber": 79, + "lineNumber": 82, "reasonCategory": "usageTrusted", "updated": "2020-05-20T20:10:43.540Z", "reasonDetail": "Used to reference popup menu" @@ -13069,24 +13066,23 @@ "rule": "React-createRef", "path": "ts/components/conversation/Message.tsx", "line": " public audioRef: React.RefObject = React.createRef();", - "lineNumber": 212, + "lineNumber": 213, "reasonCategory": "usageTrusted", - "updated": "2020-08-28T19:36:40.817Z" + "updated": "2020-09-14T23:03:44.863Z" }, { "rule": "React-createRef", "path": "ts/components/conversation/Message.tsx", "line": " public focusRef: React.RefObject = React.createRef();", - "lineNumber": 213, + "lineNumber": 215, "reasonCategory": "usageTrusted", - "updated": "2020-09-11T17:24:56.124Z", - "reasonDetail": "Used for managing focus only" + "updated": "2020-09-14T23:03:44.863Z" }, { "rule": "React-createRef", "path": "ts/components/conversation/Message.tsx", "line": " > = React.createRef();", - "lineNumber": 216, + "lineNumber": 219, "reasonCategory": "usageTrusted", "updated": "2020-08-28T19:36:40.817Z" }, @@ -13094,7 +13090,7 @@ "rule": "React-createRef", "path": "ts/components/conversation/MessageDetail.js", "line": " this.focusRef = react_1.default.createRef();", - "lineNumber": 15, + "lineNumber": 18, "reasonCategory": "usageTrusted", "updated": "2019-11-01T22:46:33.013Z", "reasonDetail": "Used for setting focus only" @@ -13112,7 +13108,7 @@ "rule": "React-createRef", "path": "ts/components/conversation/media-gallery/MediaGallery.js", "line": " this.focusRef = react_1.default.createRef();", - "lineNumber": 25, + "lineNumber": 28, "reasonCategory": "usageTrusted", "updated": "2019-11-01T22:46:33.013Z", "reasonDetail": "Used for setting focus only" @@ -13121,7 +13117,7 @@ "rule": "React-createRef", "path": "ts/components/conversation/media-gallery/MediaGallery.tsx", "line": " public readonly focusRef: React.RefObject = React.createRef();", - "lineNumber": 66, + "lineNumber": 68, "reasonCategory": "usageTrusted", "updated": "2019-11-01T22:46:33.013Z", "reasonDetail": "Used for setting focus only" @@ -13313,4 +13309,4 @@ "reasonCategory": "falseMatch", "updated": "2020-09-08T23:07:22.682Z" } -] +] \ No newline at end of file diff --git a/tslint.json b/tslint.json index a4e30c33c..cca358a99 100644 --- a/tslint.json +++ b/tslint.json @@ -181,6 +181,7 @@ "ts/backbone/**", "ts/build/**", "ts/components/*.ts[x]", + "ts/components/conversation/**", "ts/components/emoji/**", "ts/notifications/**", "ts/protobuf/**",