On conversation open, scroll to unread indicator if present
This commit is contained in:
parent
efee887135
commit
944d60f40b
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue