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

@ -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}
>
<ListView
width={width}
height={height}
rowCount={filteredConversations.length}
calculateRowHeight={handleCalculateRowHeight}
rowRenderer={renderGroupListItem}
/>
{size != null && (
<ListView
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
)}
onScroll={() => {
const scrollTop = bodyRef.current?.scrollTop || 0;
setScrolled(scrollTop > 2);
}}
ref={refMerger(measureRef, bodyRef)}
>
{children}
</div>
<div
className={classNames(
getClassName('__body'),
scrolled ? getClassName('__body--scrolled') : null,
hasOverflow || scrolled ? getClassName('__body--overflow') : null
)}
</Measure>
ref={bodyRef}
>
<div ref={bodyInnerRef}>{children}</div>
</div>
{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,152 +169,142 @@ 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);
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className="TextAttachment"
onClick={() => {
if (linkPreviewOffsetTop) {
setLinkPreviewOffsetTop(undefined);
}
onClick?.();
}}
onKeyUp={ev => {
if (ev.key === 'Escape' && linkPreviewOffsetTop) {
setLinkPreviewOffsetTop(undefined);
}
}}
ref={measureRef}
style={isThumbnail ? storyBackgroundColor : undefined}
>
{/*
const scaleFactor = (size?.height || 1) / 1280;
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className="TextAttachment"
onClick={() => {
if (linkPreviewOffsetTop) {
setLinkPreviewOffsetTop(undefined);
}
onClick?.();
}}
onKeyUp={ev => {
if (ev.key === 'Escape' && linkPreviewOffsetTop) {
setLinkPreviewOffsetTop(undefined);
}
}}
ref={ref}
style={isThumbnail ? storyBackgroundColor : undefined}
>
{/*
The tooltip must be outside of the scaled area, as it should not scale with
the story, but it must be positioned using the scaled offset
*/}
{textAttachment.preview &&
textAttachment.preview.url &&
linkPreviewOffsetTop &&
!isThumbnail && (
<a
className="TextAttachment__preview__tooltip"
href={textAttachment.preview.url}
rel="noreferrer"
style={{
top: linkPreviewOffsetTop * scaleFactor - 89, // minus height of tooltip and some spacing
}}
target="_blank"
>
<div>
<div className="TextAttachment__preview__tooltip__title">
{i18n('icu:TextAttachment__preview__link')}
</div>
<div className="TextAttachment__preview__tooltip__url">
{textAttachment.preview.url}
</div>
</div>
<div className="TextAttachment__preview__tooltip__arrow" />
</a>
)}
<div
className="TextAttachment__story"
style={{
...(isThumbnail ? {} : storyBackgroundColor),
transform: `scale(${scaleFactor})`,
}}
>
{(textContent || onChange) && (
<div
className={classNames('TextAttachment__text', {
'TextAttachment__text--with-bg': Boolean(
textAttachment.textBackgroundColor
),
})}
style={{
backgroundColor: textAttachment.textBackgroundColor
? getHexFromNumber(textAttachment.textBackgroundColor)
: 'transparent',
}}
>
{onChange ? (
<TextareaAutosize
dir="auto"
className="TextAttachment__text__container TextAttachment__text__textarea"
disabled={!isEditingText}
onChange={ev => onChange(ev.currentTarget.value)}
placeholder={i18n('icu:TextAttachment__placeholder')}
ref={refMerger(forwardedTextEditorRef, textEditorRef)}
style={getTextStyles(
textContent,
textAttachment.textForegroundColor,
textAttachment.textStyle,
i18n
)}
value={textContent}
/>
) : (
<div
className="TextAttachment__text__container"
style={getTextStyles(
textContent,
textAttachment.textForegroundColor,
textAttachment.textStyle,
i18n
)}
>
<Emojify
text={textContent}
renderNonEmoji={renderNewLines}
/>
</div>
)}
</div>
)}
{textAttachment.preview && textAttachment.preview.url && (
<div
className={classNames('TextAttachment__preview-container', {
'TextAttachment__preview-container--large': Boolean(
textAttachment.preview.title
),
})}
ref={linkPreview}
onBlur={() => setIsHoveringOverTooltip(false)}
onFocus={showTooltip}
onMouseOut={() => setIsHoveringOverTooltip(false)}
onMouseOver={showTooltip}
>
{onRemoveLinkPreview && (
<div className="TextAttachment__preview__remove">
<button
aria-label={i18n(
'icu:Keyboard--remove-draft-link-preview'
)}
type="button"
onClick={onRemoveLinkPreview}
/>
</div>
)}
<StoryLinkPreview
{...textAttachment.preview}
domain={getDomain(String(textAttachment.preview.url))}
forceCompactMode={
getTextSize(textContent) !== TextSize.Large
}
i18n={i18n}
title={textAttachment.preview.title || undefined}
url={textAttachment.preview.url}
/>
</div>
)}
{textAttachment.preview &&
textAttachment.preview.url &&
linkPreviewOffsetTop &&
!isThumbnail && (
<a
className="TextAttachment__preview__tooltip"
href={textAttachment.preview.url}
rel="noreferrer"
style={{
top: linkPreviewOffsetTop * scaleFactor - 89, // minus height of tooltip and some spacing
}}
target="_blank"
>
<div>
<div className="TextAttachment__preview__tooltip__title">
{i18n('icu:TextAttachment__preview__link')}
</div>
<div className="TextAttachment__preview__tooltip__url">
{textAttachment.preview.url}
</div>
</div>
<div className="TextAttachment__preview__tooltip__arrow" />
</a>
)}
<div
className="TextAttachment__story"
style={{
...(isThumbnail ? {} : storyBackgroundColor),
transform: `scale(${scaleFactor})`,
}}
>
{(textContent || onChange) && (
<div
className={classNames('TextAttachment__text', {
'TextAttachment__text--with-bg': Boolean(
textAttachment.textBackgroundColor
),
})}
style={{
backgroundColor: textAttachment.textBackgroundColor
? getHexFromNumber(textAttachment.textBackgroundColor)
: 'transparent',
}}
>
{onChange ? (
<TextareaAutosize
dir="auto"
className="TextAttachment__text__container TextAttachment__text__textarea"
disabled={!isEditingText}
onChange={ev => onChange(ev.currentTarget.value)}
placeholder={i18n('icu:TextAttachment__placeholder')}
ref={refMerger(forwardedTextEditorRef, textEditorRef)}
style={getTextStyles(
textContent,
textAttachment.textForegroundColor,
textAttachment.textStyle,
i18n
)}
value={textContent}
/>
) : (
<div
className="TextAttachment__text__container"
style={getTextStyles(
textContent,
textAttachment.textForegroundColor,
textAttachment.textStyle,
i18n
)}
>
<Emojify text={textContent} renderNonEmoji={renderNewLines} />
</div>
)}
</div>
);
}}
</Measure>
)}
{textAttachment.preview && textAttachment.preview.url && (
<div
className={classNames('TextAttachment__preview-container', {
'TextAttachment__preview-container--large': Boolean(
textAttachment.preview.title
),
})}
ref={linkPreview}
onBlur={() => setIsHoveringOverTooltip(false)}
onFocus={showTooltip}
onMouseOut={() => setIsHoveringOverTooltip(false)}
onMouseOver={showTooltip}
>
{onRemoveLinkPreview && (
<div className="TextAttachment__preview__remove">
<button
aria-label={i18n('icu:Keyboard--remove-draft-link-preview')}
type="button"
onClick={onRemoveLinkPreview}
/>
</div>
)}
<StoryLinkPreview
{...textAttachment.preview}
domain={getDomain(String(textAttachment.preview.url))}
forceCompactMode={getTextSize(textContent) !== TextSize.Large}
i18n={i18n}
title={textAttachment.preview.title || undefined}
url={textAttachment.preview.url}
/>
</div>
)}
</div>
</div>
);
}
);

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;
}
// Observer was updated from under us
if (this.intersectionObserver !== observer) {
return;
}
intersectionObserverCallback(entries, observer);
}
);
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,38 +441,40 @@ export function ChooseGroupMembersModal({
return (
<div
className="module-AddGroupMembersModal__list-wrapper"
ref={measureRef}
ref={ref}
onKeyDown={event => {
if (event.key === 'Enter') {
inputRef.current?.focus();
}
}}
>
<ListView
width={width}
height={height}
rowCount={rowCount}
calculateRowHeight={index => {
const row = getRow(index);
if (!row) {
assertDev(false, `Expected a row at index ${index}`);
return 52;
}
switch (row.type) {
case RowType.Header:
return 40;
default:
{size != null && (
<ListView
width={size.width}
height={size.height}
rowCount={rowCount}
calculateRowHeight={index => {
const row = getRow(index);
if (!row) {
assertDev(false, `Expected a row at index ${index}`);
return 52;
}
}}
rowRenderer={renderItem}
/>
}
switch (row.type) {
case RowType.Header:
return 40;
default:
return 52;
}
}}
rowRenderer={renderItem}
/>
)}
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
}}
</Measure>
</SizeObserver>
) : (
<div className="module-AddGroupMembersModal__no-candidate-contacts">
{i18n('icu:noContactsFound')}