From 4e48d7792b495f3fe47b1fccdda66298d7d65983 Mon Sep 17 00:00:00 2001 From: Josh Perez <60019601+josh-signal@users.noreply.github.com> Date: Tue, 8 Mar 2022 14:11:11 -0500 Subject: [PATCH] Use a hook for the ever-updating now --- ts/components/StoryListItem.tsx | 1 - ts/components/StoryViewer.tsx | 1 - ts/components/StoryViewsNRepliesModal.tsx | 3 --- .../conversation/CallingNotification.tsx | 4 +-- .../ChangeNumberNotification.stories.tsx | 2 -- .../conversation/ChangeNumberNotification.tsx | 5 ++-- .../conversation/Message.stories.tsx | 2 -- ts/components/conversation/Message.tsx | 6 ----- ts/components/conversation/MessageAudio.tsx | 3 --- ts/components/conversation/MessageDetail.tsx | 19 +------------ .../conversation/MessageMetadata.tsx | 3 --- .../conversation/MessageTimestamp.stories.tsx | 1 - .../conversation/MessageTimestamp.tsx | 4 +-- ts/components/conversation/Quote.stories.tsx | 1 - .../conversation/Timeline.stories.tsx | 3 --- ts/components/conversation/Timeline.tsx | 17 +----------- ts/components/conversation/TimelineItem.tsx | 3 --- ts/hooks/useNowThatUpdatesEveryMinute.ts | 27 +++++++++++++++++++ ts/state/smart/MessageAudio.tsx | 1 - ts/state/smart/Timeline.tsx | 3 --- ts/state/smart/TimelineItem.tsx | 3 --- 21 files changed, 34 insertions(+), 78 deletions(-) create mode 100644 ts/hooks/useNowThatUpdatesEveryMinute.ts diff --git a/ts/components/StoryListItem.tsx b/ts/components/StoryListItem.tsx index 9ba87ebce4..992e4b94cb 100644 --- a/ts/components/StoryListItem.tsx +++ b/ts/components/StoryListItem.tsx @@ -163,7 +163,6 @@ export const StoryListItem = ({ diff --git a/ts/components/StoryViewer.tsx b/ts/components/StoryViewer.tsx index 4232351bca..de8863d7b2 100644 --- a/ts/components/StoryViewer.tsx +++ b/ts/components/StoryViewer.tsx @@ -233,7 +233,6 @@ export const StoryViewer = ({
diff --git a/ts/components/StoryViewsNRepliesModal.tsx b/ts/components/StoryViewsNRepliesModal.tsx index a91f224c00..72b7d66377 100644 --- a/ts/components/StoryViewsNRepliesModal.tsx +++ b/ts/components/StoryViewsNRepliesModal.tsx @@ -249,7 +249,6 @@ export const StoryViewsNRepliesModal = ({
@@ -285,7 +284,6 @@ export const StoryViewsNRepliesModal = ({ @@ -324,7 +322,6 @@ export const StoryViewsNRepliesModal = ({ diff --git a/ts/components/conversation/CallingNotification.tsx b/ts/components/conversation/CallingNotification.tsx index 38a97662ce..ba4e8a6b40 100644 --- a/ts/components/conversation/CallingNotification.tsx +++ b/ts/components/conversation/CallingNotification.tsx @@ -32,13 +32,12 @@ type PropsHousekeeping = { i18n: LocalizerType; conversationId: string; nextItem: undefined | TimelineItemType; - now: number; }; type PropsType = CallingNotificationType & PropsActionsType & PropsHousekeeping; export const CallingNotification: React.FC = React.memo(props => { - const { i18n, now } = props; + const { i18n } = props; let timestamp: number; let wasMissed = false; @@ -67,7 +66,6 @@ export const CallingNotification: React.FC = React.memo(props => { ( ( story.add('Long name', () => ( = props => { - const { i18n, now, sender, timestamp } = props; + const { i18n, sender, timestamp } = props; return ( = props => { i18n={i18n} />  ยท  - + } icon="phone" diff --git a/ts/components/conversation/Message.stories.tsx b/ts/components/conversation/Message.stories.tsx index ed95f81997..a8e93ee4bf 100644 --- a/ts/components/conversation/Message.stories.tsx +++ b/ts/components/conversation/Message.stories.tsx @@ -84,7 +84,6 @@ const MessageAudioContainer: React.FC = props => { audio={audio} computePeaks={computePeaks} setActiveAudioID={(id, context) => setActive({ id, context })} - now={Date.now()} onFirstPlayed={action('onFirstPlayed')} activeAudioID={active.id} activeAudioContext={active.context} @@ -134,7 +133,6 @@ const createProps = (overrideProps: Partial = {}): Props => ({ getPreferredBadge: overrideProps.getPreferredBadge || (() => undefined), i18n, id: text('id', overrideProps.id || ''), - now: Date.now(), renderingContext: 'storybook', interactionMode: overrideProps.interactionMode || 'keyboard', isSticker: isBoolean(overrideProps.isSticker) diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 7b19df5b5d..97778f6349 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -143,7 +143,6 @@ export type AudioAttachmentProps = { expirationLength?: number; expirationTimestamp?: number; id: string; - now: number; played: boolean; showMessageDetail: (id: string) => void; status?: MessageStatusType; @@ -240,7 +239,6 @@ export type PropsHousekeeping = { disableScroll?: boolean; getPreferredBadge: PreferredBadgeSelectorType; i18n: LocalizerType; - now: number; interactionMode: InteractionModeType; item?: TimelineItemType; nextItem?: TimelineItemType; @@ -709,7 +707,6 @@ export class Message extends React.PureComponent { isTapToViewExpired, status, i18n, - now, text, textPending, timestamp, @@ -732,7 +729,6 @@ export class Message extends React.PureComponent { isShowingImage={this.isShowingImage()} isSticker={isStickerLike} isTapToViewExpired={isTapToViewExpired} - now={now} onWidthMeasured={isInline ? this.updateMetadataWidth : undefined} showMessageDetail={showMessageDetail} status={status} @@ -786,7 +782,6 @@ export class Message extends React.PureComponent { kickOffAttachmentDownload, markAttachmentAsCorrupted, markViewed, - now, quote, readStatus, reducedMotion, @@ -922,7 +917,6 @@ export class Message extends React.PureComponent { expirationLength, expirationTimestamp, id, - now, played, showMessageDetail, status, diff --git a/ts/components/conversation/MessageAudio.tsx b/ts/components/conversation/MessageAudio.tsx index 5b987765e2..9bbfda5714 100644 --- a/ts/components/conversation/MessageAudio.tsx +++ b/ts/components/conversation/MessageAudio.tsx @@ -28,7 +28,6 @@ export type Props = { expirationLength?: number; expirationTimestamp?: number; id: string; - now: number; played: boolean; showMessageDetail: (id: string) => void; status?: MessageStatusType; @@ -160,7 +159,6 @@ export const MessageAudio: React.FC = (props: Props) => { expirationLength, expirationTimestamp, id, - now, played, showMessageDetail, status, @@ -543,7 +541,6 @@ export const MessageAudio: React.FC = (props: Props) => { isShowingImage={false} isSticker={false} isTapToViewExpired={false} - now={now} showMessageDetail={showMessageDetail} status={status} textPending={textPending} diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index 622261112f..19056150d3 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -23,8 +23,6 @@ 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, @@ -101,20 +99,15 @@ 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 { - override state = { nowThatUpdatesEveryMinute: Date.now() }; - +export class MessageDetail extends React.Component { private readonly focusRef = React.createRef(); private readonly messageContainerRef = React.createRef(); - private nowThatUpdatesEveryMinuteInterval?: ReturnType; public override componentDidMount(): void { // When this component is created, it's initially not part of the DOM, and then it's @@ -124,14 +117,6 @@ export class MessageDetail extends React.Component { 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 { @@ -314,7 +299,6 @@ export class MessageDetail extends React.Component { showVisualAttachment, theme, } = this.props; - const { nowThatUpdatesEveryMinute } = this.state; return ( // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex @@ -352,7 +336,6 @@ export class MessageDetail extends React.Component { markAttachmentAsCorrupted={markAttachmentAsCorrupted} markViewed={markViewed} messageExpanded={noop} - now={nowThatUpdatesEveryMinute} openConversation={openConversation} openLink={openLink} reactToMessage={reactToMessage} diff --git a/ts/components/conversation/MessageMetadata.tsx b/ts/components/conversation/MessageMetadata.tsx index 71dcfd0a68..9b3780652e 100644 --- a/ts/components/conversation/MessageMetadata.tsx +++ b/ts/components/conversation/MessageMetadata.tsx @@ -24,7 +24,6 @@ type PropsType = { isShowingImage: boolean; isSticker?: boolean; isTapToViewExpired?: boolean; - now: number; onWidthMeasured?: (width: number) => unknown; showMessageDetail: (id: string) => void; status?: MessageStatusType; @@ -44,7 +43,6 @@ export const MessageMetadata = ({ isShowingImage, isSticker, isTapToViewExpired, - now, onWidthMeasured, showMessageDetail, status, @@ -106,7 +104,6 @@ export const MessageMetadata = ({ => [ const createProps = (overrideProps: Partial = {}): Props => ({ i18n, timestamp: overrideProps.timestamp || Date.now(), - now: Date.now(), module: text('module', ''), withImageNoCaption: boolean('withImageNoCaption', false), withSticker: boolean('withSticker', false), diff --git a/ts/components/conversation/MessageTimestamp.tsx b/ts/components/conversation/MessageTimestamp.tsx index 219b8819b9..c151a34566 100644 --- a/ts/components/conversation/MessageTimestamp.tsx +++ b/ts/components/conversation/MessageTimestamp.tsx @@ -9,9 +9,9 @@ import { formatTime } from '../../util/timestamp'; import type { LocalizerType } from '../../types/Util'; import { Time } from '../Time'; +import { useNowThatUpdatesEveryMinute } from '../../hooks/useNowThatUpdatesEveryMinute'; export type Props = { - now: number; timestamp: number; module?: string; withImageNoCaption?: boolean; @@ -25,12 +25,12 @@ export function MessageTimestamp({ direction, i18n, module, - now, timestamp, withImageNoCaption, withSticker, withTapToViewExpired, }: Readonly): ReactElement { + const now = useNowThatUpdatesEveryMinute(); const moduleName = module || 'module-timestamp'; return ( diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index a578586202..37074e7f60 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -61,7 +61,6 @@ const defaultMessageProps: MessagesProps = { getPreferredBadge: () => undefined, i18n, id: 'messageId', - now: Date.now(), renderingContext: 'storybook', interactionMode: 'keyboard', isBlocked: false, diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index 327e50da0c..6de37f212f 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -414,13 +414,11 @@ const renderItem = ({ containerElementRef, containerWidthBreakpoint, isOldestTimelineItem, - now, }: { messageId: string; containerElementRef: React.RefObject; containerWidthBreakpoint: WidthBreakpoint; isOldestTimelineItem: boolean; - now: number; }) => ( undefined} @@ -432,7 +430,6 @@ const renderItem = ({ item={items[messageId]} previousItem={undefined} nextItem={undefined} - now={now} i18n={i18n} interactionMode="keyboard" theme={ThemeType.light} diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index fa21d64c49..ef8a9067e3 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -41,7 +41,6 @@ import { scrollToBottom, setScrollBottom, } from '../../util/scrollUtil'; -import { MINUTE } from '../../util/durations'; const AT_BOTTOM_THRESHOLD = 15; const MIN_ROW_HEIGHT = 18; @@ -118,7 +117,6 @@ type PropsHousekeepingType = { isOldestTimelineItem: boolean; messageId: string; nextMessageId: undefined | string; - now: number; previousMessageId: undefined | string; unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement; }) => JSX.Element; @@ -175,7 +173,6 @@ type StateType = { hasRecentlyScrolled: boolean; lastMeasuredWarningHeight: number; newestFullyVisibleMessageId?: string; - nowThatUpdatesEveryMinute: number; oldestPartiallyVisibleMessageId?: string; widthBreakpoint: WidthBreakpoint; }; @@ -269,12 +266,10 @@ export class Timeline extends React.Component< private hasRecentlyScrolledTimeout?: NodeJS.Timeout; private delayedPeekTimeout?: NodeJS.Timeout; - private nowThatUpdatesEveryMinuteInterval?: NodeJS.Timeout; override state: StateType = { hasRecentlyScrolled: true, hasDismissedDirectContactSpoofingWarning: false, - nowThatUpdatesEveryMinute: Date.now(), // These may be swiftly overridden. lastMeasuredWarningHeight: 0, @@ -512,17 +507,10 @@ export class Timeline extends React.Component< const { id, peekGroupCallForTheFirstTime } = this.props; peekGroupCallForTheFirstTime(id); }, 500); - - this.nowThatUpdatesEveryMinuteInterval = setInterval(() => { - this.setState({ nowThatUpdatesEveryMinute: Date.now() }); - }, MINUTE); } public override componentWillUnmount(): void { - const { - delayedPeekTimeout, - nowThatUpdatesEveryMinuteInterval: nowThatUpdatesEveryMinuteTimeout, - } = this; + const { delayedPeekTimeout } = this; window.unregisterForActive(this.markNewestFullyVisibleMessageRead); @@ -530,7 +518,6 @@ export class Timeline extends React.Component< this.intersectionObserver?.disconnect(); clearTimeoutIfNecessary(delayedPeekTimeout); - clearTimeoutIfNecessary(nowThatUpdatesEveryMinuteTimeout); } public override getSnapshotBeforeUpdate( @@ -774,7 +761,6 @@ export class Timeline extends React.Component< hasRecentlyScrolled, lastMeasuredWarningHeight, newestFullyVisibleMessageId, - nowThatUpdatesEveryMinute, oldestPartiallyVisibleMessageId, widthBreakpoint, } = this.state; @@ -883,7 +869,6 @@ export class Timeline extends React.Component< isOldestTimelineItem: haveOldest && itemIndex === 0, messageId, nextMessageId, - now: nowThatUpdatesEveryMinute, previousMessageId, unreadIndicatorPlacement, })} diff --git a/ts/components/conversation/TimelineItem.tsx b/ts/components/conversation/TimelineItem.tsx index 29b2faa101..1575835fbc 100644 --- a/ts/components/conversation/TimelineItem.tsx +++ b/ts/components/conversation/TimelineItem.tsx @@ -155,7 +155,6 @@ type PropsLocalType = { theme: ThemeType; previousItem: undefined | TimelineItemType; nextItem: undefined | TimelineItemType; - now: number; unreadIndicatorPlacement?: undefined | UnreadIndicatorPlacement; }; @@ -190,7 +189,6 @@ export class TimelineItem extends React.PureComponent { i18n, theme, nextItem, - now, previousItem, renderContact, renderUniversalTimerNotification, @@ -237,7 +235,6 @@ export class TimelineItem extends React.PureComponent { conversationId={conversationId} i18n={i18n} nextItem={nextItem} - now={now} returnToActiveCall={returnToActiveCall} startCallingLobby={startCallingLobby} {...item.data} diff --git a/ts/hooks/useNowThatUpdatesEveryMinute.ts b/ts/hooks/useNowThatUpdatesEveryMinute.ts new file mode 100644 index 0000000000..ca7c0eb4c3 --- /dev/null +++ b/ts/hooks/useNowThatUpdatesEveryMinute.ts @@ -0,0 +1,27 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { EventEmitter } from 'events'; +import { useEffect, useState } from 'react'; + +import { MINUTE } from '../util/durations'; + +const ev = new EventEmitter(); +setInterval(() => ev.emit('tick'), MINUTE); + +export function useNowThatUpdatesEveryMinute(): number { + const [now, setNow] = useState(Date.now()); + + useEffect(() => { + const updateNow = () => setNow(Date.now()); + updateNow(); + + ev.on('tick', updateNow); + + return () => { + ev.off('tick', updateNow); + }; + }, []); + + return now; +} diff --git a/ts/state/smart/MessageAudio.tsx b/ts/state/smart/MessageAudio.tsx index ec38d5607b..be6c44d312 100644 --- a/ts/state/smart/MessageAudio.tsx +++ b/ts/state/smart/MessageAudio.tsx @@ -29,7 +29,6 @@ export type Props = { expirationLength?: number; expirationTimestamp?: number; id: string; - now: number; played: boolean; showMessageDetail: (id: string) => void; status?: MessageStatusType; diff --git a/ts/state/smart/Timeline.tsx b/ts/state/smart/Timeline.tsx index 98bcfb9b00..54458e3273 100644 --- a/ts/state/smart/Timeline.tsx +++ b/ts/state/smart/Timeline.tsx @@ -108,7 +108,6 @@ function renderItem({ isOldestTimelineItem, messageId, nextMessageId, - now, previousMessageId, unreadIndicatorPlacement, }: { @@ -119,7 +118,6 @@ function renderItem({ isOldestTimelineItem: boolean; messageId: string; nextMessageId: undefined | string; - now: number; previousMessageId: undefined | string; unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement; }): JSX.Element { @@ -133,7 +131,6 @@ function renderItem({ messageId={messageId} previousMessageId={previousMessageId} nextMessageId={nextMessageId} - now={now} renderEmojiPicker={renderEmojiPicker} renderReactionPicker={renderReactionPicker} renderAudioAttachment={renderAudioAttachment} diff --git a/ts/state/smart/TimelineItem.tsx b/ts/state/smart/TimelineItem.tsx index 74e512bd88..72e6671a6b 100644 --- a/ts/state/smart/TimelineItem.tsx +++ b/ts/state/smart/TimelineItem.tsx @@ -28,7 +28,6 @@ type ExternalProps = { messageId: string; nextMessageId: undefined | string; previousMessageId: undefined | string; - now: number; unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement; }; @@ -48,7 +47,6 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { messageId, nextMessageId, previousMessageId, - now, unreadIndicatorPlacement, } = props; @@ -71,7 +69,6 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { item, previousItem, nextItem, - now, id: messageId, containerElementRef, conversationId,