Remove React Virtualized from <Timeline>

This commit is contained in:
Evan Hahn 2022-03-03 14:23:10 -06:00 committed by GitHub
parent 1eafe79905
commit 0c31ad25ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 798 additions and 2512 deletions

View file

@ -1,11 +1,11 @@
// Copyright 2021 Signal Messenger, LLC
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { FunctionComponent, ReactNode } from 'react';
import React, { useRef, useEffect, Children } from 'react';
import { usePrevious } from '../hooks/usePrevious';
import { scrollToBottom } from '../util/scrollToBottom';
import { scrollToBottom } from '../util/scrollUtil';
type PropsType = {
children?: ReactNode;

View file

@ -21,7 +21,7 @@ const SampleMessage = ({
direction,
i18n,
text,
timestamp,
timestampDeltaFromNow,
status,
style,
}: {
@ -29,7 +29,7 @@ const SampleMessage = ({
direction: 'incoming' | 'outgoing';
i18n: LocalizerType;
text: string;
timestamp: number;
timestampDeltaFromNow: number;
status: 'delivered' | 'read' | 'sent';
style?: CSSProperties;
}): JSX.Element => (
@ -51,7 +51,7 @@ const SampleMessage = ({
<span
className={`module-message__metadata__date module-message__metadata__date--${direction}`}
>
{formatTime(i18n, timestamp)}
{formatTime(i18n, Date.now() - timestampDeltaFromNow, Date.now())}
</span>
{direction === 'outgoing' && (
<div
@ -78,7 +78,7 @@ export const SampleMessageBubbles = ({
direction={includeAnotherBubble ? 'outgoing' : 'incoming'}
i18n={i18n}
text={i18n('ChatColorPicker__sampleBubble1')}
timestamp={Date.now() - A_FEW_DAYS_AGO}
timestampDeltaFromNow={A_FEW_DAYS_AGO}
status="read"
style={firstBubbleStyle}
/>
@ -91,7 +91,7 @@ export const SampleMessageBubbles = ({
direction="incoming"
i18n={i18n}
text={i18n('ChatColorPicker__sampleBubble2')}
timestamp={Date.now() - A_FEW_DAYS_AGO / 2}
timestampDeltaFromNow={A_FEW_DAYS_AGO / 2}
status="read"
/>
<br />
@ -103,7 +103,7 @@ export const SampleMessageBubbles = ({
direction="outgoing"
i18n={i18n}
text={i18n('ChatColorPicker__sampleBubble3')}
timestamp={Date.now()}
timestampDeltaFromNow={0}
status="delivered"
style={backgroundStyle}
/>

View file

@ -19,8 +19,8 @@ const getCommonProps = () => ({
conversationId: 'fake-conversation-id',
i18n,
messageId: 'fake-message-id',
messageSizeChanged: action('messageSizeChanged'),
nextItem: undefined,
now: Date.now(),
returnToActiveCall: action('returnToActiveCall'),
startCallingLobby: action('startCallingLobby'),
});

View file

@ -2,8 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react';
import React, { useState, useEffect } from 'react';
import Measure from 'react-measure';
import React from 'react';
import { noop } from 'lodash';
import { SystemMessage } from './SystemMessage';
@ -16,14 +15,12 @@ import {
getCallingIcon,
getCallingNotificationText,
} from '../../util/callingNotification';
import { usePrevious } from '../../hooks/usePrevious';
import { missingCaseError } from '../../util/missingCaseError';
import { Tooltip, TooltipPlacement } from '../Tooltip';
import type { TimelineItemType } from './TimelineItem';
import * as log from '../../logging/log';
export type PropsActionsType = {
messageSizeChanged: (messageId: string, conversationId: string) => void;
returnToActiveCall: () => void;
startCallingLobby: (_: {
conversationId: string;
@ -34,27 +31,14 @@ export type PropsActionsType = {
type PropsHousekeeping = {
i18n: LocalizerType;
conversationId: string;
messageId: string;
nextItem: undefined | TimelineItemType;
now: number;
};
type PropsType = CallingNotificationType & PropsActionsType & PropsHousekeeping;
export const CallingNotification: React.FC<PropsType> = React.memo(props => {
const { conversationId, i18n, messageId, messageSizeChanged } = props;
const [height, setHeight] = useState<null | number>(null);
const previousHeight = usePrevious<null | number>(null, height);
useEffect(() => {
if (height === null) {
return;
}
if (previousHeight !== null && height !== previousHeight) {
messageSizeChanged(messageId, conversationId);
}
}, [height, previousHeight, conversationId, messageId, messageSizeChanged]);
const { i18n, now } = props;
let timestamp: number;
let wasMissed = false;
@ -75,38 +59,25 @@ export const CallingNotification: React.FC<PropsType> = React.memo(props => {
const icon = getCallingIcon(props);
return (
<Measure
bounds
onResize={({ bounds }) => {
if (!bounds) {
log.error('We should be measuring the bounds');
return;
}
setHeight(bounds.height);
}}
>
{({ measureRef }) => (
<SystemMessage
button={renderCallingNotificationButton(props)}
contents={
<>
{getCallingNotificationText(props, i18n)} &middot;{' '}
<MessageTimestamp
direction="outgoing"
i18n={i18n}
timestamp={timestamp}
withImageNoCaption={false}
withSticker={false}
withTapToViewExpired={false}
/>
</>
}
icon={icon}
isError={wasMissed}
ref={measureRef}
/>
)}
</Measure>
<SystemMessage
button={renderCallingNotificationButton(props)}
contents={
<>
{getCallingNotificationText(props, i18n)} &middot;{' '}
<MessageTimestamp
direction="outgoing"
i18n={i18n}
now={now}
timestamp={timestamp}
withImageNoCaption={false}
withSticker={false}
withTapToViewExpired={false}
/>
</>
}
icon={icon}
isError={wasMissed}
/>
);
});

View file

@ -1,4 +1,4 @@
// Copyright 2021 Signal Messenger, LLC
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
@ -19,6 +19,7 @@ const i18n = setupI18n('en', enMessages);
story.add('Default', () => (
<ChangeNumberNotification
now={Date.now()}
sender={getDefaultConversation()}
timestamp={1618894800000}
i18n={i18n}
@ -27,6 +28,7 @@ story.add('Default', () => (
story.add('Long name', () => (
<ChangeNumberNotification
now={Date.now()}
sender={getDefaultConversation({
firstName: '💅😇🖋'.repeat(50),
})}

View file

@ -18,12 +18,13 @@ export type PropsData = {
export type PropsHousekeeping = {
i18n: LocalizerType;
now: number;
};
export type Props = PropsData & PropsHousekeeping;
export const ChangeNumberNotification: React.FC<Props> = props => {
const { i18n, sender, timestamp } = props;
const { i18n, now, sender, timestamp } = props;
return (
<SystemMessage
@ -37,7 +38,7 @@ export const ChangeNumberNotification: React.FC<Props> = props => {
i18n={i18n}
/>
&nbsp;·&nbsp;
<MessageTimestamp i18n={i18n} timestamp={timestamp} />
<MessageTimestamp i18n={i18n} now={now} timestamp={timestamp} />
</>
}
icon="phone"

View file

@ -1,4 +1,4 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
@ -55,7 +55,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
'Fifth',
]}
unblurAvatar={action('unblurAvatar')}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -83,7 +82,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
'Fourth',
]}
unblurAvatar={action('unblurAvatar')}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -106,7 +104,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
updateSharedGroups={updateSharedGroups}
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party', 'Friends 🌿']}
unblurAvatar={action('unblurAvatar')}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -129,7 +126,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
updateSharedGroups={updateSharedGroups}
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
unblurAvatar={action('unblurAvatar')}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -152,7 +148,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
updateSharedGroups={updateSharedGroups}
sharedGroupNames={['NYC Rock Climbers']}
unblurAvatar={action('unblurAvatar')}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -175,7 +170,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
updateSharedGroups={updateSharedGroups}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -198,7 +192,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
updateSharedGroups={updateSharedGroups}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -221,7 +214,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
updateSharedGroups={updateSharedGroups}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -243,7 +235,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -265,7 +256,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -285,7 +275,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -305,7 +294,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -326,7 +314,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -347,7 +334,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -367,7 +353,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
onHeightChange={action('onHeightChange')}
/>
</div>
);
@ -386,7 +371,6 @@ storiesOf('Components/Conversation/ConversationHero', module)
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
onHeightChange={action('onHeightChange')}
/>
</div>
);

View file

@ -1,7 +1,7 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useState } from 'react';
import type { Props as AvatarProps } from '../Avatar';
import { Avatar, AvatarBlur } from '../Avatar';
import { ContactName } from './ContactName';
@ -12,7 +12,6 @@ import type { LocalizerType, ThemeType } from '../../types/Util';
import { ConfirmationDialog } from '../ConfirmationDialog';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
import * as log from '../../logging/log';
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
export type Props = {
@ -22,7 +21,6 @@ export type Props = {
i18n: LocalizerType;
isMe: boolean;
membersCount?: number;
onHeightChange: () => unknown;
phoneNumber?: string;
sharedGroupNames?: Array<string>;
unblurAvatar: () => void;
@ -111,13 +109,10 @@ export const ConversationHero = ({
profileName,
theme,
title,
onHeightChange,
unblurAvatar,
unblurredAvatarPath,
updateSharedGroups,
}: Props): JSX.Element => {
const firstRenderRef = useRef(true);
const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] =
useState(false);
const closeMessageRequestWarning = () => {
@ -129,30 +124,6 @@ export const ConversationHero = ({
updateSharedGroups();
}, [updateSharedGroups]);
const sharedGroupNamesStringified = JSON.stringify(sharedGroupNames);
useEffect(() => {
const isFirstRender = firstRenderRef.current;
if (isFirstRender) {
firstRenderRef.current = false;
return;
}
log.info('ConversationHero: calling onHeightChange');
onHeightChange();
}, [
about,
conversationType,
groupDescription,
isMe,
membersCount,
name,
onHeightChange,
phoneNumber,
profileName,
title,
sharedGroupNamesStringified,
]);
let avatarBlur: AvatarBlur;
let avatarOnClick: undefined | (() => void);
if (

View file

@ -1,4 +1,4 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
@ -83,6 +83,7 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = props => {
audio={audio}
computePeaks={computePeaks}
setActiveAudioID={(id, context) => setActive({ id, context })}
now={Date.now()}
onFirstPlayed={action('onFirstPlayed')}
activeAudioID={active.id}
activeAudioContext={active.context}
@ -131,6 +132,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
getPreferredBadge: overrideProps.getPreferredBadge || (() => undefined),
i18n,
id: text('id', overrideProps.id || ''),
now: Date.now(),
renderingContext: 'storybook',
interactionMode: overrideProps.interactionMode || 'keyboard',
isSticker: isBoolean(overrideProps.isSticker)
@ -149,7 +151,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
markViewed: action('markViewed'),
messageExpanded: action('messageExpanded'),
onHeightChange: action('onHeightChange'),
openConversation: action('openConversation'),
openLink: action('openLink'),
previews: overrideProps.previews || [],

View file

@ -1,7 +1,7 @@
// Copyright 2018-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { RefObject } from 'react';
import type { ReactNode, RefObject } from 'react';
import React from 'react';
import ReactDOM, { createPortal } from 'react-dom';
import classNames from 'classnames';
@ -118,6 +118,7 @@ export type AudioAttachmentProps = {
expirationLength?: number;
expirationTimestamp?: number;
id: string;
now: number;
played: boolean;
showMessageDetail: (id: string) => void;
status?: MessageStatusType;
@ -210,6 +211,7 @@ export type PropsHousekeeping = {
containerWidthBreakpoint: WidthBreakpoint;
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
now: number;
interactionMode: InteractionModeType;
theme: ThemeType;
disableMenu?: boolean;
@ -224,7 +226,6 @@ export type PropsHousekeeping = {
export type PropsActions = {
clearSelectedMessage: () => unknown;
doubleCheckMissingQuoteReference: (messageId: string) => unknown;
onHeightChange: () => unknown;
messageExpanded: (id: string, displayLimit: number) => unknown;
checkForAccount: (identifier: string) => unknown;
@ -471,7 +472,6 @@ export class Message extends React.PureComponent<Props, State> {
}
this.checkExpired();
this.checkForHeightChange(prevProps);
if (
prevProps.status === 'sending' &&
@ -491,24 +491,6 @@ export class Message extends React.PureComponent<Props, State> {
}
}
public checkForHeightChange(prevProps: Props): void {
const { contact, onHeightChange } = this.props;
const willRenderSendMessageButton = Boolean(
contact && contact.firstNumber && contact.isNumberOnSignal
);
const { contact: previousContact } = prevProps;
const previouslyRenderedSendMessageButton = Boolean(
previousContact &&
previousContact.firstNumber &&
previousContact.isNumberOnSignal
);
if (willRenderSendMessageButton !== previouslyRenderedSendMessageButton) {
onHeightChange();
}
}
public startSelectedTimer(): void {
const { clearSelectedMessage, interactionMode } = this.props;
const { isSelected } = this.state;
@ -609,6 +591,7 @@ export class Message extends React.PureComponent<Props, State> {
isTapToViewExpired,
status,
i18n,
now,
text,
textPending,
timestamp,
@ -640,6 +623,7 @@ export class Message extends React.PureComponent<Props, State> {
isShowingImage={this.isShowingImage()}
isSticker={isStickerLike}
isTapToViewExpired={isTapToViewExpired}
now={now}
showMessageDetail={showMessageDetail}
status={status}
textPending={textPending}
@ -705,6 +689,7 @@ export class Message extends React.PureComponent<Props, State> {
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
markViewed,
now,
quote,
readStatus,
reducedMotion,
@ -834,6 +819,7 @@ export class Message extends React.PureComponent<Props, State> {
expirationLength,
expirationTimestamp,
id,
now,
played,
showMessageDetail,
status,
@ -1238,7 +1224,6 @@ export class Message extends React.PureComponent<Props, State> {
i18n,
id,
messageExpanded,
onHeightChange,
openConversation,
status,
text,
@ -1276,7 +1261,6 @@ export class Message extends React.PureComponent<Props, State> {
id={id}
messageExpanded={messageExpanded}
openConversation={openConversation}
onHeightChange={onHeightChange}
text={contents || ''}
textPending={textPending}
/>
@ -1284,13 +1268,9 @@ export class Message extends React.PureComponent<Props, State> {
);
}
public renderError(isCorrectSide: boolean): JSX.Element | null {
private renderError(): ReactNode {
const { status, direction } = this.props;
if (!isCorrectSide) {
return null;
}
if (
status !== 'paused' &&
status !== 'error' &&
@ -1312,10 +1292,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
public renderMenu(
isCorrectSide: boolean,
triggerId: string
): JSX.Element | null {
private renderMenu(triggerId: string): ReactNode {
const {
attachments,
canDownload,
@ -1334,7 +1311,7 @@ export class Message extends React.PureComponent<Props, State> {
selectedReaction,
} = this.props;
if (!isCorrectSide || disableMenu) {
if (disableMenu) {
return null;
}
@ -2462,12 +2439,10 @@ export class Message extends React.PureComponent<Props, State> {
onFocus={this.handleFocus}
ref={this.focusRef}
>
{this.renderError(direction === 'incoming')}
{this.renderMenu(direction === 'outgoing', triggerId)}
{this.renderError()}
{this.renderAvatar()}
{this.renderContainer()}
{this.renderError(direction === 'outgoing')}
{this.renderMenu(direction === 'incoming', triggerId)}
{this.renderMenu(triggerId)}
{this.renderContextMenu(triggerId)}
</div>
);

View file

@ -1,4 +1,4 @@
// Copyright 2021 Signal Messenger, LLC
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useRef, useEffect, useState } from 'react';
@ -27,6 +27,7 @@ export type Props = {
expirationLength?: number;
expirationTimestamp?: number;
id: string;
now: number;
played: boolean;
showMessageDetail: (id: string) => void;
status?: MessageStatusType;
@ -157,6 +158,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
expirationLength,
expirationTimestamp,
id,
now,
played,
showMessageDetail,
status,
@ -539,6 +541,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
isShowingImage={false}
isSticker={false}
isTapToViewExpired={false}
now={now}
showMessageDetail={showMessageDetail}
status={status}
textPending={textPending}

View file

@ -1,4 +1,4 @@
// Copyright 2021 Signal Messenger, LLC
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState } from 'react';
@ -23,7 +23,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
i18n,
id: 'some-id',
messageExpanded: action('messageExpanded'),
onHeightChange: action('onHeightChange'),
text: text('text', overrideProps.text || ''),
});

View file

@ -1,11 +1,10 @@
// Copyright 2021 Signal Messenger, LLC
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect } from 'react';
import React from 'react';
import type { Props as MessageBodyPropsType } from './MessageBody';
import { MessageBody } from './MessageBody';
import { usePrevious } from '../../hooks/usePrevious';
export type Props = Pick<
MessageBodyPropsType,
@ -20,7 +19,6 @@ export type Props = Pick<
id: string;
displayLimit?: number;
messageExpanded: (id: string, displayLimit: number) => unknown;
onHeightChange: () => unknown;
};
const INITIAL_LENGTH = 800;
@ -70,19 +68,11 @@ export function MessageBodyReadMore({
i18n,
id,
messageExpanded,
onHeightChange,
openConversation,
text,
textPending,
}: Props): JSX.Element {
const maxLength = displayLimit || INITIAL_LENGTH;
const previousMaxLength = usePrevious(maxLength, maxLength);
useEffect(() => {
if (previousMaxLength !== maxLength) {
onHeightChange();
}
}, [maxLength, previousMaxLength, onHeightChange]);
const { hasReadMore, text: slicedText } = graphemeAwareSlice(text, maxLength);

View file

@ -23,6 +23,8 @@ import { SendStatus } from '../../messages/MessageSendState';
import { WidthBreakpoint } from '../_util';
import * as log from '../../logging/log';
import { formatDateTimeLong } from '../../util/timestamp';
import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary';
import { MINUTE } from '../../util/durations';
export type Contact = Pick<
ConversationType,
@ -98,16 +100,20 @@ export type PropsReduxActions = Pick<
export type ExternalProps = PropsData & PropsBackboneActions;
export type Props = PropsData & PropsBackboneActions & PropsReduxActions;
type State = { nowThatUpdatesEveryMinute: number };
const contactSortCollator = new Intl.Collator();
const _keyForError = (error: Error): string => {
return `${error.name}-${error.message}`;
};
export class MessageDetail extends React.Component<Props> {
private readonly focusRef = React.createRef<HTMLDivElement>();
export class MessageDetail extends React.Component<Props, State> {
override state = { nowThatUpdatesEveryMinute: Date.now() };
private readonly focusRef = React.createRef<HTMLDivElement>();
private readonly messageContainerRef = React.createRef<HTMLDivElement>();
private nowThatUpdatesEveryMinuteInterval?: ReturnType<typeof setInterval>;
public override componentDidMount(): void {
// When this component is created, it's initially not part of the DOM, and then it's
@ -117,6 +123,14 @@ export class MessageDetail extends React.Component<Props> {
this.focusRef.current.focus();
}
});
this.nowThatUpdatesEveryMinuteInterval = setInterval(() => {
this.setState({ nowThatUpdatesEveryMinute: Date.now() });
}, MINUTE);
}
public override componentWillUnmount(): void {
clearTimeoutIfNecessary(this.nowThatUpdatesEveryMinuteInterval);
}
public renderAvatar(contact: Contact): JSX.Element {
@ -298,6 +312,7 @@ export class MessageDetail extends React.Component<Props> {
showVisualAttachment,
theme,
} = this.props;
const { nowThatUpdatesEveryMinute } = this.state;
return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
@ -335,7 +350,7 @@ export class MessageDetail extends React.Component<Props> {
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
markViewed={markViewed}
messageExpanded={noop}
onHeightChange={noop}
now={nowThatUpdatesEveryMinute}
openConversation={openConversation}
openLink={openLink}
reactToMessage={reactToMessage}

View file

@ -22,6 +22,7 @@ type PropsType = {
isShowingImage: boolean;
isSticker?: boolean;
isTapToViewExpired?: boolean;
now: number;
showMessageDetail: (id: string) => void;
status?: MessageStatusType;
textPending?: boolean;
@ -40,6 +41,7 @@ export const MessageMetadata: FunctionComponent<PropsType> = props => {
isShowingImage,
isSticker,
isTapToViewExpired,
now,
showMessageDetail,
status,
textPending,
@ -97,6 +99,7 @@ export const MessageMetadata: FunctionComponent<PropsType> = props => {
<MessageTimestamp
i18n={i18n}
timestamp={timestamp}
now={now}
direction={metadataDirection}
withImageNoCaption={withImageNoCaption}
withSticker={isSticker}

View file

@ -42,6 +42,7 @@ const times = (): Array<[string, number]> => [
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
i18n,
timestamp: overrideProps.timestamp,
now: Date.now(),
module: text('module', ''),
withImageNoCaption: boolean('withImageNoCaption', false),
withSticker: boolean('withSticker', false),

View file

@ -1,16 +1,17 @@
// Copyright 2018-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement } from 'react';
import React from 'react';
import classNames from 'classnames';
import moment from 'moment';
import { formatTime } from '../../util/timestamp';
import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary';
import type { LocalizerType } from '../../types/Util';
import { Time } from '../Time';
export type Props = {
now: number;
timestamp?: number;
module?: string;
withImageNoCaption?: boolean;
@ -20,63 +21,36 @@ export type Props = {
i18n: LocalizerType;
};
const UPDATE_FREQUENCY = 60 * 1000;
export function MessageTimestamp({
direction,
i18n,
module,
now,
timestamp,
withImageNoCaption,
withSticker,
withTapToViewExpired,
}: Readonly<Props>): null | ReactElement {
const moduleName = module || 'module-timestamp';
export class MessageTimestamp extends React.Component<Props> {
private interval: NodeJS.Timeout | null;
constructor(props: Props) {
super(props);
this.interval = null;
if (timestamp === null || timestamp === undefined) {
return null;
}
public override componentDidMount(): void {
const update = () => {
this.setState({
// Used to trigger renders
// eslint-disable-next-line react/no-unused-state
lastUpdated: Date.now(),
});
};
this.interval = setInterval(update, UPDATE_FREQUENCY);
}
public override componentWillUnmount(): void {
clearTimeoutIfNecessary(this.interval);
}
public override render(): JSX.Element | null {
const {
direction,
i18n,
module,
timestamp,
withImageNoCaption,
withSticker,
withTapToViewExpired,
} = this.props;
const moduleName = module || 'module-timestamp';
if (timestamp === null || timestamp === undefined) {
return null;
}
return (
<span
className={classNames(
moduleName,
direction ? `${moduleName}--${direction}` : null,
withTapToViewExpired && direction
? `${moduleName}--${direction}-with-tap-to-view-expired`
: null,
withImageNoCaption ? `${moduleName}--with-image-no-caption` : null,
withSticker ? `${moduleName}--with-sticker` : null
)}
title={moment(timestamp).format('llll')}
>
{formatTime(i18n, timestamp)}
</span>
);
}
return (
<Time
className={classNames(
moduleName,
direction ? `${moduleName}--${direction}` : null,
withTapToViewExpired && direction
? `${moduleName}--${direction}-with-tap-to-view-expired`
: null,
withImageNoCaption ? `${moduleName}--with-image-no-caption` : null,
withSticker ? `${moduleName}--with-sticker` : null
)}
timestamp={timestamp}
>
{formatTime(i18n, timestamp, now)}
</Time>
);
}

View file

@ -1,4 +1,4 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
@ -59,6 +59,7 @@ const defaultMessageProps: MessagesProps = {
getPreferredBadge: () => undefined,
i18n,
id: 'messageId',
now: Date.now(),
renderingContext: 'storybook',
interactionMode: 'keyboard',
isBlocked: false,
@ -67,7 +68,6 @@ const defaultMessageProps: MessagesProps = {
markAttachmentAsCorrupted: action('default--markAttachmentAsCorrupted'),
markViewed: action('default--markViewed'),
messageExpanded: action('default--message-expanded'),
onHeightChange: action('default--onHeightChange'),
openConversation: action('default--openConversation'),
openLink: action('default--openLink'),
previews: [],

View file

@ -324,11 +324,9 @@ const actions = () => ({
'acknowledgeGroupMemberNameCollisions'
),
checkForAccount: action('checkForAccount'),
clearChangedMessages: action('clearChangedMessages'),
clearInvitedUuidsForNewlyCreatedGroup: action(
'clearInvitedUuidsForNewlyCreatedGroup'
),
setLoadCountdownStart: action('setLoadCountdownStart'),
setIsNearBottom: action('setIsNearBottom'),
learnMoreAboutDeliveryIssue: action('learnMoreAboutDeliveryIssue'),
loadAndScroll: action('loadAndScroll'),
@ -358,7 +356,6 @@ const actions = () => ({
displayTapToViewMessage: action('displayTapToViewMessage'),
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
onHeightChange: action('onHeightChange'),
openLink: action('openLink'),
scrollToQuotedMessage: action('scrollToQuotedMessage'),
showExpiredIncomingTapToViewToast: action(
@ -373,7 +370,6 @@ const actions = () => ({
downloadNewVersion: action('downloadNewVersion'),
messageSizeChanged: action('messageSizeChanged'),
startCallingLobby: action('startCallingLobby'),
returnToActiveCall: action('returnToActiveCall'),
@ -401,11 +397,13 @@ const renderItem = ({
containerElementRef,
containerWidthBreakpoint,
isOldestTimelineItem,
now,
}: {
messageId: string;
containerElementRef: React.RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint;
isOldestTimelineItem: boolean;
now: number;
}) => (
<TimelineItem
getPreferredBadge={() => undefined}
@ -417,6 +415,7 @@ const renderItem = ({
item={items[messageId]}
previousItem={undefined}
nextItem={undefined}
now={now}
i18n={i18n}
interactionMode="keyboard"
theme={ThemeType.light}
@ -460,7 +459,6 @@ const renderHeroRow = () => {
profileName={getProfileName()}
phoneNumber={getPhoneNumber()}
conversationType="direct"
onHeightChange={action('onHeightChange in ConversationHero')}
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
theme={theme}
unblurAvatar={action('unblurAvatar')}
@ -486,6 +484,7 @@ const renderTypingBubble = () => (
);
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
discardMessages: action('discardMessages'),
getPreferredBadge: () => undefined,
i18n,
theme: React.useContext(StorybookThemeContext),
@ -493,6 +492,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
getTimestampForMessage: Date.now,
haveNewest: boolean('haveNewest', overrideProps.haveNewest !== false),
haveOldest: boolean('haveOldest', overrideProps.haveOldest !== false),
isConversationSelected: true,
isIncomingMessageRequest: boolean(
'isIncomingMessageRequest',
overrideProps.isIncomingMessageRequest === true
@ -502,7 +502,6 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
overrideProps.isLoadingMessages === false
),
items: overrideProps.items || Object.keys(items),
resetCounter: 0,
scrollToIndex: overrideProps.scrollToIndex,
scrollToIndexCounter: 0,
totalUnread: number('totalUnread', overrideProps.totalUnread || 0),

File diff suppressed because it is too large Load diff

View file

@ -86,16 +86,15 @@ const getDefaultProps = () => ({
showExpiredOutgoingTapToViewToast: action(
'showExpiredIncomingTapToViewToast'
),
onHeightChange: action('onHeightChange'),
openLink: action('openLink'),
scrollToQuotedMessage: action('scrollToQuotedMessage'),
downloadNewVersion: action('downloadNewVersion'),
showIdentity: action('showIdentity'),
messageSizeChanged: action('messageSizeChanged'),
startCallingLobby: action('startCallingLobby'),
returnToActiveCall: action('returnToActiveCall'),
previousItem: undefined,
nextItem: undefined,
now: Date.now(),
renderContact,
renderUniversalTimerNotification,

View file

@ -54,7 +54,6 @@ import type { SmartContactRendererType } from '../../groupChange';
import { ResetSessionNotification } from './ResetSessionNotification';
import type { PropsType as ProfileChangeNotificationPropsType } from './ProfileChangeNotification';
import { ProfileChangeNotification } from './ProfileChangeNotification';
import * as log from '../../logging/log';
import type { FullJSXType } from '../Intl';
type CallHistoryType = {
@ -156,6 +155,7 @@ type PropsLocalType = {
theme: ThemeType;
previousItem: undefined | TimelineItemType;
nextItem: undefined | TimelineItemType;
now: number;
};
type PropsActionsType = MessageActionsType &
@ -188,8 +188,8 @@ export class TimelineItem extends React.PureComponent<PropsType> {
item,
i18n,
theme,
messageSizeChanged,
nextItem,
now,
previousItem,
renderContact,
renderUniversalTimerNotification,
@ -199,8 +199,12 @@ export class TimelineItem extends React.PureComponent<PropsType> {
} = this.props;
if (!item) {
log.warn(`TimelineItem: item ${id} provided was falsey`);
// This can happen under normal conditions.
//
// `<Timeline>` and `<TimelineItem>` are connected to Redux separately. If a
// timeline item is removed from Redux, `<TimelineItem>` might re-render before
// `<Timeline>` does, which means we'll try to render nothing. This should resolve
// itself quickly, as soon as `<Timeline>` re-renders.
return null;
}
@ -229,9 +233,8 @@ export class TimelineItem extends React.PureComponent<PropsType> {
<CallingNotification
conversationId={conversationId}
i18n={i18n}
messageId={id}
messageSizeChanged={messageSizeChanged}
nextItem={nextItem}
now={now}
returnToActiveCall={returnToActiveCall}
startCallingLobby={startCallingLobby}
{...item.data}