diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index ea81ab11a..dc7f0379a 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -10059,9 +10059,20 @@ $contact-modal-padding: 18px; padding: 3px; } .module-delivery-issue-dialog__button { +} + +.module-delivery-issue-dialog__learn-more-button { + @include button-reset; + @include button-secondary; @include font-body-1-bold; + + border-radius: 4px; + padding: 7px 14px; +} +.module-delivery-issue-dialog__close-button { @include button-reset; @include button-primary; + @include font-body-1-bold; border-radius: 4px; padding: 7px 14px; diff --git a/ts/components/AvatarPopup.tsx b/ts/components/AvatarPopup.tsx index 3b91f5226..f1295703f 100644 --- a/ts/components/AvatarPopup.tsx +++ b/ts/components/AvatarPopup.tsx @@ -23,7 +23,6 @@ export type Props = { } & AvatarProps; export const AvatarPopup = (props: Props): JSX.Element => { - const focusRef = React.useRef(null); const { i18n, name, @@ -42,7 +41,7 @@ export const AvatarPopup = (props: Props): JSX.Element => { // Note: mechanisms to dismiss this view are all in its host, MainHeader // Focus first button after initial render, restore focus on teardown - useRestoreFocus(focusRef); + const [focusRef] = useRestoreFocus(); return (
diff --git a/ts/components/CaptchaDialog.tsx b/ts/components/CaptchaDialog.tsx index dbfdbd93e..86bb9c537 100644 --- a/ts/components/CaptchaDialog.tsx +++ b/ts/components/CaptchaDialog.tsx @@ -39,6 +39,7 @@ export function CaptchaDialog(props: Readonly): JSX.Element { moduleClassName="module-Modal" i18n={i18n} title={i18n('CaptchaDialog--can-close__title')} + onClose={() => setIsClosing(false)} >

{i18n('CaptchaDialog--can-close__body')}

diff --git a/ts/components/NeedsScreenRecordingPermissionsModal.tsx b/ts/components/NeedsScreenRecordingPermissionsModal.tsx index a3c1c1da4..bcbf0b92b 100644 --- a/ts/components/NeedsScreenRecordingPermissionsModal.tsx +++ b/ts/components/NeedsScreenRecordingPermissionsModal.tsx @@ -29,6 +29,7 @@ export const NeedsScreenRecordingPermissionsModal = ({ i18n={i18n} title={i18n('calling__presenting--permission-title')} theme={Theme.Dark} + onClose={toggleScreenRecordingPermissionsDialog} >

{i18n('calling__presenting--macos-permission-description')}

    diff --git a/ts/components/SafetyNumberChangeDialog.tsx b/ts/components/SafetyNumberChangeDialog.tsx index 70fec246c..cea73dbb1 100644 --- a/ts/components/SafetyNumberChangeDialog.tsx +++ b/ts/components/SafetyNumberChangeDialog.tsx @@ -54,7 +54,7 @@ export const SafetyNumberChangeDialog = ({ if (selectedContact) { return ( - + {renderSafetyNumber({ contactID: selectedContact.id, onClose })} ); diff --git a/ts/components/ShortcutGuide.tsx b/ts/components/ShortcutGuide.tsx index 4adf76dbf..fd1fce9e6 100644 --- a/ts/components/ShortcutGuide.tsx +++ b/ts/components/ShortcutGuide.tsx @@ -205,12 +205,11 @@ const CALLING_SHORTCUTS: Array = [ ]; export const ShortcutGuide = (props: Props): JSX.Element => { - const focusRef = React.useRef(null); const { i18n, close, hasInstalledStickers, platform } = props; const isMacOS = platform === 'darwin'; // Restore focus on teardown - useRestoreFocus(focusRef); + const [focusRef] = useRestoreFocus(); return (
    diff --git a/ts/components/conversation/ChatSessionRefreshedDialog.tsx b/ts/components/conversation/ChatSessionRefreshedDialog.tsx index 0bcda908f..6487639ec 100644 --- a/ts/components/conversation/ChatSessionRefreshedDialog.tsx +++ b/ts/components/conversation/ChatSessionRefreshedDialog.tsx @@ -6,6 +6,8 @@ import classNames from 'classnames'; import { Modal } from '../Modal'; +import { useRestoreFocus } from '../../util/hooks'; + import { LocalizerType } from '../../types/Util'; export type PropsType = { @@ -19,8 +21,11 @@ export function ChatSessionRefreshedDialog( ): React.ReactElement { const { i18n, contactSupport, onClose } = props; + // Focus first button after initial render, restore focus on teardown + const [focusRef] = useRestoreFocus(); + return ( - +
    {i18n('Confirmation--confirm')} diff --git a/ts/components/conversation/DeliveryIssueDialog.stories.tsx b/ts/components/conversation/DeliveryIssueDialog.stories.tsx index 7ddc1f9cd..6c68051b9 100644 --- a/ts/components/conversation/DeliveryIssueDialog.stories.tsx +++ b/ts/components/conversation/DeliveryIssueDialog.stories.tsx @@ -21,6 +21,7 @@ storiesOf('Components/Conversation/DeliveryIssueDialog', module).add( i18n={i18n} sender={sender} inGroup={false} + learnMoreAboutDeliveryIssue={action('learnMoreAboutDeliveryIssue')} onClose={action('onClose')} /> ); @@ -35,6 +36,7 @@ storiesOf('Components/Conversation/DeliveryIssueDialog', module).add( i18n={i18n} sender={sender} inGroup + learnMoreAboutDeliveryIssue={action('learnMoreAboutDeliveryIssue')} onClose={action('onClose')} /> ); diff --git a/ts/components/conversation/DeliveryIssueDialog.tsx b/ts/components/conversation/DeliveryIssueDialog.tsx index bae85be7f..b7da93fd4 100644 --- a/ts/components/conversation/DeliveryIssueDialog.tsx +++ b/ts/components/conversation/DeliveryIssueDialog.tsx @@ -8,24 +8,30 @@ import { Modal } from '../Modal'; import { Intl } from '../Intl'; import { Emojify } from './Emojify'; +import { useRestoreFocus } from '../../util/hooks'; + import { LocalizerType } from '../../types/Util'; export type PropsType = { i18n: LocalizerType; sender: ConversationType; inGroup: boolean; + learnMoreAboutDeliveryIssue: () => unknown; onClose: () => unknown; }; export function DeliveryIssueDialog(props: PropsType): React.ReactElement { - const { i18n, inGroup, sender, onClose } = props; + const { i18n, inGroup, learnMoreAboutDeliveryIssue, sender, onClose } = props; const key = inGroup ? 'DeliveryIssue--summary--group' : 'DeliveryIssue--summary'; + // Focus first button after initial render, restore focus on teardown + const [focusRef] = useRestoreFocus(); + return ( - +
    + diff --git a/ts/components/conversation/DeliveryIssueNotification.stories.tsx b/ts/components/conversation/DeliveryIssueNotification.stories.tsx index 38659d6dc..824a13bb9 100644 --- a/ts/components/conversation/DeliveryIssueNotification.stories.tsx +++ b/ts/components/conversation/DeliveryIssueNotification.stories.tsx @@ -6,6 +6,7 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; import { setup as setupI18n } from '../../../js/modules/i18n'; import enMessages from '../../../_locales/en/messages.json'; @@ -19,7 +20,12 @@ storiesOf('Components/Conversation/DeliveryIssueNotification', module).add( 'Default', () => { return ( - + ); } ); @@ -27,6 +33,13 @@ storiesOf('Components/Conversation/DeliveryIssueNotification', module).add( storiesOf('Components/Conversation/DeliveryIssueNotification', module).add( 'In Group', () => { - return ; + return ( + + ); } ); diff --git a/ts/components/conversation/DeliveryIssueNotification.tsx b/ts/components/conversation/DeliveryIssueNotification.tsx index 0519803c6..2235a4bb4 100644 --- a/ts/components/conversation/DeliveryIssueNotification.tsx +++ b/ts/components/conversation/DeliveryIssueNotification.tsx @@ -15,16 +15,22 @@ export type PropsDataType = { inGroup: boolean; }; +export type PropsActionsType = { + learnMoreAboutDeliveryIssue: () => unknown; +}; + type PropsHousekeepingType = { i18n: LocalizerType; }; -export type PropsType = PropsDataType & PropsHousekeepingType; +export type PropsType = PropsDataType & + PropsActionsType & + PropsHousekeepingType; export function DeliveryIssueNotification( props: PropsType ): ReactElement | null { - const { i18n, inGroup, sender } = props; + const { i18n, inGroup, sender, learnMoreAboutDeliveryIssue } = props; const [isDialogOpen, setIsDialogOpen] = useState(false); const openDialog = useCallback(() => { @@ -61,6 +67,7 @@ export function DeliveryIssueNotification( diff --git a/ts/components/conversation/ReactionPicker.tsx b/ts/components/conversation/ReactionPicker.tsx index ac9111c87..8042c6187 100644 --- a/ts/components/conversation/ReactionPicker.tsx +++ b/ts/components/conversation/ReactionPicker.tsx @@ -40,7 +40,6 @@ export const ReactionPicker = React.forwardRef( ref ) => { const [pickingOther, setPickingOther] = React.useState(false); - const focusRef = React.useRef(null); // Handle escape key React.useEffect(() => { @@ -70,7 +69,7 @@ export const ReactionPicker = React.forwardRef( ); // Focus first button and restore focus on unmount - useRestoreFocus(focusRef); + const [focusRef] = useRestoreFocus(); const otherSelected = selected && !emojis.includes(selected); diff --git a/ts/components/conversation/ReactionViewer.tsx b/ts/components/conversation/ReactionViewer.tsx index 4133b152e..2f9d81df2 100644 --- a/ts/components/conversation/ReactionViewer.tsx +++ b/ts/components/conversation/ReactionViewer.tsx @@ -124,8 +124,6 @@ export const ReactionViewer = React.forwardRef( selectedReactionCategory, setSelectedReactionCategory, ] = React.useState(pickedReaction || 'all'); - const focusRef = React.useRef(null); - // Handle escape key React.useEffect(() => { const handler = (e: KeyboardEvent) => { @@ -142,7 +140,7 @@ export const ReactionViewer = React.forwardRef( }, [onClose]); // Focus first button and restore focus on unmount - useRestoreFocus(focusRef); + const [focusRef] = useRestoreFocus(); // If we have previously selected a reaction type that is no longer present // (removed on another device, for instance) we should select another diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index d51e4a844..b0cce695e 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -304,6 +304,7 @@ const actions = () => ({ ), setLoadCountdownStart: action('setLoadCountdownStart'), setIsNearBottom: action('setIsNearBottom'), + learnMoreAboutDeliveryIssue: action('learnMoreAboutDeliveryIssue'), loadAndScroll: action('loadAndScroll'), loadOlderMessages: action('loadOlderMessages'), loadNewerMessages: action('loadNewerMessages'), diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index 9ff5acdc0..8485b3fac 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -135,6 +135,7 @@ type PropsActionsType = { }> ) => void; + learnMoreAboutDeliveryIssue: () => unknown; loadAndScroll: (messageId: string) => unknown; loadOlderMessages: (messageId: string) => unknown; loadNewerMessages: (messageId: string) => unknown; diff --git a/ts/components/conversation/TimelineItem.stories.tsx b/ts/components/conversation/TimelineItem.stories.tsx index 18d3c81c1..2b6acb29f 100644 --- a/ts/components/conversation/TimelineItem.stories.tsx +++ b/ts/components/conversation/TimelineItem.stories.tsx @@ -55,6 +55,7 @@ const getDefaultProps = () => ({ deleteMessage: action('deleteMessage'), deleteMessageForEveryone: action('deleteMessageForEveryone'), kickOffAttachmentDownload: action('kickOffAttachmentDownload'), + learnMoreAboutDeliveryIssue: action('learnMoreAboutDeliveryIssue'), markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'), showMessageDetail: action('showMessageDetail'), openConversation: action('openConversation'), diff --git a/ts/components/conversation/TimelineItem.tsx b/ts/components/conversation/TimelineItem.tsx index 3d95b9ed7..9ebb8f5e1 100644 --- a/ts/components/conversation/TimelineItem.tsx +++ b/ts/components/conversation/TimelineItem.tsx @@ -21,6 +21,7 @@ import { } from './ChatSessionRefreshedNotification'; import { DeliveryIssueNotification, + PropsActionsType as DeliveryIssueActionProps, PropsDataType as DeliveryIssueProps, } from './DeliveryIssueNotification'; import { CallingNotificationType } from '../../util/callingNotification'; @@ -156,6 +157,7 @@ type PropsLocalType = { type PropsActionsType = MessageActionsType & CallingNotificationActionsType & + DeliveryIssueActionProps & PropsChatSessionRefreshedActionsType & UnsupportedMessageActionsType & SafetyNumberActionsType; @@ -226,7 +228,9 @@ export class TimelineItem extends React.PureComponent { /> ); } else if (item.type === 'deliveryIssue') { - notification = ; + notification = ( + + ); } else if (item.type === 'linkNotification') { notification = (
    diff --git a/ts/components/stickers/StickerPicker.tsx b/ts/components/stickers/StickerPicker.tsx index 0055c5a68..e6b21a230 100644 --- a/ts/components/stickers/StickerPicker.tsx +++ b/ts/components/stickers/StickerPicker.tsx @@ -70,7 +70,6 @@ export const StickerPicker = React.memo( }: Props, ref ) => { - const focusRef = React.useRef(null); const tabIds = React.useMemo( () => ['recents', ...packs.map(({ id }) => id)], [packs] @@ -124,7 +123,7 @@ export const StickerPicker = React.memo( }, [onClose]); // Focus popup on after initial render, restore focus on teardown - useRestoreFocus(focusRef); + const [focusRef] = useRestoreFocus(); const isEmpty = stickers.length === 0; const addPackRef = isEmpty ? focusRef : undefined; diff --git a/ts/components/stickers/StickerPreviewModal.tsx b/ts/components/stickers/StickerPreviewModal.tsx index 138a4b344..1808f7ca4 100644 --- a/ts/components/stickers/StickerPreviewModal.tsx +++ b/ts/components/stickers/StickerPreviewModal.tsx @@ -76,12 +76,11 @@ export const StickerPreviewModal = React.memo((props: Props) => { installStickerPack, uninstallStickerPack, } = props; - const focusRef = React.useRef(null); const [root, setRoot] = React.useState(null); const [confirmingUninstall, setConfirmingUninstall] = React.useState(false); // Restore focus on teardown - useRestoreFocus(focusRef, root); + const [focusRef] = useRestoreFocus(); React.useEffect(() => { const div = document.createElement('div'); diff --git a/ts/util/hooks.ts b/ts/util/hooks.ts index a6f3f534a..8567a96dc 100644 --- a/ts/util/hooks.ts +++ b/ts/util/hooks.ts @@ -13,34 +13,47 @@ export function usePrevious(initialValue: T, currentValue: T): T { return result; } +type CallbackType = (toFocus: HTMLElement | null | undefined) => void; + // Restore focus on teardown -export const useRestoreFocus = ( - // The ref for the element to receive initial focus - focusRef: React.RefObject, - // Allow for an optional root element that must exist - root: boolean | HTMLElement | null = true -): void => { +export const useRestoreFocus = (): Array => { + const toFocusRef = React.useRef(null); + const lastFocusedRef = React.useRef(null); + + // We need to use a callback here because refs aren't necessarily populated on first + // render. For example, ModalHost makes a top-level parent div first, and then renders + // into it. And the children you pass it don't have access to that root div. + const setFocusRef = React.useCallback( + (toFocus: HTMLElement | null | undefined) => { + if (!toFocus) { + return; + } + + // We only want to do this once. + if (toFocusRef.current) { + return; + } + toFocusRef.current = toFocus; + + // Remember last-focused element, focus this new target element. + lastFocusedRef.current = document.activeElement as HTMLElement; + toFocus.focus(); + }, + [] + ); + React.useEffect(() => { - if (!root) { - return undefined; - } - - const lastFocused = document.activeElement as HTMLElement; - - if (focusRef.current) { - focusRef.current.focus(); - } - return () => { - // This ensures that the focus is returned to - // previous element + // On unmount, returned focus to element focused before we set the focus setTimeout(() => { - if (lastFocused && lastFocused.focus) { - lastFocused.focus(); + if (lastFocusedRef.current && lastFocusedRef.current.focus) { + lastFocusedRef.current.focus(); } }); }; - }, [focusRef, root]); + }, []); + + return [setFocusRef]; }; export const useBoundActions = ( diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 7aaf7e876..15c9da006 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -13058,6 +13058,13 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T21:59:32.770Z" }, + { + "rule": "React-useRef", + "path": "sticker-creator/components/StickerFrame.tsx", + "line": " const timerRef = React.useRef();", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "jQuery-$(", "path": "sticker-creator/util/i18n.tsx", @@ -13184,6 +13191,13 @@ "updated": "2021-01-06T00:47:54.313Z", "reasonDetail": "Needed to render remote video elements. Doesn't interact with the DOM." }, + { + "rule": "React-useRef", + "path": "ts/calling/useGetCallingFrameBuffer.ts", + "line": " const ref = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "jQuery-load(", "path": "ts/challenge.js", @@ -13235,6 +13249,20 @@ "updated": "2021-03-01T18:34:36.638Z", "reasonDetail": "Used to reference popup menu" }, + { + "rule": "React-useRef", + "path": "ts/components/AvatarInput.tsx", + "line": " const fileInputRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/AvatarInput.tsx", + "line": " const menuTriggerRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/AvatarInputContainer.js", @@ -13244,11 +13272,10 @@ }, { "rule": "React-useRef", - "path": "ts/components/AvatarPopup.js", - "line": " const focusRef = React.useRef(null);", + "path": "ts/components/AvatarInputContainer.tsx", + "line": " const startingAvatarPathRef = useRef(avatarPath);", "reasonCategory": "usageTrusted", - "updated": "2020-10-26T19:12:24.410Z", - "reasonDetail": "Only used to focus the element." + "updated": "2021-07-30T16:57:33.618Z" }, { "rule": "React-useRef", @@ -13264,6 +13291,20 @@ "reasonCategory": "usageTrusted", "updated": "2021-06-09T04:02:08.305Z" }, + { + "rule": "React-useRef", + "path": "ts/components/BackboneHost.tsx", + "line": " const hostRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/BackboneHost.tsx", + "line": " const viewRef = useRef(undefined);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/CallNeedPermissionScreen.js", @@ -13272,6 +13313,13 @@ "updated": "2020-10-26T19:12:24.410Z", "reasonDetail": "Doesn't touch the DOM." }, + { + "rule": "React-useRef", + "path": "ts/components/CallNeedPermissionScreen.tsx", + "line": " const autoCloseAtRef = useRef(Date.now() + AUTO_CLOSE_MS);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/CallScreen.js", @@ -13280,6 +13328,13 @@ "updated": "2020-10-26T21:35:52.858Z", "reasonDetail": "Used to get the local video element for rendering." }, + { + "rule": "React-useRef", + "path": "ts/components/CallScreen.tsx", + "line": " const localVideoRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/CallingLobby.js", @@ -13288,6 +13343,13 @@ "updated": "2020-10-26T19:12:24.410Z", "reasonDetail": "Used to get the local video element for rendering." }, + { + "rule": "React-useRef", + "path": "ts/components/CallingLobby.tsx", + "line": " const localVideoRef = React.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/CallingPip.js", @@ -13312,6 +13374,13 @@ "updated": "2020-10-26T19:12:24.410Z", "reasonDetail": "Used to get the local video element for rendering." }, + { + "rule": "React-useRef", + "path": "ts/components/CallingPip.tsx", + "line": " const videoContainerRef = React.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/CallingToastManager.js", @@ -13319,6 +13388,13 @@ "reasonCategory": "usageTrusted", "updated": "2021-05-13T19:40:31.751Z" }, + { + "rule": "React-useRef", + "path": "ts/components/CallingToastManager.tsx", + "line": " const timeoutRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/CaptchaDialog.js", @@ -13327,6 +13403,13 @@ "updated": "2021-05-05T23:11:22.692Z", "reasonDetail": "Used only to set focus" }, + { + "rule": "React-useRef", + "path": "ts/components/CaptchaDialog.tsx", + "line": " const buttonRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-createRef", "path": "ts/components/CaptionEditor.js", @@ -13350,6 +13433,13 @@ "reasonCategory": "usageTrusted", "updated": "2021-05-25T18:25:53.896Z" }, + { + "rule": "React-useRef", + "path": "ts/components/ChatColorPicker.tsx", + "line": " const menuRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "DOM-innerHTML", "path": "ts/components/CompositionArea.js", @@ -13382,6 +13472,20 @@ "updated": "2020-06-03T19:23:21.195Z", "reasonDetail": "Our code, no user input, only clearing out the dom" }, + { + "rule": "React-useRef", + "path": "ts/components/CompositionArea.tsx", + "line": " const inputApiRef = React.useRef();", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/CompositionArea.tsx", + "line": " const micCellRef = React.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/CompositionInput.js", @@ -13444,6 +13548,48 @@ "reasonCategory": "usageTrusted", "updated": "2021-04-21T21:35:38.757Z" }, + { + "rule": "React-useRef", + "path": "ts/components/CompositionInput.tsx", + "line": " const emojiCompletionRef = React.useRef();", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/CompositionInput.tsx", + "line": " const mentionCompletionRef = React.useRef();", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/CompositionInput.tsx", + "line": " const quillRef = React.useRef();", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/CompositionInput.tsx", + "line": " const scrollerRef = React.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/CompositionInput.tsx", + "line": " const propsRef = React.useRef(props);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/CompositionInput.tsx", + "line": " const memberRepositoryRef = React.useRef(", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/ContactPills.js", @@ -13452,6 +13598,13 @@ "updated": "2021-03-01T18:34:36.638Z", "reasonDetail": "Used for scrolling. Doesn't otherwise manipulate the DOM" }, + { + "rule": "React-useRef", + "path": "ts/components/ContactPills.tsx", + "line": " const elRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/ConversationList.js", @@ -13460,6 +13613,13 @@ "updated": "2021-02-12T16:25:08.285Z", "reasonDetail": "Used for scroll calculations" }, + { + "rule": "React-useRef", + "path": "ts/components/ConversationList.tsx", + "line": " const listRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/DirectCallRemoteParticipant.js", @@ -13468,6 +13628,13 @@ "updated": "2020-11-11T21:56:04.179Z", "reasonDetail": "Needed to render the remote video element." }, + { + "rule": "React-useRef", + "path": "ts/components/DirectCallRemoteParticipant.tsx", + "line": " const remoteVideoRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/ForwardMessageModal.js", @@ -13482,6 +13649,20 @@ "reasonCategory": "usageTrusted", "updated": "2021-04-19T18:13:21.664Z" }, + { + "rule": "React-useRef", + "path": "ts/components/ForwardMessageModal.tsx", + "line": " const inputRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/ForwardMessageModal.tsx", + "line": " const inputApiRef = React.useRef();", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/GradientDial.js", @@ -13489,6 +13670,13 @@ "reasonCategory": "usageTrusted", "updated": "2021-05-25T18:25:53.896Z" }, + { + "rule": "React-useRef", + "path": "ts/components/GradientDial.tsx", + "line": " const containerRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/GroupCallOverflowArea.js", @@ -13497,6 +13685,13 @@ "updated": "2021-01-08T15:48:46.313Z", "reasonDetail": "Used to deal with scroll position." }, + { + "rule": "React-useRef", + "path": "ts/components/GroupCallOverflowArea.tsx", + "line": " const overflowRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/GroupCallRemoteParticipant.js", @@ -13529,6 +13724,20 @@ "updated": "2021-06-17T20:46:02.342Z", "reasonDetail": "Doesn't reference the DOM." }, + { + "rule": "React-useRef", + "path": "ts/components/GroupCallRemoteParticipant.tsx", + "line": " const remoteVideoRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/GroupCallRemoteParticipant.tsx", + "line": " const canvasContextRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/Inbox.js", @@ -13543,6 +13752,20 @@ "reasonCategory": "usageTrusted", "updated": "2021-06-08T02:49:25.154Z" }, + { + "rule": "React-useRef", + "path": "ts/components/Inbox.tsx", + "line": " const hostRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/Inbox.tsx", + "line": " const viewRef = useRef(undefined);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/Input.js", @@ -13564,6 +13787,27 @@ "reasonCategory": "usageTrusted", "updated": "2021-07-14T00:50:58.330Z" }, + { + "rule": "React-useRef", + "path": "ts/components/Input.tsx", + "line": " const innerRef = useRef(", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/Input.tsx", + "line": " const valueOnKeydownRef = useRef(value);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/Input.tsx", + "line": " const selectionStartOnKeydownRef = useRef(value.length);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "jQuery-$(", "path": "ts/components/Intl.js", @@ -13624,6 +13868,13 @@ "reasonCategory": "usageTrusted", "updated": "2021-07-14T00:50:58.330Z" }, + { + "rule": "React-useRef", + "path": "ts/components/ProfileEditor.tsx", + "line": " const focusInputRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-createRef", "path": "ts/components/SafetyNumberChangeDialog.js", @@ -13632,14 +13883,6 @@ "updated": "2020-06-23T06:48:06.829Z", "reasonDetail": "Used to focus cancel button when dialog opens" }, - { - "rule": "React-useRef", - "path": "ts/components/ShortcutGuide.js", - "line": " const focusRef = React.useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2020-10-26T19:12:24.410Z", - "reasonDetail": "Only used to focus the element." - }, { "rule": "React-useRef", "path": "ts/components/Slider.js", @@ -13661,6 +13904,27 @@ "reasonCategory": "usageTrusted", "updated": "2021-05-25T18:25:53.896Z" }, + { + "rule": "React-useRef", + "path": "ts/components/Slider.tsx", + "line": " const diff = useRef(0);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/Slider.tsx", + "line": " const handleRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/Slider.tsx", + "line": " const sliderRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/Tooltip.js", @@ -13669,6 +13933,13 @@ "updated": "2020-12-04T00:11:08.128Z", "reasonDetail": "Used to add (and remove) event listeners." }, + { + "rule": "React-useRef", + "path": "ts/components/Tooltip.tsx", + "line": " const wrapperRef = React.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/conversation/ContactModal.js", @@ -13683,6 +13954,20 @@ "reasonCategory": "usageTrusted", "updated": "2020-11-10T21:27:04.909Z" }, + { + "rule": "React-useRef", + "path": "ts/components/conversation/ContactModal.tsx", + "line": " const overlayRef = React.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/conversation/ContactModal.tsx", + "line": " const closeButtonRef = React.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-createRef", "path": "ts/components/conversation/ConversationHeader.js", @@ -13723,6 +14008,13 @@ "updated": "2020-10-26T19:12:24.410Z", "reasonDetail": "Doesn't refer to a DOM element." }, + { + "rule": "React-useRef", + "path": "ts/components/conversation/ConversationHero.tsx", + "line": " const previousHeightRef = useRef();", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/conversation/GIF.js", @@ -13731,6 +14023,13 @@ "updated": "2021-04-17T01:47:31.419Z", "reasonDetail": "Used for managing playback of GIF video" }, + { + "rule": "React-useRef", + "path": "ts/components/conversation/GIF.tsx", + "line": " const videoRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/conversation/GroupDescription.js", @@ -13738,6 +14037,13 @@ "reasonCategory": "usageTrusted", "updated": "2021-05-29T02:15:39.186Z" }, + { + "rule": "React-useRef", + "path": "ts/components/conversation/GroupDescription.tsx", + "line": " const textRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-createRef", "path": "ts/components/conversation/InlineNotificationWrapper.js", @@ -13810,6 +14116,13 @@ "updated": "2021-03-09T01:19:04.057Z", "reasonDetail": "Used for obtanining the bounding box for the container" }, + { + "rule": "React-useRef", + "path": "ts/components/conversation/MessageAudio.tsx", + "line": " const waveformRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-createRef", "path": "ts/components/conversation/MessageDetail.js", @@ -13834,22 +14147,6 @@ "updated": "2021-01-20T21:30:08.430Z", "reasonDetail": "Doesn't touch the DOM." }, - { - "rule": "React-useRef", - "path": "ts/components/conversation/ReactionPicker.js", - "line": " const focusRef = React.useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2020-10-26T19:12:24.410Z", - "reasonDetail": "Only used to focus the element." - }, - { - "rule": "React-useRef", - "path": "ts/components/conversation/ReactionViewer.js", - "line": " const focusRef = React.useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2020-10-26T19:12:24.410Z", - "reasonDetail": "Only used to focus the element." - }, { "rule": "React-createRef", "path": "ts/components/conversation/Timeline.js", @@ -13866,6 +14163,13 @@ "updated": "2021-03-11T20:49:17.292Z", "reasonDetail": "Used to focus an input." }, + { + "rule": "React-useRef", + "path": "ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.tsx", + "line": " const inputRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-useRef", "path": "ts/components/conversation/conversation-details/EditConversationAttributesModal.js", @@ -13890,6 +14194,27 @@ "updated": "2021-06-04T14:19:49.714Z", "reasonDetail": "Only stores undefined/true/false, not DOM references." }, + { + "rule": "React-useRef", + "path": "ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx", + "line": " const focusDescriptionRef = useRef(", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx", + "line": " const startingTitleRef = useRef(externalTitle);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx", + "line": " const startingAvatarPathRef = useRef(externalAvatarPath);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, { "rule": "React-createRef", "path": "ts/components/conversation/media-gallery/MediaGallery.js", @@ -13914,22 +14239,6 @@ "updated": "2019-11-21T06:13:49.384Z", "reasonDetail": "Used for setting focus only" }, - { - "rule": "React-useRef", - "path": "ts/components/stickers/StickerPicker.js", - "line": " const focusRef = React.useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2020-10-26T19:12:24.410Z", - "reasonDetail": "Only used to focus the element." - }, - { - "rule": "React-useRef", - "path": "ts/components/stickers/StickerPreviewModal.js", - "line": " const focusRef = React.useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2020-10-26T19:12:24.410Z", - "reasonDetail": "Only used to focus the element." - }, { "rule": "jQuery-append(", "path": "ts/logging/debuglogs.js", @@ -14102,5 +14411,49 @@ "reasonCategory": "usageTrusted", "updated": "2021-03-18T21:41:28.361Z", "reasonDetail": "A generic hook. Typically not to be used with non-DOM values." + }, + { + "rule": "React-useRef", + "path": "ts/util/hooks.js", + "line": " const toFocusRef = React.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T01:08:01.309Z", + "reasonDetail": "Used to set focus" + }, + { + "rule": "React-useRef", + "path": "ts/util/hooks.js", + "line": " const lastFocusedRef = React.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T01:08:01.309Z", + "reasonDetail": "Used to store the previous-focused item, again to set focus" + }, + { + "rule": "React-useRef", + "path": "ts/util/hooks.ts", + "line": " const previousValueRef = React.useRef(initialValue);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/util/hooks.ts", + "line": " const toFocusRef = React.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/util/hooks.ts", + "line": " const lastFocusedRef = React.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" + }, + { + "rule": "React-useRef", + "path": "ts/util/hooks.ts", + "line": " const unobserveRef = React.useRef<(() => unknown) | null>(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-07-30T16:57:33.618Z" } ] \ No newline at end of file diff --git a/ts/util/lint/rules.json b/ts/util/lint/rules.json index e183ba86f..ed83b7161 100644 --- a/ts/util/lint/rules.json +++ b/ts/util/lint/rules.json @@ -137,7 +137,7 @@ }, { "name": "React-useRef", - "expression": "\\buseRef\\(", + "expression": "\\buseRef(\\(|<)", "reason": "Potential XSS", "excludedModules": ["node_modules/react/", "node_modules/react-dom"] }, diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index c5e914ed2..984636f07 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -903,6 +903,10 @@ Whisper.ConversationView = Whisper.View.extend({ this.navigateTo(url); }; + const learnMoreAboutDeliveryIssue = () => { + this.navigateTo('https://support.signal.org/hc/articles/4404859745690'); + }; + const scrollToQuotedMessage = async (options: any) => { const { authorId, sentAt } = options; @@ -1079,6 +1083,7 @@ Whisper.ConversationView = Whisper.View.extend({ model.acknowledgeGroupMemberNameCollisions(groupNameCollisions); }, contactSupport, + learnMoreAboutDeliveryIssue, loadNewerMessages, loadNewestMessages: this.loadNewestMessages.bind(this), loadAndScroll: this.loadAndScroll.bind(this),