Add useSizeObserver and replace most react-measure
This commit is contained in:
parent
7267391de4
commit
6c70cd450b
20 changed files with 539 additions and 421 deletions
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')}
|
||||
|
|
188
ts/hooks/useSizeObserver.tsx
Normal file
188
ts/hooks/useSizeObserver.tsx
Normal 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]);
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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/.+',
|
||||
|
|
29
yarn.lock
29
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue