On conversation open, scroll to unread indicator if present

This commit is contained in:
Evan Hahn 2022-03-08 14:05:05 -06:00 committed by GitHub
parent efee887135
commit 944d60f40b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 49 additions and 67 deletions

View File

@ -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<HTMLDivElement, Props>(
({ count, i18n }, ref) => {
const message =
count === 1
? i18n('unreadMessage')
: i18n('unreadMessages', [String(count)]);
return (
<div className="module-last-seen-indicator">
<div className="module-last-seen-indicator__bar" />
<div className="module-last-seen-indicator__text">{message}</div>
</div>
);
};
return (
<div className="module-last-seen-indicator" ref={ref}>
<div className="module-last-seen-indicator__bar" />
<div className="module-last-seen-indicator__text">{message}</div>
</div>
);
}
);

View File

@ -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 = () => (
<LastSeenIndicator count={2} i18n={i18n} />
);
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> = {}): PropsType => ({
id: uuid(),
renderItem,
renderLastSeenIndicator,
renderHeroRow,
renderTypingBubble,
typingContactId: overrideProps.typingContactId,

View File

@ -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<HTMLDivElement>();
private readonly messagesRef = React.createRef<HTMLDivElement>();
private readonly lastSeenIndicatorRef = React.createRef<HTMLDivElement>();
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,
'<Timeline> 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(
<Fragment key="unread">{renderLastSeenIndicator(id)}</Fragment>
<LastSeenIndicator
key="last seen indicator"
count={totalUnread}
i18n={i18n}
ref={this.lastSeenIndicatorRef}
/>
);
} else if (oldestUnreadIndex === nextItemIndex) {
unreadIndicatorPlacement = UnreadIndicatorPlacement.JustBelow;

View File

@ -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);

View File

@ -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 <SmartLastSeenIndicator id={id} />;
}
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,