import React from 'react'; import { SessionScrollButton } from '../SessionScrollButton'; import { contextMenu } from 'react-contexify'; import { connect, useSelector } from 'react-redux'; import { SessionMessagesList } from './SessionMessagesList'; import styled from 'styled-components'; import autoBind from 'auto-bind'; import { getMessagesBySentAt } from '../../data/data'; import { ConversationTypeEnum } from '../../models/conversation'; import { MessageModel } from '../../models/message'; import { QuoteClickOptions } from '../../models/messageType'; import { getConversationController } from '../../session/conversations'; import { ToastUtils } from '../../session/utils'; import { quotedMessageToAnimate, ReduxConversationType, showScrollToBottomButton, SortedMessageModelProps, updateHaveDoneFirstScroll, } from '../../state/ducks/conversations'; import { StateType } from '../../state/reducer'; import { getFirstUnreadMessageId, getQuotedMessageToAnimate, getSelectedConversation, getSelectedConversationKey, getShowScrollButton, getSortedMessagesOfSelectedConversation, isFirstUnreadMessageIdAbove, } from '../../state/selectors/conversations'; import { TypingBubble } from './TypingBubble'; export type SessionMessageListProps = { messageContainerRef: React.RefObject; }; const SessionUnreadAboveIndicator = styled.div` position: sticky; top: 0; margin: 1em; display: flex; justify-content: center; background: var(--color-sent-message-background); color: var(--color-sent-message-text); `; const UnreadAboveIndicator = () => { const isFirstUnreadAbove = useSelector(isFirstUnreadMessageIdAbove); const firstUnreadMessageId = useSelector(getFirstUnreadMessageId) as string; if (!isFirstUnreadAbove) { return null; } return ( {window.i18n('latestUnreadIsAbove')} ); }; type Props = SessionMessageListProps & { conversationKey?: string; messagesProps: Array; conversation?: ReduxConversationType; showScrollButton: boolean; animateQuotedMessageId: string | undefined; firstUnreadOnOpen: string | undefined; }; class SessionMessagesListContainerInner extends React.Component { private timeoutResetQuotedScroll: NodeJS.Timeout | null = null; public constructor(props: Props) { super(props); autoBind(this); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~ LIFECYCLES ~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ public componentDidMount() { this.initialMessageLoadingPosition(); } public componentWillUnmount() { if (this.timeoutResetQuotedScroll) { global.clearTimeout(this.timeoutResetQuotedScroll); } } public componentDidUpdate( prevProps: Props, _prevState: any // snapShot: { // fakeScrollTop: number; // realScrollTop: number; // scrollHeight: number; // oldTopMessageId?: string; // } ) { // const { oldTopMessageId } = snapShot; // console.warn('didupdate with oldTopMessageId', oldTopMessageId); // // If you want to mess with this, be my guest. // // just make sure you don't remove that as a bug in chrome makes the column-reverse do bad things // // https://bugs.chromium.org/p/chromium/issues/detail?id=1189195&q=column-reverse&can=2#makechanges const isSameConvo = prevProps.conversationKey === this.props.conversationKey; // if (isSameConvo && oldTopMessageId) { // this.scrollToMessage(oldTopMessageId, 'center'); // // if (messageAddedWasMoreRecentOne) { // // if (snapShot.scrollHeight - snapShot.realScrollTop < 50) { // // // consider that we were scrolled to bottom // // currentRef.scrollTop = 0; // // } else { // // currentRef.scrollTop = -(currentRef.scrollHeight - snapShot.realScrollTop); // // } // // } else { // // currentRef.scrollTop = snapShot.fakeScrollTop; // // } // } if (!isSameConvo) { console.info('Not same convo, resetting scrolling posiiton'); this.setupTimeoutResetQuotedHighlightedMessage(this.props.animateQuotedMessageId); // displayed conversation changed. We have a bit of cleaning to do here this.initialMessageLoadingPosition(); } } public getSnapshotBeforeUpdate() { // const messagePropsBeforeUpdate = this.props.messagesProps; // const oldTopMessageId = messagePropsBeforeUpdate.length // ? messagePropsBeforeUpdate[messagePropsBeforeUpdate.length - 1].propsForMessage.id // : undefined; // console.warn('oldTopMessageId', oldTopMessageId); // const messageContainer = this.props.messageContainerRef.current; // const scrollTop = messageContainer?.scrollTop || undefined; // const scrollHeight = messageContainer?.scrollHeight || undefined; // // as we use column-reverse for displaying message list // // the top is < 0 // const realScrollTop = scrollHeight && scrollTop ? scrollHeight + scrollTop : undefined; // return { // realScrollTop, // fakeScrollTop: scrollTop, // scrollHeight: scrollHeight, // oldTopMessageId, // }; } public render() { const { conversationKey, conversation } = this.props; if (!conversationKey || !conversation) { return null; } let displayedName = null; if (conversation.type === ConversationTypeEnum.PRIVATE) { displayedName = getConversationController().getContactProfileNameOrShortenedPubKey( conversationKey ); } return (
); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~ SCROLLING METHODS ~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ private handleScroll() { contextMenu.hideAll(); } /** * Position the list to the middle of the loaded list if the conversation has unread messages and we have some messages loaded */ private initialMessageLoadingPosition() { const { messagesProps, conversation, firstUnreadOnOpen } = this.props; if (!conversation || !messagesProps.length) { return; } console.warn('firstUnreadOnOpen', firstUnreadOnOpen); if ( (conversation.unreadCount && conversation.unreadCount <= 0) || firstUnreadOnOpen === undefined ) { this.scrollToBottom(); } else { // just assume that this need to be shown by default window.inboxStore?.dispatch(showScrollToBottomButton(true)); const firstUnreadIndex = messagesProps.findIndex( m => m.propsForMessage.id === firstUnreadOnOpen ); console.warn('firstUnreadIndex', firstUnreadIndex); if (firstUnreadIndex === -1) { // the first unread message is not in the 30 most recent messages // just scroll to the middle as we don't have enough loaded message nevertheless const middle = Math.floor(messagesProps.length / 2); const idToStringTo = messagesProps[middle].propsForMessage.id; this.scrollToMessage(idToStringTo, 'center'); } else { const messageElementDom = document.getElementById('unread-indicator'); messageElementDom?.scrollIntoView({ behavior: 'auto', block: 'end', }); } } setTimeout(() => { window.inboxStore?.dispatch(updateHaveDoneFirstScroll()); }, 100); } /** * Could not find a better name, but when we click on a quoted message, * the UI takes us there and highlights it. * If the user clicks again on this message, we want this highlight to be * shown once again. * * So we need to reset the state of of the highlighted message so when the users clicks again, * the highlight is shown once again */ private setupTimeoutResetQuotedHighlightedMessage(messageId: string | undefined) { if (this.timeoutResetQuotedScroll) { global.clearTimeout(this.timeoutResetQuotedScroll); } if (messageId !== undefined) { this.timeoutResetQuotedScroll = global.setTimeout(() => { window.inboxStore?.dispatch(quotedMessageToAnimate(undefined)); }, 2000); // should match .flash-green-once } } private scrollToMessage(messageId: string, block: ScrollLogicalPosition | undefined) { const messageElementDom = document.getElementById(`msg-${messageId}`); console.warn('scrollToMessage', messageElementDom); debugger; messageElementDom?.scrollIntoView({ behavior: 'auto', block, }); } private scrollToBottom() { const messageContainer = this.props.messageContainerRef.current; if (!messageContainer) { return; } messageContainer.scrollTop = messageContainer.scrollHeight - messageContainer.clientHeight; } private scrollPgUp() { const messageContainer = this.props.messageContainerRef.current; if (!messageContainer) { return; } messageContainer.scrollBy({ top: Math.floor(-messageContainer.clientHeight * 2) / 3, behavior: 'smooth', }); } private scrollPgDown() { const messageContainer = this.props.messageContainerRef.current; if (!messageContainer) { return; } // tslint:disable-next-line: restrict-plus-operands messageContainer.scrollBy({ top: Math.floor(+messageContainer.clientHeight * 2) / 3, behavior: 'smooth', }); } private scrollTop() { const messageContainer = this.props.messageContainerRef.current; if (!messageContainer) { return; } messageContainer.scrollTo(0, -messageContainer.scrollHeight); } private scrollEnd() { const messageContainer = this.props.messageContainerRef.current; if (!messageContainer) { return; } messageContainer.scrollTo(0, 0); } private async scrollToQuoteMessage(options: QuoteClickOptions) { const { quoteAuthor, quoteId, referencedMessageNotFound } = options; const { messagesProps } = this.props; // For simplicity's sake, we show the 'not found' toast no matter what if we were // not able to find the referenced message when the quote was received. if (referencedMessageNotFound) { ToastUtils.pushOriginalNotFound(); return; } // Look for message in memory first, which would tell us if we could scroll to it const targetMessage = messagesProps.find(item => { const messageAuthor = item.propsForMessage?.sender; if (!messageAuthor || quoteAuthor !== messageAuthor) { return false; } if (quoteId !== item.propsForMessage?.timestamp) { return false; } return true; }); // If there's no message already in memory, we won't be scrolling. So we'll gather // some more information then show an informative toast to the user. if (!targetMessage) { const collection = await getMessagesBySentAt(quoteId); const found = Boolean( collection.find((item: MessageModel) => { const messageAuthor = item.getSource(); return Boolean(messageAuthor && quoteAuthor === messageAuthor); }) ); if (found) { ToastUtils.pushFoundButNotLoaded(); } else { ToastUtils.pushOriginalNoLongerAvailable(); } return; } const databaseId = targetMessage.propsForMessage.id; this.scrollToMessage(databaseId, 'center'); // Highlight this message on the UI window.inboxStore?.dispatch(quotedMessageToAnimate(databaseId)); this.setupTimeoutResetQuotedHighlightedMessage(databaseId); } } const mapStateToProps = (state: StateType) => { return { conversationKey: getSelectedConversationKey(state), conversation: getSelectedConversation(state), messagesProps: getSortedMessagesOfSelectedConversation(state), showScrollButton: getShowScrollButton(state), animateQuotedMessageId: getQuotedMessageToAnimate(state), firstUnreadOnOpen: getFirstUnreadMessageId(state), }; }; const smart = connect(mapStateToProps); export const SessionMessagesListContainer = smart(SessionMessagesListContainerInner);