Add useSizeObserver and replace most react-measure

This commit is contained in:
Jamie Kyle 2023-07-25 16:56:56 -07:00 committed by GitHub
parent 7267391de4
commit 6c70cd450b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 539 additions and 421 deletions

View file

@ -2138,30 +2138,6 @@ Signal Desktop makes use of the following open source projects.
License: BSD-3-Clause
## react-measure
The MIT License (MIT)
Copyright (c) 2018 React Measure authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## react-popper
The MIT License (MIT)

View file

@ -154,7 +154,6 @@
"react-dom": "17.0.2",
"react-hot-loader": "4.13.0",
"react-intl": "6.1.1",
"react-measure": "2.3.0",
"react-popper": "2.3.0",
"react-quill": "2.0.0-beta.4",
"react-redux": "7.2.8",
@ -236,7 +235,6 @@
"@types/quill": "1.3.10",
"@types/react": "17.0.45",
"@types/react-dom": "17.0.17",
"@types/react-measure": "2.0.5",
"@types/react-redux": "7.1.24",
"@types/react-router-dom": "4.3.4",
"@types/react-virtualized": "9.18.12",

View file

@ -3,8 +3,6 @@
import { pick } from 'lodash';
import React, { useCallback } from 'react';
import type { MeasuredComponentProps } from 'react-measure';
import Measure from 'react-measure';
import type { ListRowProps } from 'react-virtualized';
import type { ConversationType } from '../state/ducks/conversations';
@ -23,6 +21,7 @@ import { useRestoreFocus } from '../hooks/useRestoreFocus';
import { ListView } from './ListView';
import { ListTile } from './ListTile';
import type { ShowToastAction } from '../state/ducks/toast';
import { SizeObserver } from '../hooks/useSizeObserver';
type OwnProps = {
i18n: LocalizerType;
@ -180,33 +179,26 @@ export function AddUserToAnotherGroupModal({
ref={inputRef}
value={searchTerm}
/>
<Measure bounds>
{({ contentRect, measureRef }: MeasuredComponentProps) => {
// Though `width` and `height` are required properties, we want to be
// careful in case the caller sends bogus data. Notably, react-measure's
// types seem to be inaccurate.
const { width = 100, height = 100 } = contentRect.bounds || {};
if (!width || !height) {
return null;
}
<SizeObserver>
{(ref, size) => {
return (
<div
className="AddUserToAnotherGroupModal__list-wrapper"
ref={measureRef}
ref={ref}
>
{size != null && (
<ListView
width={width}
height={height}
width={size.width}
height={size.height}
rowCount={filteredConversations.length}
calculateRowHeight={handleCalculateRowHeight}
rowRenderer={renderGroupListItem}
/>
)}
</div>
);
}}
</Measure>
</SizeObserver>
</div>
</Modal>
)}

View file

@ -2,14 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState, useCallback, useRef } from 'react';
import type { ContentRect } from 'react-measure';
import Measure from 'react-measure';
import { useComputePeaks } from '../hooks/useComputePeaks';
import type { LocalizerType } from '../types/Util';
import { WaveformScrubber } from './conversation/WaveformScrubber';
import { PlaybackButton } from './PlaybackButton';
import { RecordingComposer } from './RecordingComposer';
import * as log from '../logging/log';
import type { Size } from '../hooks/useSizeObserver';
import { SizeObserver } from '../hooks/useSizeObserver';
type Props = {
i18n: LocalizerType;
@ -46,8 +46,8 @@ export function CompositionRecordingDraft({
const timeout = useRef<undefined | NodeJS.Timeout>(undefined);
const handleResize = useCallback(
({ bounds }: ContentRect) => {
if (!bounds || bounds.width === state.width) {
(size: Size) => {
if (size.width === state.width) {
return;
}
@ -59,7 +59,7 @@ export function CompositionRecordingDraft({
clearTimeout(timeout.current);
}
const newWidth = bounds.width;
const newWidth = size.width;
// if mounting, set width immediately
// otherwise debounce
@ -106,13 +106,13 @@ export function CompositionRecordingDraft({
}
onClick={handlePlaybackClick}
/>
<Measure bounds onResize={handleResize}>
{({ measureRef }) => (
<div ref={measureRef} className="CompositionRecordingDraft__sizer">
<SizeObserver onSizeChange={handleResize}>
{ref => (
<div ref={ref} className="CompositionRecordingDraft__sizer">
{scrubber}
</div>
)}
</Measure>
</SizeObserver>
</RecordingComposer>
);
}

View file

@ -489,14 +489,11 @@ export function ConversationList({
]
);
// Though `width` and `height` are required properties, we want to be careful in case
// the caller sends bogus data. Notably, react-measure's types seem to be inaccurate.
const { width = 0, height = 0 } = dimensions || {};
if (!width || !height) {
if (dimensions == null) {
return null;
}
const widthBreakpoint = getConversationListWidthBreakpoint(width);
const widthBreakpoint = getConversationListWidthBreakpoint(dimensions.width);
return (
<ListView
@ -504,8 +501,8 @@ export function ConversationList({
'module-conversation-list',
`module-conversation-list--width-${widthBreakpoint}`
)}
width={width}
height={height}
width={dimensions.width}
height={dimensions.height}
rowCount={rowCount}
calculateRowHeight={calculateRowHeight}
rowRenderer={renderRow}

View file

@ -9,8 +9,6 @@ import React, {
useState,
Fragment,
} from 'react';
import type { MeasuredComponentProps } from 'react-measure';
import Measure from 'react-measure';
import { AttachmentList } from './conversation/AttachmentList';
import type { AttachmentType } from '../types/Attachment';
import { Button } from './Button';
@ -42,6 +40,7 @@ import type { HydratedBodyRangesType } from '../types/BodyRange';
import { BodyRange } from '../types/BodyRange';
import { UserText } from './UserText';
import { Modal } from './Modal';
import { SizeObserver } from '../hooks/useSizeObserver';
export type DataPropsType = {
candidateConversations: ReadonlyArray<ConversationType>;
@ -334,14 +333,14 @@ export function ForwardMessagesModal({
value={searchTerm}
/>
{candidateConversations.length ? (
<Measure bounds>
{({ contentRect, measureRef }: MeasuredComponentProps) => (
<SizeObserver>
{(ref, size) => (
<div
className="module-ForwardMessageModal__list-wrapper"
ref={measureRef}
ref={ref}
>
<ConversationList
dimensions={contentRect.bounds}
dimensions={size ?? undefined}
getPreferredBadge={getPreferredBadge}
getRow={getRow}
i18n={i18n}
@ -379,7 +378,7 @@ export function ForwardMessagesModal({
/>
</div>
)}
</Measure>
</SizeObserver>
) : (
<div className="module-ForwardMessageModal__no-candidate-contacts">
{i18n('icu:noContactsFound')}

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import Measure from 'react-measure';
import { takeWhile, clamp, chunk, maxBy, flatten, noop } from 'lodash';
import type { VideoFrameSource } from '@signalapp/ringrtc';
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';
@ -25,6 +24,7 @@ import { filter, join } from '../util/iterables';
import * as setUtil from '../util/setUtil';
import * as log from '../logging/log';
import { MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH } from '../calling/constants';
import { SizeObserver } from '../hooks/useSizeObserver';
const MIN_RENDERED_HEIGHT = 180;
const PARTICIPANT_MARGIN = 10;
@ -398,40 +398,27 @@ export function GroupCallRemoteParticipants({
]);
return (
<Measure
bounds
onResize={({ bounds }) => {
if (!bounds) {
log.error('We should be measuring the bounds');
return;
}
setContainerDimensions(bounds);
<SizeObserver
onSizeChange={size => {
setContainerDimensions(size);
}}
>
{containerMeasure => (
<div
className="module-ongoing-call__participants"
ref={containerMeasure.measureRef}
>
<Measure
bounds
onResize={({ bounds }) => {
if (!bounds) {
log.error('We should be measuring the bounds');
return;
}
setGridDimensions(bounds);
{containerRef => (
<div className="module-ongoing-call__participants" ref={containerRef}>
<SizeObserver
onSizeChange={size => {
setGridDimensions(size);
}}
>
{gridMeasure => (
{gridRef => (
<div
className="module-ongoing-call__participants__grid"
ref={gridMeasure.measureRef}
ref={gridRef}
>
{flatten(rowElements)}
</div>
)}
</Measure>
</SizeObserver>
<GroupCallOverflowArea
getFrameBuffer={getFrameBuffer}
@ -444,7 +431,7 @@ export function GroupCallRemoteParticipants({
/>
</div>
)}
</Measure>
</SizeObserver>
);
}

View file

@ -2,8 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useCallback, useMemo, useState } from 'react';
import type { MeasuredComponentProps } from 'react-measure';
import Measure from 'react-measure';
import classNames from 'classnames';
import { clamp, isNumber, noop } from 'lodash';
@ -51,6 +49,7 @@ import type {
ReplaceAvatarActionType,
SaveAvatarToDiskActionType,
} from '../types/Avatar';
import { SizeObserver } from '../hooks/useSizeObserver';
export enum LeftPaneMode {
Inbox,
@ -652,9 +651,9 @@ export function LeftPane({
))}
</div>
{preRowsNode && <React.Fragment key={0}>{preRowsNode}</React.Fragment>}
<Measure bounds>
{({ contentRect, measureRef }: MeasuredComponentProps) => (
<div className="module-left-pane__list--measure" ref={measureRef}>
<SizeObserver>
{(ref, size) => (
<div className="module-left-pane__list--measure" ref={ref}>
<div className="module-left-pane__list--wrapper">
<div
aria-live="polite"
@ -667,7 +666,7 @@ export function LeftPane({
<ConversationList
dimensions={{
width,
height: contentRect.bounds?.height || 0,
height: size?.height || 0,
}}
getPreferredBadge={getPreferredBadge}
getRow={getRow}
@ -717,7 +716,7 @@ export function LeftPane({
</div>
</div>
)}
</Measure>
</SizeObserver>
{footerContents && (
<div className="module-left-pane__footer">{footerContents}</div>
)}

View file

@ -1,7 +1,6 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Measure from 'react-measure';
import React, { useCallback, useEffect, useState } from 'react';
import classNames from 'classnames';
import { createPortal } from 'react-dom';
@ -47,6 +46,7 @@ import type { HydratedBodyRangesType } from '../types/BodyRange';
import { MessageBody } from './conversation/MessageBody';
import { RenderLocation } from './conversation/MessageTextRenderer';
import { arrow } from '../util/keyboard';
import { SizeObserver } from '../hooks/useSizeObserver';
export type MediaEditorResultType = Readonly<{
data: Uint8Array;
@ -911,19 +911,14 @@ export function MediaEditor({
return createPortal(
<div className="MediaEditor">
<div className="MediaEditor__container">
<Measure
bounds
onResize={({ bounds }) => {
if (!bounds) {
log.error('We should be measuring the bounds');
return;
}
setContainerWidth(bounds.width);
setContainerHeight(bounds.height);
<SizeObserver
onSizeChange={size => {
setContainerWidth(size.width);
setContainerHeight(size.height);
}}
>
{({ measureRef }) => (
<div className="MediaEditor__media" ref={measureRef}>
{ref => (
<div className="MediaEditor__media" ref={ref}>
{image && (
<div>
<canvas
@ -937,7 +932,7 @@ export function MediaEditor({
)}
</div>
)}
</Measure>
</SizeObserver>
</div>
<div className="MediaEditor__toolbar">
{tooling ? (

View file

@ -3,8 +3,6 @@
import type { ReactElement, ReactNode } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import type { ContentRect, MeasuredComponentProps } from 'react-measure';
import Measure from 'react-measure';
import classNames from 'classnames';
import { noop } from 'lodash';
import { animated } from '@react-spring/web';
@ -16,8 +14,12 @@ import { assertDev } from '../util/assert';
import { getClassNamesFor } from '../util/getClassNamesFor';
import { useAnimated } from '../hooks/useAnimated';
import { useHasWrapped } from '../hooks/useHasWrapped';
import { useRefMerger } from '../hooks/useRefMerger';
import * as log from '../logging/log';
import {
isOverflowing,
isScrolled,
useScrollObserver,
} from '../hooks/useSizeObserver';
type PropsType = {
children: ReactNode;
@ -169,24 +171,19 @@ export function ModalPage({
}: ModalPageProps): JSX.Element {
const modalRef = useRef<HTMLDivElement | null>(null);
const refMerger = useRefMerger();
const bodyRef = useRef<HTMLDivElement>(null);
const bodyInnerRef = useRef<HTMLDivElement>(null);
const bodyRef = useRef<HTMLDivElement | null>(null);
const [scrolled, setScrolled] = useState(false);
const [hasOverflow, setHasOverflow] = useState(false);
const hasHeader = Boolean(hasXButton || title || onBackButtonClick);
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
function handleResize({ scroll }: ContentRect) {
const modalNode = modalRef?.current;
if (!modalNode) {
return;
}
if (scroll) {
setHasOverflow(scroll.height > modalNode.clientHeight);
}
}
useScrollObserver(bodyRef, bodyInnerRef, scroll => {
setScrolled(isScrolled(scroll));
setHasOverflow(isOverflowing(scroll));
});
return (
<>
@ -249,26 +246,16 @@ export function ModalPage({
)}
</div>
)}
<Measure scroll onResize={handleResize}>
{({ measureRef }: MeasuredComponentProps) => (
<div
className={classNames(
getClassName('__body'),
scrolled ? getClassName('__body--scrolled') : null,
hasOverflow || scrolled
? getClassName('__body--overflow')
: null
hasOverflow || scrolled ? getClassName('__body--overflow') : null
)}
onScroll={() => {
const scrollTop = bodyRef.current?.scrollTop || 0;
setScrolled(scrollTop > 2);
}}
ref={refMerger(measureRef, bodyRef)}
ref={bodyRef}
>
{children}
<div ref={bodyInnerRef}>{children}</div>
</div>
)}
</Measure>
{modalFooter && <Modal.ButtonFooter>{modalFooter}</Modal.ButtonFooter>}
</div>
</>

View file

@ -1,10 +1,8 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MeasuredComponentProps } from 'react-measure';
import type { ReactNode } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import Measure from 'react-measure';
import { noop } from 'lodash';
import type { ConversationType } from '../state/ducks/conversations';
@ -41,6 +39,7 @@ import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
import { getGroupMemberships } from '../util/getGroupMemberships';
import { strictAssert } from '../util/assert';
import { UserText } from './UserText';
import { SizeObserver } from '../hooks/useSizeObserver';
export type PropsType = {
candidateConversations: Array<ConversationType>;
@ -1193,14 +1192,11 @@ export function EditDistributionListModal({
</ContactPills>
) : undefined}
{candidateConversations.length ? (
<Measure bounds>
{({ contentRect, measureRef }: MeasuredComponentProps) => (
<div
className="StoriesSettingsModal__conversation-list"
ref={measureRef}
>
<SizeObserver>
{(ref, size) => (
<div className="StoriesSettingsModal__conversation-list" ref={ref}>
<ConversationList
dimensions={contentRect.bounds}
dimensions={size ?? undefined}
getPreferredBadge={getPreferredBadge}
getRow={getRow}
i18n={i18n}
@ -1228,7 +1224,7 @@ export function EditDistributionListModal({
/>
</div>
)}
</Measure>
</SizeObserver>
) : (
<div className="module-ForwardMessageModal__no-candidate-contacts">
{i18n('icu:noContactsFound')}

View file

@ -1,7 +1,6 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Measure from 'react-measure';
import React, { forwardRef, useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import classNames from 'classnames';
@ -22,6 +21,7 @@ import {
} from '../util/getStoryBackground';
import { SECOND } from '../util/durations';
import { useRefMerger } from '../hooks/useRefMerger';
import { useSizeObserver } from '../hooks/useSizeObserver';
const renderNewLines: RenderTextCallbackType = ({
text: textWithNewLines,
@ -169,10 +169,10 @@ export const TextAttachment = forwardRef<HTMLTextAreaElement, PropsType>(
background: getBackgroundColor(textAttachment),
};
return (
<Measure bounds>
{({ contentRect, measureRef }) => {
const scaleFactor = (contentRect.bounds?.height || 1) / 1280;
const ref = useRef<HTMLDivElement>(null);
const size = useSizeObserver(ref);
const scaleFactor = (size?.height || 1) / 1280;
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
@ -189,7 +189,7 @@ export const TextAttachment = forwardRef<HTMLTextAreaElement, PropsType>(
setLinkPreviewOffsetTop(undefined);
}
}}
ref={measureRef}
ref={ref}
style={isThumbnail ? storyBackgroundColor : undefined}
>
{/*
@ -266,10 +266,7 @@ export const TextAttachment = forwardRef<HTMLTextAreaElement, PropsType>(
i18n
)}
>
<Emojify
text={textContent}
renderNonEmoji={renderNewLines}
/>
<Emojify text={textContent} renderNonEmoji={renderNewLines} />
</div>
)}
</div>
@ -290,9 +287,7 @@ export const TextAttachment = forwardRef<HTMLTextAreaElement, PropsType>(
{onRemoveLinkPreview && (
<div className="TextAttachment__preview__remove">
<button
aria-label={i18n(
'icu:Keyboard--remove-draft-link-preview'
)}
aria-label={i18n('icu:Keyboard--remove-draft-link-preview')}
type="button"
onClick={onRemoveLinkPreview}
/>
@ -301,9 +296,7 @@ export const TextAttachment = forwardRef<HTMLTextAreaElement, PropsType>(
<StoryLinkPreview
{...textAttachment.preview}
domain={getDomain(String(textAttachment.preview.url))}
forceCompactMode={
getTextSize(textContent) !== TextSize.Large
}
forceCompactMode={getTextSize(textContent) !== TextSize.Large}
i18n={i18n}
title={textAttachment.preview.title || undefined}
url={textAttachment.preview.url}
@ -313,8 +306,5 @@ export const TextAttachment = forwardRef<HTMLTextAreaElement, PropsType>(
</div>
</div>
);
}}
</Measure>
);
}
);

View file

@ -3,7 +3,6 @@
import type { ReactNode } from 'react';
import React from 'react';
import Measure from 'react-measure';
import classNames from 'classnames';
import {
ContextMenu,
@ -40,6 +39,7 @@ import {
import { PanelType } from '../../types/Panels';
import { UserText } from '../UserText';
import { Alert } from '../Alert';
import { SizeObserver } from '../../hooks/useSizeObserver';
export enum OutgoingCallButtonStyle {
None,
@ -783,16 +783,12 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
{this.renderDeleteMessagesConfirmationDialog()}
{this.renderLeaveGroupConfirmationDialog()}
{this.renderCannotLeaveGroupBecauseYouAreLastAdminAlert()}
<Measure
bounds
onResize={({ bounds }) => {
if (!bounds || !bounds.width) {
return;
}
this.setState({ isNarrow: bounds.width < 500 });
<SizeObserver
onSizeChange={size => {
this.setState({ isNarrow: size.width < 500 });
}}
>
{({ measureRef }) => (
{measureRef => (
<div
className={classNames('module-ConversationHeader', {
'module-ConversationHeader--narrow': isNarrow,
@ -821,7 +817,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
{this.renderMenu(triggerId)}
</div>
)}
</Measure>
</SizeObserver>
</>
);
}

View file

@ -4,8 +4,6 @@
import type { ReactChild } from 'react';
import React, { forwardRef, useCallback, useState } from 'react';
import classNames from 'classnames';
import type { ContentRect } from 'react-measure';
import Measure from 'react-measure';
import type { LocalizerType } from '../../types/Util';
import type { DirectionType, MessageStatusType } from './Message';
@ -17,6 +15,8 @@ import { PanelType } from '../../types/Panels';
import { Spinner } from '../Spinner';
import { ConfirmationDialog } from '../ConfirmationDialog';
import { refMerger } from '../../util/refMerger';
import type { Size } from '../../hooks/useSizeObserver';
import { SizeObserver } from '../../hooks/useSizeObserver';
type PropsType = {
deletedForEveryone?: boolean;
@ -254,21 +254,21 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
);
const onResize = useCallback(
({ bounds }: ContentRect) => {
onWidthMeasured?.(bounds?.width || 0);
(size: Size) => {
onWidthMeasured?.(size.width);
},
[onWidthMeasured]
);
if (onWidthMeasured) {
return (
<Measure bounds onResize={onResize}>
{({ measureRef }) => (
<SizeObserver onSizeChange={onResize}>
{measureRef => (
<div className={className} ref={refMerger(measureRef, ref)}>
{children}
</div>
)}
</Measure>
</SizeObserver>
);
}

View file

@ -5,7 +5,6 @@ import { first, get, isNumber, last, throttle } from 'lodash';
import classNames from 'classnames';
import type { ReactChild, ReactNode, RefObject } from 'react';
import React from 'react';
import Measure from 'react-measure';
import type { ReadonlyDeep } from 'type-fest';
import { ScrollDownButton, ScrollDownButtonVariant } from './ScrollDownButton';
@ -43,6 +42,7 @@ import {
} from '../../util/scrollUtil';
import { LastSeenIndicator } from './LastSeenIndicator';
import { MINUTE } from '../../util/durations';
import { SizeObserver } from '../../hooks/useSizeObserver';
const AT_BOTTOM_THRESHOLD = 15;
const AT_BOTTOM_DETECTOR_STYLE = { height: AT_BOTTOM_THRESHOLD };
@ -204,7 +204,6 @@ export class Timeline extends React.Component<
private readonly atBottomDetectorRef = React.createRef<HTMLDivElement>();
private readonly lastSeenIndicatorRef = React.createRef<HTMLDivElement>();
private intersectionObserver?: IntersectionObserver;
private intersectionObserverCallbackFrame?: number;
// This is a best guess. It will likely be overridden when the timeline is measured.
private maxVisibleRows = Math.ceil(window.innerHeight / MIN_ROW_HEIGHT);
@ -340,10 +339,6 @@ export class Timeline extends React.Component<
// this another way, but this approach works.)
this.intersectionObserver?.disconnect();
if (this.intersectionObserverCallbackFrame !== undefined) {
window.cancelAnimationFrame(this.intersectionObserverCallbackFrame);
}
const intersectionRatios = new Map<Element, number>();
const intersectionObserverCallback: IntersectionObserverCallback =
@ -445,19 +440,12 @@ export class Timeline extends React.Component<
'observer.disconnect() should prevent callbacks from firing'
);
// `react-measure` schedules the callbacks on the next tick and so
// should we because we want other parts of this component to respond
// to resize events before we recalculate what is visible.
this.intersectionObserverCallbackFrame = window.requestAnimationFrame(
() => {
// Observer was updated from under us
if (this.intersectionObserver !== observer) {
return;
}
intersectionObserverCallback(entries, observer);
}
);
},
{
root: containerEl,
@ -1002,17 +990,12 @@ export class Timeline extends React.Component<
}
headerElements = (
<Measure
bounds
onResize={({ bounds }) => {
if (!bounds) {
assertDev(false, 'We should be measuring the bounds');
return;
}
this.setState({ lastMeasuredWarningHeight: bounds.height });
<SizeObserver
onSizeChange={size => {
this.setState({ lastMeasuredWarningHeight: size.height });
}}
>
{({ measureRef }) => (
{measureRef => (
<TimelineWarnings ref={measureRef}>
{renderMiniPlayer({ shouldFlow: true })}
{text && (
@ -1025,7 +1008,7 @@ export class Timeline extends React.Component<
)}
</TimelineWarnings>
)}
</Measure>
</SizeObserver>
);
}
@ -1061,18 +1044,15 @@ export class Timeline extends React.Component<
return (
<>
<Measure
bounds
onResize={({ bounds }) => {
<SizeObserver
onSizeChange={size => {
const { isNearBottom } = this.props;
strictAssert(bounds, 'We should be measuring the bounds');
this.setState({
widthBreakpoint: getWidthBreakpoint(bounds.width),
widthBreakpoint: getWidthBreakpoint(size.width),
});
this.maxVisibleRows = Math.ceil(bounds.height / MIN_ROW_HEIGHT);
this.maxVisibleRows = Math.ceil(size.height / MIN_ROW_HEIGHT);
const containerEl = this.containerRef.current;
if (containerEl && isNearBottom) {
@ -1080,7 +1060,7 @@ export class Timeline extends React.Component<
}
}}
>
{({ measureRef }) => (
{ref => (
<div
className={classNames(
'module-timeline',
@ -1091,7 +1071,7 @@ export class Timeline extends React.Component<
tabIndex={-1}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
ref={measureRef}
ref={ref}
>
{headerElements}
@ -1152,7 +1132,7 @@ export class Timeline extends React.Component<
) : null}
</div>
)}
</Measure>
</SizeObserver>
{Boolean(invitedContactsForNewlyCreatedGroup.length) && (
<NewlyCreatedGroupInvitedContactsDialog

View file

@ -9,8 +9,6 @@ import React, {
useCallback,
} from 'react';
import { omit } from 'lodash';
import type { MeasuredComponentProps } from 'react-measure';
import Measure from 'react-measure';
import type { ListRowProps } from 'react-virtualized';
import type { LocalizerType, ThemeType } from '../../../../types/Util';
@ -47,6 +45,7 @@ import { SearchInput } from '../../../SearchInput';
import { ListView } from '../../../ListView';
import { UsernameCheckbox } from '../../../conversationList/UsernameCheckbox';
import { PhoneNumberCheckbox } from '../../../conversationList/PhoneNumberCheckbox';
import { SizeObserver } from '../../../../hooks/useSizeObserver';
export type StatePropsType = {
regionCode: string | undefined;
@ -432,16 +431,8 @@ export function ChooseGroupMembersModal({
</ContactPills>
)}
{rowCount ? (
<Measure bounds>
{({ contentRect, measureRef }: MeasuredComponentProps) => {
// Though `width` and `height` are required properties, we want to be
// careful in case the caller sends bogus data. Notably, react-measure's
// types seem to be inaccurate.
const { width = 100, height = 100 } = contentRect.bounds || {};
if (!width || !height) {
return null;
}
<SizeObserver>
{(ref, size) => {
// We disable this ESLint rule because we're capturing a bubbled keydown
// event. See [this note in the jsx-a11y docs][0].
//
@ -450,16 +441,17 @@ export function ChooseGroupMembersModal({
return (
<div
className="module-AddGroupMembersModal__list-wrapper"
ref={measureRef}
ref={ref}
onKeyDown={event => {
if (event.key === 'Enter') {
inputRef.current?.focus();
}
}}
>
{size != null && (
<ListView
width={width}
height={height}
width={size.width}
height={size.height}
rowCount={rowCount}
calculateRowHeight={index => {
const row = getRow(index);
@ -477,11 +469,12 @@ export function ChooseGroupMembersModal({
}}
rowRenderer={renderItem}
/>
)}
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
}}
</Measure>
</SizeObserver>
) : (
<div className="module-AddGroupMembersModal__no-candidate-contacts">
{i18n('icu:noContactsFound')}

View file

@ -0,0 +1,188 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { RefObject } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { strictAssert } from '../util/assert';
export type Size = Readonly<{
width: number;
height: number;
}>;
export type SizeChangeHandler = (size: Size) => void;
export function isSameSize(a: Size, b: Size): boolean {
return a.width === b.width && a.height === b.height;
}
export function useSizeObserver<T extends Element = Element>(
ref: RefObject<T>,
/**
* Note: If you provide `onSizeChange`, `useSizeObserver()` will always return `null`
*/
onSizeChange?: SizeChangeHandler
): Size | null {
const [size, setSize] = useState<Size | null>(null);
const sizeRef = useRef<Size | null>(null);
const onSizeChangeRef = useRef<SizeChangeHandler | void>(onSizeChange);
useEffect(() => {
// This means you don't need to wrap `onSizeChange` with `useCallback()`
onSizeChangeRef.current = onSizeChange;
}, [onSizeChange]);
useEffect(() => {
const observer = new ResizeObserver(entries => {
// It's possible that ResizeObserver emit entries after disconnect()
if (ref.current == null) {
return;
}
// We're only ever observing one element, and `ResizeObserver` for some
// reason is an array of exactly one rect (I assume to support wrapped
// inline elements in the future)
const borderBoxSize = entries[0].borderBoxSize[0];
// We are assuming a horizontal writing-mode here, we could call
// `getBoundingClientRect()` here but MDN says not to. In the future if
// we are adding support for a vertical locale we may need to change this
const next: Size = {
width: borderBoxSize.inlineSize,
height: borderBoxSize.blockSize,
};
const prev = sizeRef.current;
if (prev == null || !isSameSize(prev, next)) {
sizeRef.current = next;
if (onSizeChangeRef.current != null) {
onSizeChangeRef.current(next);
} else {
setSize(next);
}
}
});
strictAssert(
ref.current instanceof Element,
'ref must be assigned to an element'
);
observer.observe(ref.current, {
box: 'border-box',
});
return () => {
observer.disconnect();
};
}, [ref]);
return size;
}
// Note we use `any` for ref below because TypeScript doesn't currently have
// good inference for JSX generics and it creates confusing errors. We have
// a better error being reported by the hook.
export type SizeObserverProps = Readonly<{
/**
* Note: If you provide `onSizeChange`, in `children()` the `size` will always be `null`
*/
onSizeChange?: SizeChangeHandler;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
children(ref: RefObject<any>, size: Size | null): JSX.Element;
}>;
export function SizeObserver({
onSizeChange,
children,
}: SizeObserverProps): JSX.Element {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ref = useRef<any>();
const size = useSizeObserver(ref, onSizeChange);
return children(ref, size);
}
export type Scroll = Readonly<{
scrollTop: number;
scrollHeight: number;
clientHeight: number;
}>;
export type ScrollChangeHandler = (scroll: Scroll) => void;
export function isSameScroll(a: Scroll, b: Scroll): boolean {
return (
a.scrollTop === b.scrollTop &&
a.scrollHeight === b.scrollHeight &&
a.clientHeight === b.clientHeight
);
}
export function isOverflowing(scroll: Scroll): boolean {
return scroll.scrollHeight > scroll.clientHeight;
}
export function isScrolled(scroll: Scroll): boolean {
return scroll.scrollTop > 0;
}
export function isScrolledToBottom(scroll: Scroll, threshold = 0): boolean {
const maxScrollTop = scroll.scrollHeight - scroll.clientHeight;
return scroll.scrollTop >= maxScrollTop - threshold;
}
/**
* We need an extra element because there is no ResizeObserver equivalent for
* `scrollHeight`. You need something measuring the scroll container and an
* inner element wrapping all of its children.
*
* ```
* const scrollerRef = useRef()
* const scrollerInnerRef = useRef()
*
* useScrollObserver(scrollerRef, scrollerInnerRef, (scroll) => {
* setIsOverflowing(isOverflowing(scroll));
* setIsScrolled(isScrolled(scroll));
* setAtBottom(isScrolledToBottom(scroll));
* })
*
* <div ref={scrollerRef} style={{ overflow: "auto" }}>
* <div ref={scrollerInnerRef}>
* {children}
* </div>
* </div>
* ```
*/
export function useScrollObserver(
scrollerRef: RefObject<HTMLElement>,
scrollerInnerRef: RefObject<HTMLElement>,
onScrollChange: (scroll: Scroll) => void
): void {
const scrollRef = useRef<Scroll | null>(null);
const onScrollChangeRef = useRef<ScrollChangeHandler>(onScrollChange);
useEffect(() => {
// This means you don't need to wrap `onScrollChange` with `useCallback()`
onScrollChangeRef.current = onScrollChange;
}, [onScrollChange]);
const onUpdate = useCallback(() => {
const target = scrollerRef.current;
strictAssert(
target instanceof Element,
'ref must be assigned to an element'
);
const next: Scroll = {
scrollTop: target.scrollTop,
scrollHeight: target.scrollHeight,
clientHeight: target.clientHeight,
};
const prev = scrollRef.current;
if (prev == null || !isSameScroll(prev, next)) {
scrollRef.current = next;
onScrollChangeRef.current(next);
}
}, [scrollerRef]);
useSizeObserver(scrollerRef, onUpdate);
useSizeObserver(scrollerInnerRef, onUpdate);
useEffect(() => {
strictAssert(
scrollerRef.current instanceof Element,
'ref must be assigned to an element'
);
const target = scrollerRef.current;
target.addEventListener('scroll', onUpdate, { passive: true });
return () => {
target.removeEventListener('scroll', onUpdate);
};
}, [scrollerRef, onUpdate]);
}

View file

@ -2401,9 +2401,82 @@
{
"rule": "React-useRef",
"path": "ts/components/Modal.tsx",
"line": " const bodyRef = useRef<HTMLDivElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-21T01:40:08.534Z"
"line": " const bodyRef = useRef<HTMLDivElement>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/components/Modal.tsx",
"line": " const bodyInnerRef = useRef<HTMLDivElement>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/components/TextAttachment.tsx",
"line": " const ref = useRef<HTMLDivElement>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const sizeRef = useRef<Size | null>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const onSizeChangeRef = useRef<SizeChangeHandler | void>(onSizeChange);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const ref = useRef<any>();",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " * const scrollerRef = useRef()",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " * const scrollerInnerRef = useRef()",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const scrollRef = useRef<Scroll | null>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const onScrollChangeRef = useRef<ScrollChangeHandler>(onScrollChange);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",

View file

@ -88,7 +88,6 @@ const excludedFilesRegexp = RegExp(
'^node_modules/react-hot-loader/.+',
'^node_modules/react-icon-base/.+',
'^node_modules/react-input-autosize/.+',
'^node_modules/react-measure/.+',
'^node_modules/react-popper/.+',
'^node_modules/react-redux/.+',
'^node_modules/react-router/.+',

View file

@ -1175,7 +1175,7 @@
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.5.0":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.5.0":
version "7.16.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
@ -4238,13 +4238,6 @@
dependencies:
"@types/react" "^17"
"@types/react-measure@2.0.5":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@types/react-measure/-/react-measure-2.0.5.tgz#c1d304e3cab3a1c393342bf377b040628e6c29a8"
integrity sha512-T1Bpt8FlWbDhoInUaNrjTOiVRpRJmrRcqhFJxLGBq1VjaqBLHCvUPapgdKMWEIX4Oqsa1SSKjtNkNJGy6WAAZg==
dependencies:
"@types/react" "*"
"@types/react-redux@7.1.24", "@types/react-redux@^7.1.20":
version "7.1.24"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0"
@ -9932,11 +9925,6 @@ get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
has "^1.0.3"
has-symbols "^1.0.1"
get-node-dimensions@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823"
integrity sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==
get-stdin@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
@ -15538,16 +15526,6 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-measure@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.3.0.tgz#75835d39abec9ae13517f35a819c160997a7a44e"
integrity sha512-dwAvmiOeblj5Dvpnk8Jm7Q8B4THF/f1l1HtKVi0XDecsG6LXwGvzV5R1H32kq3TW6RW64OAf5aoQxpIgLa4z8A==
dependencies:
"@babel/runtime" "^7.2.0"
get-node-dimensions "^1.2.1"
prop-types "^15.6.2"
resize-observer-polyfill "^1.5.0"
react-merge-refs@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06"
@ -16222,11 +16200,6 @@ reserved-words@^0.1.2:
resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1"
integrity sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE=
resize-observer-polyfill@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-alpn@^1.0.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9"