diff --git a/ts/components/conversation/LastSeenIndicator.tsx b/ts/components/conversation/LastSeenIndicator.tsx index d6932dddb..a6b576319 100644 --- a/ts/components/conversation/LastSeenIndicator.tsx +++ b/ts/components/conversation/LastSeenIndicator.tsx @@ -1,7 +1,7 @@ -// Copyright 2019-2020 Signal Messenger, LLC +// Copyright 2019-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React from 'react'; +import React, { forwardRef } from 'react'; import type { LocalizerType } from '../../types/Util'; @@ -10,16 +10,18 @@ export type Props = { i18n: LocalizerType; }; -export const LastSeenIndicator = ({ count, i18n }: Props): JSX.Element => { - const message = - count === 1 - ? i18n('unreadMessage') - : i18n('unreadMessages', [String(count)]); +export const LastSeenIndicator = forwardRef( + ({ count, i18n }, ref) => { + const message = + count === 1 + ? i18n('unreadMessage') + : i18n('unreadMessages', [String(count)]); - return ( -
-
-
{message}
-
- ); -}; + return ( +
+
+
{message}
+
+ ); + } +); diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index 6de37f212..0f87c542e 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -19,7 +19,6 @@ import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext import { ConversationHero } from './ConversationHero'; import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation'; import { getRandomColor } from '../../test-both/helpers/getRandomColor'; -import { LastSeenIndicator } from './LastSeenIndicator'; import { TypingBubble } from './TypingBubble'; import { ContactSpoofingType } from '../../util/contactSpoofing'; import { ReadStatus } from '../../messages/MessageReadStatus'; @@ -445,10 +444,6 @@ const renderItem = ({ /> ); -const renderLastSeenIndicator = () => ( - -); - const getAbout = () => text('about', '👍 Free to chat'); const getTitle = () => text('name', 'Cayce Bollard'); const getName = () => text('name', 'Cayce Bollard'); @@ -528,7 +523,6 @@ const useProps = (overrideProps: Partial = {}): PropsType => ({ id: uuid(), renderItem, - renderLastSeenIndicator, renderHeroRow, renderTypingBubble, typingContactId: overrideProps.typingContactId, diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index ef8a9067e..51a6a93cc 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -4,7 +4,7 @@ import { first, get, isNumber, last, pick, throttle } from 'lodash'; import classNames from 'classnames'; import type { ReactChild, ReactNode, RefObject } from 'react'; -import React, { Fragment } from 'react'; +import React from 'react'; import { createSelector } from 'reselect'; import Measure from 'react-measure'; @@ -41,6 +41,7 @@ import { scrollToBottom, setScrollBottom, } from '../../util/scrollUtil'; +import { LastSeenIndicator } from './LastSeenIndicator'; const AT_BOTTOM_THRESHOLD = 15; const MIN_ROW_HEIGHT = 18; @@ -120,7 +121,6 @@ type PropsHousekeepingType = { previousMessageId: undefined | string; unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement; }) => JSX.Element; - renderLastSeenIndicator: (id: string) => JSX.Element; renderHeroRow: ( id: string, unblurAvatar: () => void, @@ -177,8 +177,11 @@ type StateType = { widthBreakpoint: WidthBreakpoint; }; +const scrollToUnreadIndicator = Symbol('scrollToUnreadIndicator'); + type SnapshotType = | null + | typeof scrollToUnreadIndicator | { scrollToIndex: number } | { scrollTop: number } | { scrollBottom: number }; @@ -258,6 +261,7 @@ export class Timeline extends React.Component< > { private readonly containerRef = React.createRef(); private readonly messagesRef = React.createRef(); + private readonly lastSeenIndicatorRef = React.createRef(); private intersectionObserver?: IntersectionObserver; private messagesResizeObserver?: ResizeObserver; @@ -538,6 +542,7 @@ export class Timeline extends React.Component< isIncomingMessageRequest, isLoadingMessages, items: newItems, + oldestUnreadIndex, scrollToIndex, scrollToIndexCounter: newScrollToIndexCounter, typingContactId, @@ -560,7 +565,13 @@ export class Timeline extends React.Component< } if (justFinishedInitialLoad) { - return isIncomingMessageRequest ? { scrollTop: 0 } : { scrollBottom: 0 }; + if (isIncomingMessageRequest) { + return { scrollTop: 0 }; + } + if (isNumber(oldestUnreadIndex)) { + return scrollToUnreadIndicator; + } + return { scrollBottom: 0 }; } if ( @@ -599,7 +610,18 @@ export class Timeline extends React.Component< const containerEl = this.containerRef.current; if (containerEl && snapshot) { - if ('scrollToIndex' in snapshot) { + if (snapshot === scrollToUnreadIndicator) { + const lastSeenIndicatorEl = this.lastSeenIndicatorRef.current; + if (lastSeenIndicatorEl) { + lastSeenIndicatorEl.scrollIntoView(); + } else { + scrollToBottom(containerEl); + assert( + false, + ' expected a last seen indicator but it was not found' + ); + } + } else if ('scrollToIndex' in snapshot) { this.scrollToItemIndex(snapshot.scrollToIndex); } else if ('scrollTop' in snapshot) { containerEl.scrollTop = snapshot.scrollTop; @@ -746,12 +768,12 @@ export class Timeline extends React.Component< removeMember, renderHeroRow, renderItem, - renderLastSeenIndicator, renderTypingBubble, reviewGroupMemberNameCollision, reviewMessageRequestNameCollision, showContactModal, theme, + totalUnread, typingContactId, unblurAvatar, unreadCount, @@ -848,7 +870,12 @@ export class Timeline extends React.Component< if (oldestUnreadIndex === itemIndex) { unreadIndicatorPlacement = UnreadIndicatorPlacement.JustAbove; messageNodes.push( - {renderLastSeenIndicator(id)} + ); } else if (oldestUnreadIndex === nextItemIndex) { unreadIndicatorPlacement = UnreadIndicatorPlacement.JustBelow; diff --git a/ts/state/smart/LastSeenIndicator.tsx b/ts/state/smart/LastSeenIndicator.tsx deleted file mode 100644 index 6066196a8..000000000 --- a/ts/state/smart/LastSeenIndicator.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2019-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { connect } from 'react-redux'; -import { mapDispatchToProps } from '../actions'; - -import { LastSeenIndicator } from '../../components/conversation/LastSeenIndicator'; - -import type { StateType } from '../reducer'; -import { getIntl } from '../selectors/user'; -import { getConversationMessagesSelector } from '../selectors/conversations'; - -type ExternalProps = { - id: string; -}; - -const mapStateToProps = (state: StateType, props: ExternalProps) => { - const { id } = props; - - const conversation = getConversationMessagesSelector(state)(id); - if (!conversation) { - throw new Error(`Did not find conversation ${id} in state!`); - } - - const { totalUnread } = conversation; - - return { - count: totalUnread, - i18n: getIntl(state), - }; -}; - -const smart = connect(mapStateToProps, mapDispatchToProps); - -export const SmartLastSeenIndicator = smart(LastSeenIndicator); diff --git a/ts/state/smart/Timeline.tsx b/ts/state/smart/Timeline.tsx index 54458e327..b8b6bf400 100644 --- a/ts/state/smart/Timeline.tsx +++ b/ts/state/smart/Timeline.tsx @@ -30,7 +30,6 @@ import { import { SmartTimelineItem } from './TimelineItem'; import { SmartTypingBubble } from './TypingBubble'; -import { SmartLastSeenIndicator } from './LastSeenIndicator'; import { SmartHeroRow } from './HeroRow'; import { renderAudioAttachment } from './renderAudioAttachment'; import { renderEmojiPicker } from './renderEmojiPicker'; @@ -139,10 +138,6 @@ function renderItem({ ); } -function renderLastSeenIndicator(id: string): JSX.Element { - return ; -} - function renderHeroRow( id: string, unblurAvatar: () => void, @@ -313,7 +308,6 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { i18n: getIntl(state), theme: getTheme(state), renderItem, - renderLastSeenIndicator, renderHeroRow, renderTypingBubble, ...actions,