fixup open convo on search, quote click or hit the bottom

pull/2142/head
Audric Ackermann 4 years ago
parent 2b0a2cff46
commit a3be2c347d
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -1853,17 +1853,19 @@ function searchConversations(query, { limit } = {}) {
return map(rows, row => jsonToObject(row.json)); return map(rows, row => jsonToObject(row.json));
} }
function searchMessages(query, { limit } = {}) { function searchMessages(query, limit) {
// order by clause is the same as orderByClause but with a table prefix so we cannot reuse it
const rows = globalInstance const rows = globalInstance
.prepare( .prepare(
`SELECT `SELECT
messages.json, ${MESSAGES_TABLE}.json,
snippet(messages_fts, -1, '<<left>>', '<<right>>', '...', 15) as snippet snippet(${MESSAGES_FTS_TABLE}, -1, '<<left>>', '<<right>>', '...', 15) as snippet
FROM ${MESSAGES_FTS_TABLE} FROM ${MESSAGES_FTS_TABLE}
INNER JOIN ${MESSAGES_TABLE} on messages_fts.id = messages.id INNER JOIN ${MESSAGES_TABLE} on ${MESSAGES_FTS_TABLE}.id = ${MESSAGES_TABLE}.id
WHERE WHERE
messages_fts match $query ${MESSAGES_FTS_TABLE} match $query
ORDER BY messages.received_at DESC ORDER BY ${MESSAGES_TABLE}.serverTimestamp DESC, ${MESSAGES_TABLE}.serverId DESC, ${MESSAGES_TABLE}.sent_at DESC, ${MESSAGES_TABLE}.received_at DESC
LIMIT $limit;` LIMIT $limit;`
) )
.all({ .all({
@ -1877,7 +1879,7 @@ function searchMessages(query, { limit } = {}) {
})); }));
} }
function searchMessagesInConversation(query, conversationId, { limit } = {}) { function searchMessagesInConversation(query, conversationId, limit) {
const rows = globalInstance const rows = globalInstance
.prepare( .prepare(
`SELECT `SELECT
@ -2241,7 +2243,6 @@ function getMessagesByConversation(conversationId, { messageId = null } = {}) {
// If messageId is null, it means we are just opening the convo to the last unread message, or at the bottom // If messageId is null, it means we are just opening the convo to the last unread message, or at the bottom
const firstUnread = getFirstUnreadMessageIdInConversation(conversationId); const firstUnread = getFirstUnreadMessageIdInConversation(conversationId);
if (messageId || firstUnread) { if (messageId || firstUnread) {
const messageFound = getMessageById(messageId || firstUnread); const messageFound = getMessageById(messageId || firstUnread);

@ -326,10 +326,11 @@
window.setCallMediaPermissions(enabled); window.setCallMediaPermissions(enabled);
}; };
Whisper.Notifications.on('click', async (id, messageId) => { Whisper.Notifications.on('click', async conversationKey => {
window.showWindow(); window.showWindow();
if (id) { if (conversationKey) {
await window.openConversationWithMessages({ conversationKey: id, messageId }); // do not put the messageId here so the conversation is loaded on the last unread instead
await window.openConversationWithMessages({ conversationKey, messageId: null });
} else { } else {
appView.openInbox({ appView.openInbox({
initialLoadComplete, initialLoadComplete,

@ -1425,10 +1425,6 @@
} }
} }
.module-message-search-result--is-selected {
background-color: $color-gray-05;
}
.module-message-search-result__text { .module-message-search-result__text {
flex-grow: 1; flex-grow: 1;
margin-inline-start: 12px; margin-inline-start: 12px;
@ -1483,29 +1479,6 @@
font-weight: 300; font-weight: 300;
} }
.module-message-search-result__body {
margin-top: 1px;
flex-grow: 1;
flex-shrink: 1;
font-size: 13px;
color: var(--color-text-subtle);
max-height: 3.6em;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
// Note: -webkit-line-clamp doesn't work for RTL text, and it forces you to use
// ... as the truncation indicator. That's not a solution that works well for
// all languages. More resources:
// - http://hackingui.com/front-end/a-pure-css-solution-for-multiline-text-truncation/
// - https://medium.com/mofed/css-line-clamp-the-good-the-bad-and-the-straight-up-broken-865413f16e5
}
// Module: Left Pane // Module: Left Pane
.module-left-pane { .module-left-pane {

@ -271,6 +271,23 @@
} }
} }
$session-highlight-message-shadow: 0px 0px 10px 1px $session-color-green;
@keyframes remove-box-shadow {
0% {
box-shadow: $session-highlight-message-shadow;
}
75% {
box-shadow: $session-highlight-message-shadow;
}
100% {
box-shadow: none;
}
}
.flash-green-once { .flash-green-once {
box-shadow: 0px 0px 6px 3px $session-color-green; animation-name: remove-box-shadow;
animation-timing-function: linear;
animation-duration: 2s;
box-shadow: $session-highlight-message-shadow;
} }

@ -423,10 +423,6 @@
} }
} }
.module-message-search-result--is-selected {
background-color: $color-dark-70;
}
.module-message-search-result__header__from { .module-message-search-result__header__from {
color: $color-gray-05; color: $color-gray-05;
} }
@ -435,10 +431,6 @@
color: $color-gray-25; color: $color-gray-25;
} }
.module-message-search-result__body {
color: $color-gray-05;
}
.module-message__link-preview__icon-container__circle-background { .module-message__link-preview__icon-container__circle-background {
background-color: $color-gray-25; background-color: $color-gray-25;
} }

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import { RenderTextCallbackType } from '../../types/Util'; import { RenderTextCallbackType } from '../../types/Util';
import { SizeClassType } from '../../util/emoji'; import { SizeClassType } from '../../util/emoji';
import { AddNewLines } from '../conversation/AddNewLines'; import { AddNewLines } from '../conversation/AddNewLines';
@ -9,6 +10,10 @@ const renderNewLines: RenderTextCallbackType = ({ text, key }) => (
<AddNewLines key={key} text={text} /> <AddNewLines key={key} text={text} />
); );
const SnippetHighlight = styled.span`
font-weight: bold;
`;
const renderEmoji = ({ const renderEmoji = ({
text, text,
key, key,
@ -51,14 +56,14 @@ export const MessageBodyHighlight = (props: { text: string }) => {
const [, toHighlight] = match; const [, toHighlight] = match;
results.push( results.push(
<span className="module-message-body__highlight" key={count++}> <SnippetHighlight key={count++}>
{renderEmoji({ {renderEmoji({
text: toHighlight, text: toHighlight,
sizeClass, sizeClass,
key: count++, key: count++,
renderNonEmoji: renderNewLines, renderNonEmoji: renderNewLines,
})} })}
</span> </SnippetHighlight>
); );
// @ts-ignore // @ts-ignore

@ -2,7 +2,7 @@ import React, { useLayoutEffect } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
// tslint:disable-next-line: no-submodule-imports // tslint:disable-next-line: no-submodule-imports
import useKey from 'react-use/lib/useKey'; import useKey from 'react-use/lib/useKey';
import { PropsForDataExtractionNotification, QuoteClickOptions } from '../../models/messageType'; import { PropsForDataExtractionNotification } from '../../models/messageType';
import { import {
PropsForCallNotification, PropsForCallNotification,
PropsForExpirationTimer, PropsForExpirationTimer,
@ -10,6 +10,7 @@ import {
PropsForGroupUpdate, PropsForGroupUpdate,
} from '../../state/ducks/conversations'; } from '../../state/ducks/conversations';
import { import {
getOldBottomMessageId,
getOldTopMessageId, getOldTopMessageId,
getSortedMessagesTypesOfSelectedConversation, getSortedMessagesTypesOfSelectedConversation,
} from '../../state/selectors/conversations'; } from '../../state/selectors/conversations';
@ -28,10 +29,9 @@ function isNotTextboxEvent(e: KeyboardEvent) {
} }
export const SessionMessagesList = (props: { export const SessionMessagesList = (props: {
scrollToQuoteMessage: (options: QuoteClickOptions) => Promise<void>;
scrollAfterLoadMore: ( scrollAfterLoadMore: (
messageIdToScrollTo: string, messageIdToScrollTo: string,
block: ScrollLogicalPosition | undefined type: 'load-more-top' | 'load-more-bottom'
) => void; ) => void;
onPageUpPressed: () => void; onPageUpPressed: () => void;
onPageDownPressed: () => void; onPageDownPressed: () => void;
@ -40,6 +40,7 @@ export const SessionMessagesList = (props: {
}) => { }) => {
const messagesProps = useSelector(getSortedMessagesTypesOfSelectedConversation); const messagesProps = useSelector(getSortedMessagesTypesOfSelectedConversation);
const oldTopMessageId = useSelector(getOldTopMessageId); const oldTopMessageId = useSelector(getOldTopMessageId);
const oldBottomMessageId = useSelector(getOldBottomMessageId);
useLayoutEffect(() => { useLayoutEffect(() => {
const newTopMessageId = messagesProps.length const newTopMessageId = messagesProps.length
@ -47,7 +48,15 @@ export const SessionMessagesList = (props: {
: undefined; : undefined;
if (oldTopMessageId !== newTopMessageId && oldTopMessageId && newTopMessageId) { if (oldTopMessageId !== newTopMessageId && oldTopMessageId && newTopMessageId) {
props.scrollAfterLoadMore(oldTopMessageId, 'start'); props.scrollAfterLoadMore(oldTopMessageId, 'load-more-top');
}
const newBottomMessageId = messagesProps.length
? messagesProps[0].message.props.messageId
: undefined;
if (newBottomMessageId !== oldBottomMessageId && oldBottomMessageId && newBottomMessageId) {
props.scrollAfterLoadMore(oldBottomMessageId, 'load-more-bottom');
} }
}); });
@ -123,15 +132,7 @@ export const SessionMessagesList = (props: {
return null; return null;
} }
return [ return [<Message messageId={messageId} key={messageId} />, dateBreak, unreadIndicator];
<Message
messageId={messageId}
onQuoteClick={props.scrollToQuoteMessage}
key={messageId}
/>,
dateBreak,
unreadIndicator,
];
})} })}
</> </>
); );

@ -8,16 +8,12 @@ import { connect, useSelector } from 'react-redux';
import { SessionMessagesList } from './SessionMessagesList'; import { SessionMessagesList } from './SessionMessagesList';
import styled from 'styled-components'; import styled from 'styled-components';
import autoBind from 'auto-bind'; import autoBind from 'auto-bind';
import { getMessagesBySentAt } from '../../data/data';
import { ConversationTypeEnum } from '../../models/conversation'; import { ConversationTypeEnum } from '../../models/conversation';
import { MessageModel } from '../../models/message';
import { QuoteClickOptions } from '../../models/messageType';
import { getConversationController } from '../../session/conversations'; import { getConversationController } from '../../session/conversations';
import { ToastUtils } from '../../session/utils';
import { import {
openConversationOnQuoteClick,
quotedMessageToAnimate, quotedMessageToAnimate,
ReduxConversationType, ReduxConversationType,
resetOldBottomMessageId,
resetOldTopMessageId, resetOldTopMessageId,
showScrollToBottomButton, showScrollToBottomButton,
SortedMessageModelProps, SortedMessageModelProps,
@ -39,6 +35,11 @@ export type SessionMessageListProps = {
messageContainerRef: React.RefObject<HTMLDivElement>; messageContainerRef: React.RefObject<HTMLDivElement>;
}; };
export const ScrollToLoadedMessageContext = React.createContext(
// tslint:disable-next-line: no-empty
(_loadedMessageIdToScrollTo: string) => {}
);
const SessionUnreadAboveIndicator = styled.div` const SessionUnreadAboveIndicator = styled.div`
position: sticky; position: sticky;
top: 0; top: 0;
@ -95,67 +96,23 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
} }
} }
public componentDidUpdate( public componentDidUpdate(prevProps: Props, _prevState: any) {
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. // // 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 // // 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 // // https://bugs.chromium.org/p/chromium/issues/detail?id=1189195&q=column-reverse&can=2#makechanges
const isSameConvo = prevProps.conversationKey === this.props.conversationKey; 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 ( if (
!isSameConvo && !isSameConvo &&
this.props.messagesProps.length && this.props.messagesProps.length &&
this.props.messagesProps[0].propsForMessage.convoId === this.props.conversationKey this.props.messagesProps[0].propsForMessage.convoId === this.props.conversationKey
) { ) {
console.info('Not same convo, resetting scrolling posiiton', this.props.messagesProps.length);
this.setupTimeoutResetQuotedHighlightedMessage(this.props.animateQuotedMessageId); this.setupTimeoutResetQuotedHighlightedMessage(this.props.animateQuotedMessageId);
// displayed conversation changed. We have a bit of cleaning to do here // displayed conversation changed. We have a bit of cleaning to do here
this.initialMessageLoadingPosition(); 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() { public render() {
const { conversationKey, conversation } = this.props; const { conversationKey, conversation } = this.props;
@ -187,18 +144,27 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
key="typing-bubble" key="typing-bubble"
/> />
<SessionMessagesList <ScrollToLoadedMessageContext.Provider value={this.scrollToQuoteMessage}>
scrollToQuoteMessage={this.scrollToQuoteMessage} <SessionMessagesList
scrollAfterLoadMore={(...args) => { scrollAfterLoadMore={(
this.scrollToMessage(...args, { isLoadMoreTop: true }); messageIdToScrollTo: string,
}} type: 'load-more-top' | 'load-more-bottom'
onPageDownPressed={this.scrollPgDown} ) => {
onPageUpPressed={this.scrollPgUp} const isLoadMoreTop = type === 'load-more-top';
onHomePressed={this.scrollTop} const isLoadMoreBottom = type === 'load-more-bottom';
onEndPressed={this.scrollEnd} this.scrollToMessage(messageIdToScrollTo, isLoadMoreTop ? 'start' : 'end', {
/> isLoadMoreTop,
isLoadMoreBottom,
<SessionScrollButton onClick={this.scrollToBottom} key="scroll-down-button" /> });
}}
onPageDownPressed={this.scrollPgDown}
onPageUpPressed={this.scrollPgUp}
onHomePressed={this.scrollTop}
onEndPressed={this.scrollEnd}
/>
</ScrollToLoadedMessageContext.Provider>
<SessionScrollButton onClick={this.scrollToMostRecentMessage} key="scroll-down-button" />
</div> </div>
); );
} }
@ -223,7 +189,7 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
(conversation.unreadCount && conversation.unreadCount <= 0) || (conversation.unreadCount && conversation.unreadCount <= 0) ||
firstUnreadOnOpen === undefined firstUnreadOnOpen === undefined
) { ) {
this.scrollToBottom(); this.scrollToMostRecentMessage();
} else { } else {
// just assume that this need to be shown by default // just assume that this need to be shown by default
window.inboxStore?.dispatch(showScrollToBottomButton(true)); window.inboxStore?.dispatch(showScrollToBottomButton(true));
@ -275,7 +241,7 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
private scrollToMessage( private scrollToMessage(
messageId: string, messageId: string,
block: ScrollLogicalPosition | undefined, block: ScrollLogicalPosition | undefined,
options?: { isLoadMoreTop: boolean | undefined } options?: { isLoadMoreTop: boolean | undefined; isLoadMoreBottom: boolean | undefined }
) { ) {
const messageElementDom = document.getElementById(`msg-${messageId}`); const messageElementDom = document.getElementById(`msg-${messageId}`);
@ -288,9 +254,13 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
// reset the oldTopInRedux so that a refresh/new message does not scroll us back here again // reset the oldTopInRedux so that a refresh/new message does not scroll us back here again
window.inboxStore?.dispatch(resetOldTopMessageId()); window.inboxStore?.dispatch(resetOldTopMessageId());
} }
if (options?.isLoadMoreBottom) {
// reset the oldBottomInRedux so that a refresh/new message does not scroll us back here again
window.inboxStore?.dispatch(resetOldBottomMessageId());
}
} }
private scrollToBottom() { private scrollToMostRecentMessage() {
const messageContainer = this.props.messageContainerRef.current; const messageContainer = this.props.messageContainerRef.current;
if (!messageContainer) { if (!messageContainer) {
return; return;
@ -340,61 +310,23 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
messageContainer.scrollTo(0, 0); messageContainer.scrollTo(0, 0);
} }
private async scrollToQuoteMessage(options: QuoteClickOptions) { private scrollToQuoteMessage(loadedQuoteMessageToScrollTo: string) {
if (!this.props.conversationKey) { if (!this.props.conversationKey || !loadedQuoteMessageToScrollTo) {
return; return;
} }
const { quoteAuthor, quoteId, referencedMessageNotFound } = options;
const { messagesProps } = this.props; 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 // 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. // some more information then show an informative toast to the user.
if (!targetMessage) { if (!messagesProps.find(m => m.propsForMessage.id === loadedQuoteMessageToScrollTo)) {
const collection = await getMessagesBySentAt(quoteId); throw new Error('this message is not loaded');
const found = collection.find((item: MessageModel) => {
const messageAuthor = item.getSource();
return Boolean(messageAuthor && quoteAuthor === messageAuthor);
});
if (found) {
void openConversationOnQuoteClick({
conversationKey: this.props.conversationKey,
messageIdToNavigateTo: found.get('id'),
});
ToastUtils.pushFoundButNotLoaded();
} else {
ToastUtils.pushOriginalNoLongerAvailable();
}
return;
} }
const databaseId = targetMessage.propsForMessage.id; this.scrollToMessage(loadedQuoteMessageToScrollTo, 'start');
this.scrollToMessage(databaseId, 'center');
// Highlight this message on the UI // Highlight this message on the UI
window.inboxStore?.dispatch(quotedMessageToAnimate(databaseId)); window.inboxStore?.dispatch(quotedMessageToAnimate(loadedQuoteMessageToScrollTo));
this.setupTimeoutResetQuotedHighlightedMessage(databaseId); this.setupTimeoutResetQuotedHighlightedMessage(loadedQuoteMessageToScrollTo);
} }
} }

@ -1,10 +1,10 @@
import classNames from 'classnames'; import classNames from 'classnames';
import moment from 'moment'; import moment from 'moment';
import React, { createContext, useCallback, useState } from 'react'; import React, { createContext, useCallback, useContext, useLayoutEffect, useState } from 'react';
import { InView } from 'react-intersection-observer'; import { InView } from 'react-intersection-observer';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { MessageRenderingProps, QuoteClickOptions } from '../../../../models/messageType'; import { MessageRenderingProps } from '../../../../models/messageType';
import { import {
getMessageContentSelectorProps, getMessageContentSelectorProps,
getMessageTextProps, getMessageTextProps,
@ -26,6 +26,7 @@ import { MessageAttachment } from './MessageAttachment';
import { MessagePreview } from './MessagePreview'; import { MessagePreview } from './MessagePreview';
import { MessageQuote } from './MessageQuote'; import { MessageQuote } from './MessageQuote';
import { MessageText } from './MessageText'; import { MessageText } from './MessageText';
import { ScrollToLoadedMessageContext } from '../../SessionMessagesListContainer';
export type MessageContentSelectorProps = Pick< export type MessageContentSelectorProps = Pick<
MessageRenderingProps, MessageRenderingProps,
@ -42,7 +43,6 @@ export type MessageContentSelectorProps = Pick<
type Props = { type Props = {
messageId: string; messageId: string;
onQuoteClick?: (quote: QuoteClickOptions) => void;
isDetailView?: boolean; isDetailView?: boolean;
}; };
@ -97,11 +97,14 @@ function onClickOnMessageInnerContainer(event: React.MouseEvent<HTMLDivElement>)
export const IsMessageVisibleContext = createContext(false); export const IsMessageVisibleContext = createContext(false);
export const MessageContent = (props: Props) => { export const MessageContent = (props: Props) => {
const [flashGreen, setFlashGreen] = useState(false);
const contentProps = useSelector(state => const contentProps = useSelector(state =>
getMessageContentSelectorProps(state as any, props.messageId) getMessageContentSelectorProps(state as any, props.messageId)
); );
const [isMessageVisible, setMessageIsVisible] = useState(false); const [isMessageVisible, setMessageIsVisible] = useState(false);
const scrollToMessage = useContext(ScrollToLoadedMessageContext);
const [imageBroken, setImageBroken] = useState(false); const [imageBroken, setImageBroken] = useState(false);
const onVisible = (inView: boolean | Object) => { const onVisible = (inView: boolean | Object) => {
@ -119,6 +122,24 @@ export const MessageContent = (props: Props) => {
setImageBroken(true); setImageBroken(true);
}, [setImageBroken]); }, [setImageBroken]);
const quotedMessageToAnimate = useSelector(getQuotedMessageToAnimate);
const isQuotedMessageToAnimate = quotedMessageToAnimate === props.messageId;
useLayoutEffect(() => {
if (isQuotedMessageToAnimate) {
if (!flashGreen) {
//scroll to me and flash me
scrollToMessage(props.messageId);
setFlashGreen(true);
}
return;
}
if (flashGreen) {
setFlashGreen(false);
}
return;
});
if (!contentProps) { if (!contentProps) {
return null; return null;
} }
@ -136,13 +157,11 @@ export const MessageContent = (props: Props) => {
} = contentProps; } = contentProps;
const selectedMsg = useSelector(state => getMessageTextProps(state as any, props.messageId)); const selectedMsg = useSelector(state => getMessageTextProps(state as any, props.messageId));
const quotedMessageToAnimate = useSelector(getQuotedMessageToAnimate);
let isDeleted = false; let isDeleted = false;
if (selectedMsg && selectedMsg.isDeleted !== undefined) { if (selectedMsg && selectedMsg.isDeleted !== undefined) {
isDeleted = selectedMsg.isDeleted; isDeleted = selectedMsg.isDeleted;
} }
const isQuotedMessageToAnimate = quotedMessageToAnimate === props.messageId;
const width = getWidth({ previews, attachments }); const width = getWidth({ previews, attachments });
const isShowingImage = getIsShowingImage({ attachments, imageBroken, previews, text }); const isShowingImage = getIsShowingImage({ attachments, imageBroken, previews, text });
@ -167,7 +186,7 @@ export const MessageContent = (props: Props) => {
lastMessageOfSeries || props.isDetailView lastMessageOfSeries || props.isDetailView
? `module-message__container--${direction}--last-of-series` ? `module-message__container--${direction}--last-of-series`
: '', : '',
isQuotedMessageToAnimate && 'flash-green-once' flashGreen && 'flash-green-once'
)} )}
style={{ style={{
width: isShowingImage ? width : undefined, width: isShowingImage ? width : undefined,
@ -186,7 +205,7 @@ export const MessageContent = (props: Props) => {
<IsMessageVisibleContext.Provider value={isMessageVisible}> <IsMessageVisibleContext.Provider value={isMessageVisible}>
{!isDeleted && ( {!isDeleted && (
<> <>
<MessageQuote messageId={props.messageId} onQuoteClick={props.onQuoteClick} /> <MessageQuote messageId={props.messageId} />
<MessageAttachment <MessageAttachment
messageId={props.messageId} messageId={props.messageId}
imageBroken={imageBroken} imageBroken={imageBroken}

@ -2,7 +2,7 @@ import classNames from 'classnames';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { replyToMessage } from '../../../../interactions/conversationInteractions'; import { replyToMessage } from '../../../../interactions/conversationInteractions';
import { MessageRenderingProps, QuoteClickOptions } from '../../../../models/messageType'; import { MessageRenderingProps } from '../../../../models/messageType';
import { toggleSelectedMessageId } from '../../../../state/ducks/conversations'; import { toggleSelectedMessageId } from '../../../../state/ducks/conversations';
import { import {
getMessageContentWithStatusesSelectorProps, getMessageContentWithStatusesSelectorProps,
@ -21,7 +21,6 @@ export type MessageContentWithStatusSelectorProps = Pick<
type Props = { type Props = {
messageId: string; messageId: string;
onQuoteClick: (quote: QuoteClickOptions) => void;
ctxMenuID: string; ctxMenuID: string;
isDetailView?: boolean; isDetailView?: boolean;
dataTestId?: string; dataTestId?: string;
@ -64,7 +63,7 @@ export const MessageContentWithStatuses = (props: Props) => {
} }
}; };
const { messageId, onQuoteClick, ctxMenuID, isDetailView, dataTestId } = props; const { messageId, ctxMenuID, isDetailView, dataTestId } = props;
if (!contentProps) { if (!contentProps) {
return null; return null;
} }
@ -88,11 +87,7 @@ export const MessageContentWithStatuses = (props: Props) => {
<div> <div>
<MessageAuthorText messageId={messageId} /> <MessageAuthorText messageId={messageId} />
<MessageContent <MessageContent messageId={messageId} isDetailView={isDetailView} />
messageId={messageId}
isDetailView={isDetailView}
onQuoteClick={onQuoteClick}
/>
</div> </div>
<MessageStatus <MessageStatus
dataTestId="msg-status-outgoing" dataTestId="msg-status-outgoing"

@ -1,62 +1,91 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import _ from 'lodash'; import _ from 'lodash';
import { MessageRenderingProps, QuoteClickOptions } from '../../../../models/messageType'; import { MessageRenderingProps } from '../../../../models/messageType';
import { PubKey } from '../../../../session/types'; import { PubKey } from '../../../../session/types';
import { toggleSelectedMessageId } from '../../../../state/ducks/conversations'; import { openConversationToSpecificMessage } from '../../../../state/ducks/conversations';
import { import {
getMessageQuoteProps, getMessageQuoteProps,
isMessageDetailView,
isMessageSelectionMode, isMessageSelectionMode,
} from '../../../../state/selectors/conversations'; } from '../../../../state/selectors/conversations';
import { Quote } from './Quote'; import { Quote } from './Quote';
import { ToastUtils } from '../../../../session/utils';
import { getMessagesBySentAt } from '../../../../data/data';
import { MessageModel } from '../../../../models/message';
// tslint:disable: use-simple-attributes // tslint:disable: use-simple-attributes
type Props = { type Props = {
onQuoteClick?: (quote: QuoteClickOptions) => void;
messageId: string; messageId: string;
}; };
export type MessageQuoteSelectorProps = Pick<MessageRenderingProps, 'quote' | 'direction'>; export type MessageQuoteSelectorProps = Pick<MessageRenderingProps, 'quote' | 'direction'>;
export const MessageQuote = (props: Props) => { export const MessageQuote = (props: Props) => {
const { onQuoteClick: scrollToQuote } = props;
const selected = useSelector(state => getMessageQuoteProps(state as any, props.messageId)); const selected = useSelector(state => getMessageQuoteProps(state as any, props.messageId));
const dispatch = useDispatch();
const multiSelectMode = useSelector(isMessageSelectionMode); const multiSelectMode = useSelector(isMessageSelectionMode);
const isMessageDetailViewMode = useSelector(isMessageDetailView);
// const scrollToLoadedMessage = useContext(ScrollToLoadedMessageContext);
const quote = selected ? selected.quote : undefined;
const direction = selected ? selected.direction : undefined;
const onQuoteClick = useCallback( const onQuoteClick = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => { async (event: React.MouseEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (!selected?.quote) { if (!quote) {
window.log.warn('onQuoteClick: quote not valid'); window.log.warn('onQuoteClick: quote not valid');
return; return;
} }
if (multiSelectMode && props.messageId) {
dispatch(toggleSelectedMessageId(props.messageId));
if (isMessageDetailViewMode) {
// trying to scroll while in the container while the message detail view is shown has unknown effects
return;
}
const {
referencedMessageNotFound,
messageId: quotedMessageSentAt,
sender: quoteAuthor,
} = quote;
// 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 || !quotedMessageSentAt || !quoteAuthor) {
ToastUtils.pushOriginalNotFound();
return; return;
} }
const { sender, referencedMessageNotFound, messageId } = selected.quote;
const quoteId = _.toNumber(messageId);
scrollToQuote?.({ const collection = await getMessagesBySentAt(_.toNumber(quotedMessageSentAt));
quoteAuthor: sender, const foundInDb = collection.find((item: MessageModel) => {
quoteId, const messageAuthor = item.getSource();
referencedMessageNotFound: referencedMessageNotFound || false,
return Boolean(messageAuthor && quoteAuthor === messageAuthor);
}); });
if (!foundInDb) {
ToastUtils.pushOriginalNotFound();
return;
}
void openConversationToSpecificMessage({
conversationKey: foundInDb.get('conversationId'),
messageIdToNavigateTo: foundInDb.get('id'),
});
// scrollToLoadedMessage?.({
// quoteAuthor: sender,
// quoteId,
// referencedMessageNotFound: referencedMessageNotFound || false,
// });
}, },
[scrollToQuote, selected?.quote, multiSelectMode, props.messageId] [quote, multiSelectMode, props.messageId]
); );
if (!selected) { if (!selected) {
return null; return null;
} }
const { quote, direction } = selected;
if (!quote || !quote.sender || !quote.messageId) { if (!quote || !quote.sender || !quote.messageId) {
return null; return null;
} }

@ -6,7 +6,7 @@ import { useDispatch, useSelector } from 'react-redux';
import useInterval from 'react-use/lib/useInterval'; import useInterval from 'react-use/lib/useInterval';
import _ from 'lodash'; import _ from 'lodash';
import { removeMessage } from '../../../../data/data'; import { removeMessage } from '../../../../data/data';
import { MessageRenderingProps, QuoteClickOptions } from '../../../../models/messageType'; import { MessageRenderingProps } from '../../../../models/messageType';
import { getConversationController } from '../../../../session/conversations'; import { getConversationController } from '../../../../session/conversations';
import { messageExpired } from '../../../../state/ducks/conversations'; import { messageExpired } from '../../../../state/ducks/conversations';
import { import {
@ -94,7 +94,6 @@ function useIsExpired(props: ExpiringProps) {
type Props = { type Props = {
messageId: string; messageId: string;
onQuoteClick: (quote: QuoteClickOptions) => void;
ctxMenuID: string; ctxMenuID: string;
isDetailView?: boolean; isDetailView?: boolean;
}; };
@ -181,7 +180,6 @@ export const GenericReadableMessage = (props: Props) => {
<MessageContentWithStatuses <MessageContentWithStatuses
ctxMenuID={props.ctxMenuID} ctxMenuID={props.ctxMenuID}
messageId={messageId} messageId={messageId}
onQuoteClick={props.onQuoteClick}
isDetailView={isDetailView} isDetailView={isDetailView}
dataTestId={`message-content-${messageId}`} dataTestId={`message-content-${messageId}`}
/> />

@ -3,7 +3,6 @@ import React from 'react';
import _ from 'lodash'; import _ from 'lodash';
import uuid from 'uuid'; import uuid from 'uuid';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { QuoteClickOptions } from '../../../../models/messageType';
import { getGenericReadableMessageSelectorProps } from '../../../../state/selectors/conversations'; import { getGenericReadableMessageSelectorProps } from '../../../../state/selectors/conversations';
import { GenericReadableMessage } from './GenericReadableMessage'; import { GenericReadableMessage } from './GenericReadableMessage';
@ -13,7 +12,6 @@ export const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
type Props = { type Props = {
messageId: string; messageId: string;
isDetailView?: boolean; // when the detail is shown for a message, we disble click and some other stuff isDetailView?: boolean; // when the detail is shown for a message, we disble click and some other stuff
onQuoteClick?: (options: QuoteClickOptions) => Promise<void>;
}; };
export const Message = (props: Props) => { export const Message = (props: Props) => {
@ -22,9 +20,6 @@ export const Message = (props: Props) => {
); );
const ctxMenuID = `ctx-menu-message-${uuid()}`; const ctxMenuID = `ctx-menu-message-${uuid()}`;
const onQuoteClick = (quote: QuoteClickOptions) => {
void props.onQuoteClick?.(quote);
};
if (msgProps?.isDeleted && msgProps.direction === 'outgoing') { if (msgProps?.isDeleted && msgProps.direction === 'outgoing') {
return null; return null;
@ -32,7 +27,6 @@ export const Message = (props: Props) => {
return ( return (
<GenericReadableMessage <GenericReadableMessage
onQuoteClick={onQuoteClick}
ctxMenuID={ctxMenuID} ctxMenuID={ctxMenuID}
messageId={props.messageId} messageId={props.messageId}
isDetailView={props.isDetailView} isDetailView={props.isDetailView}

@ -5,17 +5,20 @@ import { useDispatch, useSelector } from 'react-redux';
import { getMessageById } from '../../../../data/data'; import { getMessageById } from '../../../../data/data';
import { getConversationController } from '../../../../session/conversations'; import { getConversationController } from '../../../../session/conversations';
import { import {
fetchBottomMessagesForConversation,
fetchTopMessagesForConversation, fetchTopMessagesForConversation,
markConversationFullyRead, markConversationFullyRead,
showScrollToBottomButton, showScrollToBottomButton,
} from '../../../../state/ducks/conversations'; } from '../../../../state/ducks/conversations';
import { import {
areMoreBottomMessagesBeingFetched,
areMoreTopMessagesBeingFetched, areMoreTopMessagesBeingFetched,
getHaveDoneFirstScroll, getHaveDoneFirstScroll,
getLoadedMessagesLength, getLoadedMessagesLength,
getMostRecentMessageId, getMostRecentMessageId,
getOldestMessageId, getOldestMessageId,
getSelectedConversationKey, getSelectedConversationKey,
getYoungestMessageId,
} from '../../../../state/selectors/conversations'; } from '../../../../state/selectors/conversations';
import { getIsAppFocused } from '../../../../state/selectors/section'; import { getIsAppFocused } from '../../../../state/selectors/section';
@ -40,6 +43,18 @@ const debouncedTriggerLoadMoreTop = _.debounce(
100 100
); );
const debouncedTriggerLoadMoreBottom = _.debounce(
(selectedConversationKey: string, youngestMessageId: string) => {
(window.inboxStore?.dispatch as any)(
fetchBottomMessagesForConversation({
conversationKey: selectedConversationKey,
oldBottomMessageId: youngestMessageId,
})
);
},
100
);
export const ReadableMessage = (props: ReadableMessageProps) => { export const ReadableMessage = (props: ReadableMessageProps) => {
const { messageId, onContextMenu, className, receivedAt, isUnread } = props; const { messageId, onContextMenu, className, receivedAt, isUnread } = props;
@ -51,7 +66,9 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
const haveDoneFirstScroll = useSelector(getHaveDoneFirstScroll); const haveDoneFirstScroll = useSelector(getHaveDoneFirstScroll);
const mostRecentMessageId = useSelector(getMostRecentMessageId); const mostRecentMessageId = useSelector(getMostRecentMessageId);
const oldestMessageId = useSelector(getOldestMessageId); const oldestMessageId = useSelector(getOldestMessageId);
const fetchingMore = useSelector(areMoreTopMessagesBeingFetched); const youngestMessageId = useSelector(getYoungestMessageId);
const fetchingTopMore = useSelector(areMoreTopMessagesBeingFetched);
const fetchingBottomMore = useSelector(areMoreBottomMessagesBeingFetched);
const shouldMarkReadWhenVisible = isUnread; const shouldMarkReadWhenVisible = isUnread;
const onVisible = useCallback( const onVisible = useCallback(
@ -85,12 +102,22 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
inView === true && inView === true &&
isAppFocused && isAppFocused &&
oldestMessageId === messageId && oldestMessageId === messageId &&
!fetchingMore && !fetchingTopMore &&
selectedConversationKey selectedConversationKey
) { ) {
debouncedTriggerLoadMoreTop(selectedConversationKey, oldestMessageId); debouncedTriggerLoadMoreTop(selectedConversationKey, oldestMessageId);
} }
if (
inView === true &&
isAppFocused &&
youngestMessageId === messageId &&
!fetchingBottomMore &&
selectedConversationKey
) {
debouncedTriggerLoadMoreBottom(selectedConversationKey, youngestMessageId);
}
// this part is just handling the marking of the message as read if needed // this part is just handling the marking of the message as read if needed
if ( if (
(inView === true || (inView === true ||
@ -113,7 +140,8 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
haveDoneFirstScroll, haveDoneFirstScroll,
mostRecentMessageId, mostRecentMessageId,
oldestMessageId, oldestMessageId,
fetchingMore, fetchingTopMore,
fetchingBottomMore,
isAppFocused, isAppFocused,
loadedMessagesLength, loadedMessagesLength,
receivedAt, receivedAt,

@ -5,12 +5,13 @@ import { MessageDirection } from '../../models/messageType';
import { getOurPubKeyStrFromCache } from '../../session/utils/User'; import { getOurPubKeyStrFromCache } from '../../session/utils/User';
import { import {
FindAndFormatContactType, FindAndFormatContactType,
openConversationWithMessages, openConversationToSpecificMessage,
} from '../../state/ducks/conversations'; } from '../../state/ducks/conversations';
import { ContactName } from '../conversation/ContactName'; import { ContactName } from '../conversation/ContactName';
import { Avatar, AvatarSize } from '../avatar/Avatar'; import { Avatar, AvatarSize } from '../avatar/Avatar';
import { Timestamp } from '../conversation/Timestamp'; import { Timestamp } from '../conversation/Timestamp';
import { MessageBodyHighlight } from '../basic/MessageBodyHighlight'; import { MessageBodyHighlight } from '../basic/MessageBodyHighlight';
import styled from 'styled-components';
type PropsHousekeeping = { type PropsHousekeeping = {
isSelected?: boolean; isSelected?: boolean;
@ -83,17 +84,25 @@ const AvatarItem = (props: { source: string }) => {
return <Avatar size={AvatarSize.S} pubkey={source} />; return <Avatar size={AvatarSize.S} pubkey={source} />;
}; };
const ResultBody = styled.div`
margin-top: 1px;
flex-grow: 1;
flex-shrink: 1;
font-size: 13px;
color: var(--color-text-subtle);
max-height: 3.6em;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
`;
export const MessageSearchResult = (props: MessageResultProps) => { export const MessageSearchResult = (props: MessageResultProps) => {
const { const { id, conversationId, receivedAt, snippet, destination, source, direction } = props;
isSelected,
id,
conversationId,
receivedAt,
snippet,
destination,
source,
direction,
} = props;
// Some messages miss a source or destination. Doing checks to see if the fields can be derived from other sources. // Some messages miss a source or destination. Doing checks to see if the fields can be derived from other sources.
// E.g. if the source is missing but the message is outgoing, the source will be our pubkey // E.g. if the source is missing but the message is outgoing, the source will be our pubkey
@ -119,15 +128,12 @@ export const MessageSearchResult = (props: MessageResultProps) => {
key={`div-msg-searchresult-${id}`} key={`div-msg-searchresult-${id}`}
role="button" role="button"
onClick={async () => { onClick={async () => {
await openConversationWithMessages({ await openConversationToSpecificMessage({
conversationKey: conversationId, conversationKey: conversationId,
messageId: id, messageIdToNavigateTo: id,
}); });
}} }}
className={classNames( className={classNames('module-message-search-result')}
'module-message-search-result',
isSelected ? 'module-message-search-result--is-selected' : null
)}
> >
<AvatarItem source={effectiveSource} /> <AvatarItem source={effectiveSource} />
<div className="module-message-search-result__text"> <div className="module-message-search-result__text">
@ -137,9 +143,9 @@ export const MessageSearchResult = (props: MessageResultProps) => {
<Timestamp timestamp={receivedAt} /> <Timestamp timestamp={receivedAt} />
</div> </div>
</div> </div>
<div className="module-message-search-result__body"> <ResultBody>
<MessageBodyHighlight text={snippet || ''} /> <MessageBodyHighlight text={snippet || ''} />
</div> </ResultBody>
</div> </div>
</div> </div>
); );

@ -9,7 +9,6 @@ export type SearchResultsProps = {
contacts: Array<ConversationListItemProps>; contacts: Array<ConversationListItemProps>;
conversations: Array<ConversationListItemProps>; conversations: Array<ConversationListItemProps>;
messages: Array<MessageResultProps>; messages: Array<MessageResultProps>;
hideMessagesHeader: boolean;
searchTerm: string; searchTerm: string;
}; };
@ -18,14 +17,17 @@ const ContactsItem = (props: { header: string; items: Array<ConversationListItem
<div className="module-search-results__contacts"> <div className="module-search-results__contacts">
<div className="module-search-results__contacts-header">{props.header}</div> <div className="module-search-results__contacts-header">{props.header}</div>
{props.items.map(contact => ( {props.items.map(contact => (
<MemoConversationListItemWithDetails {...contact} /> <MemoConversationListItemWithDetails
{...contact}
key={`search-result-contact-${contact.id}`}
/>
))} ))}
</div> </div>
); );
}; };
export const SearchResults = (props: SearchResultsProps) => { export const SearchResults = (props: SearchResultsProps) => {
const { conversations, contacts, messages, searchTerm, hideMessagesHeader } = props; const { conversations, contacts, messages, searchTerm } = props;
const haveConversations = conversations && conversations.length; const haveConversations = conversations && conversations.length;
const haveContacts = contacts && contacts.length; const haveContacts = contacts && contacts.length;
@ -45,7 +47,11 @@ export const SearchResults = (props: SearchResultsProps) => {
{window.i18n('conversationsHeader')} {window.i18n('conversationsHeader')}
</div> </div>
{conversations.map(conversation => ( {conversations.map(conversation => (
<MemoConversationListItemWithDetails {...conversation} /> <MemoConversationListItemWithDetails
{...conversation}
mentionedUs={false}
key={`search-result-convo-${conversation.id}`}
/>
))} ))}
</div> </div>
) : null} ) : null}
@ -55,13 +61,11 @@ export const SearchResults = (props: SearchResultsProps) => {
{haveMessages ? ( {haveMessages ? (
<div className="module-search-results__messages"> <div className="module-search-results__messages">
{hideMessagesHeader ? null : ( <div className="module-search-results__messages-header">
<div className="module-search-results__messages-header"> {`${window.i18n('messagesHeader')}: ${messages.length}`}
{window.i18n('messagesHeader')} </div>
</div>
)}
{messages.map(message => ( {messages.map(message => (
<MessageSearchResult key={`search-result-${message.id}`} {...message} /> <MessageSearchResult key={`search-result-message-${message.id}`} {...message} />
))} ))}
</div> </div>
) : null} ) : null}

@ -587,9 +587,12 @@ export async function searchConversations(query: string): Promise<Array<any>> {
return conversations; return conversations;
} }
export async function searchMessages(query: string, { limit }: any = {}): Promise<Array<any>> { export async function searchMessages(query: string, limit: number): Promise<Array<Object>> {
const messages = await channels.searchMessages(query, { limit }); const messages = await channels.searchMessages(query, limit);
return messages;
return _.uniqWith(messages, (left: { id: string }, right: { id: string }) => {
return left.id === right.id;
});
} }
/** /**
@ -598,10 +601,10 @@ export async function searchMessages(query: string, { limit }: any = {}): Promis
export async function searchMessagesInConversation( export async function searchMessagesInConversation(
query: string, query: string,
conversationId: string, conversationId: string,
options: { limit: number } | undefined limit: number
): Promise<Object> { ): Promise<Object> {
const messages = await channels.searchMessagesInConversation(query, conversationId, { const messages = await channels.searchMessagesInConversation(query, conversationId, {
limit: options?.limit, limit,
}); });
return messages; return messages;
} }

@ -216,12 +216,6 @@ export const fillMessageAttributesWithDefaults = (
return defaulted; return defaulted;
}; };
export type QuoteClickOptions = {
quoteAuthor: string;
quoteId: number;
referencedMessageNotFound: boolean;
};
/** /**
* Those props are the one generated from a single Message improved by the one by the app itself. * Those props are the one generated from a single Message improved by the one by the app itself.
* Some of the one added comes from the MessageList, some from redux, etc.. * Some of the one added comes from the MessageList, some from redux, etc..
@ -235,5 +229,4 @@ export type MessageRenderingProps = PropsForMessageWithConvoProps & {
multiSelectMode: boolean; multiSelectMode: boolean;
firstMessageOfSeries: boolean; firstMessageOfSeries: boolean;
lastMessageOfSeries: boolean; lastMessageOfSeries: boolean;
onQuoteClick?: (options: QuoteClickOptions) => Promise<void>;
}; };

@ -1,7 +1,6 @@
import { initIncomingMessage } from './dataMessage'; import { initIncomingMessage } from './dataMessage';
import { toNumber } from 'lodash'; import { toNumber } from 'lodash';
import { getConversationController } from '../session/conversations'; import { getConversationController } from '../session/conversations';
import { actions as conversationActions } from '../state/ducks/conversations';
import { ConversationTypeEnum } from '../models/conversation'; import { ConversationTypeEnum } from '../models/conversation';
import { toLogFormat } from '../types/attachments/Errors'; import { toLogFormat } from '../types/attachments/Errors';

@ -273,7 +273,9 @@ export type ConversationsStateType = {
lightBox?: LightBoxOptions; lightBox?: LightBoxOptions;
quotedMessage?: ReplyingToMessageProps; quotedMessage?: ReplyingToMessageProps;
areMoreTopMessagesBeingFetched: boolean; areMoreTopMessagesBeingFetched: boolean;
areMoreBottomMessagesBeingFetched: boolean;
oldTopMessageId: string | null; oldTopMessageId: string | null;
oldBottomMessageId: string | null;
haveDoneFirstScroll: boolean; haveDoneFirstScroll: boolean;
showScrollButton: boolean; showScrollButton: boolean;
@ -316,31 +318,27 @@ export type SortedMessageModelProps = MessageModelPropsWithoutConvoProps & {
lastMessageOfSeries: boolean; lastMessageOfSeries: boolean;
}; };
type FetchedMessageResults = { type FetchedTopMessageResults = {
conversationKey: string; conversationKey: string;
messagesProps: Array<MessageModelPropsWithoutConvoProps>; messagesProps: Array<MessageModelPropsWithoutConvoProps>;
oldTopMessageId: string | null; oldTopMessageId: string | null;
}; };
export const fetchTopMessagesForConversation = createAsyncThunk( export const fetchTopMessagesForConversation = createAsyncThunk(
'messages/fetchByConversationKey', 'messages/fetchTopByConversationKey',
async ({ async ({
conversationKey, conversationKey,
oldTopMessageId, oldTopMessageId,
}: { }: {
conversationKey: string; conversationKey: string;
oldTopMessageId: string | null; oldTopMessageId: string | null;
}): Promise<FetchedMessageResults> => { }): Promise<FetchedTopMessageResults> => {
const beforeTimestamp = Date.now(); const beforeTimestamp = Date.now();
perfStart('fetchTopMessagesForConversation');
const messagesProps = await getMessages({ const messagesProps = await getMessages({
conversationKey, conversationKey,
messageId: oldTopMessageId, messageId: oldTopMessageId,
}); });
const afterTimestamp = Date.now(); const time = Date.now() - beforeTimestamp;
perfEnd('fetchTopMessagesForConversation', 'fetchTopMessagesForConversation');
const time = afterTimestamp - beforeTimestamp;
window?.log?.info(`Loading ${messagesProps.length} messages took ${time}ms to load.`); window?.log?.info(`Loading ${messagesProps.length} messages took ${time}ms to load.`);
return { return {
@ -351,6 +349,37 @@ export const fetchTopMessagesForConversation = createAsyncThunk(
} }
); );
type FetchedBottomMessageResults = {
conversationKey: string;
messagesProps: Array<MessageModelPropsWithoutConvoProps>;
oldBottomMessageId: string | null;
};
export const fetchBottomMessagesForConversation = createAsyncThunk(
'messages/fetchBottomByConversationKey',
async ({
conversationKey,
oldBottomMessageId,
}: {
conversationKey: string;
oldBottomMessageId: string | null;
}): Promise<FetchedBottomMessageResults> => {
const beforeTimestamp = Date.now();
const messagesProps = await getMessages({
conversationKey,
messageId: oldBottomMessageId,
});
const time = Date.now() - beforeTimestamp;
window?.log?.info(`Loading ${messagesProps.length} messages took ${time}ms to load.`);
return {
conversationKey,
messagesProps,
oldBottomMessageId,
};
}
);
// Reducer // Reducer
export function getEmptyConversationState(): ConversationsStateType { export function getEmptyConversationState(): ConversationsStateType {
@ -361,11 +390,13 @@ export function getEmptyConversationState(): ConversationsStateType {
showRightPanel: false, showRightPanel: false,
selectedMessageIds: [], selectedMessageIds: [],
areMoreTopMessagesBeingFetched: false, areMoreTopMessagesBeingFetched: false,
areMoreBottomMessagesBeingFetched: false,
showScrollButton: false, showScrollButton: false,
mentionMembers: [], mentionMembers: [],
firstUnreadMessageId: undefined, firstUnreadMessageId: undefined,
haveDoneFirstScroll: false, haveDoneFirstScroll: false,
oldTopMessageId: null, oldTopMessageId: null,
oldBottomMessageId: null,
}; };
} }
@ -695,6 +726,7 @@ const conversationsSlice = createSlice({
selectedConversation: action.payload.conversationKey, selectedConversation: action.payload.conversationKey,
areMoreTopMessagesBeingFetched: false, areMoreTopMessagesBeingFetched: false,
areMoreBottomMessagesBeingFetched: false,
messages: action.payload.initialMessages, messages: action.payload.initialMessages,
showRightPanel: false, showRightPanel: false,
selectedMessageIds: [], selectedMessageIds: [],
@ -706,6 +738,7 @@ const conversationsSlice = createSlice({
showScrollButton: false, showScrollButton: false,
animateQuotedMessageId: undefined, animateQuotedMessageId: undefined,
oldTopMessageId: null, oldTopMessageId: null,
oldBottomMessageId: null,
mentionMembers: [], mentionMembers: [],
firstUnreadMessageId: action.payload.firstUnreadIdOnOpen, firstUnreadMessageId: action.payload.firstUnreadIdOnOpen,
@ -720,23 +753,26 @@ const conversationsSlice = createSlice({
initialMessages: Array<MessageModelPropsWithoutConvoProps>; initialMessages: Array<MessageModelPropsWithoutConvoProps>;
}> }>
) { ) {
if (state.selectedConversation !== action.payload.conversationKey) {
return state;
}
return { return {
...state, ...state,
selectedConversation: action.payload.conversationKey,
areMoreTopMessagesBeingFetched: false, areMoreTopMessagesBeingFetched: false,
areMoreBottomMessagesBeingFetched: false,
messages: action.payload.initialMessages, messages: action.payload.initialMessages,
showScrollButton: true, showScrollButton: true,
animateQuotedMessageId: action.payload.messageIdToNavigateTo, animateQuotedMessageId: action.payload.messageIdToNavigateTo,
oldTopMessageId: null, oldTopMessageId: null,
oldBottomMessageId: null,
}; };
}, },
resetOldTopMessageId(state: ConversationsStateType) { resetOldTopMessageId(state: ConversationsStateType) {
state.oldTopMessageId = null; state.oldTopMessageId = null;
return state; return state;
}, },
resetOldBottomMessageId(state: ConversationsStateType) {
state.oldBottomMessageId = null;
return state;
},
updateHaveDoneFirstScroll(state: ConversationsStateType) { updateHaveDoneFirstScroll(state: ConversationsStateType) {
state.haveDoneFirstScroll = true; state.haveDoneFirstScroll = true;
return state; return state;
@ -786,7 +822,7 @@ const conversationsSlice = createSlice({
// Add reducers for additional action types here, and handle loading state as needed // Add reducers for additional action types here, and handle loading state as needed
builder.addCase( builder.addCase(
fetchTopMessagesForConversation.fulfilled, fetchTopMessagesForConversation.fulfilled,
(state: ConversationsStateType, action: PayloadAction<FetchedMessageResults>) => { (state: ConversationsStateType, action: PayloadAction<FetchedTopMessageResults>) => {
// this is called once the messages are loaded from the db for the currently selected conversation // this is called once the messages are loaded from the db for the currently selected conversation
const { messagesProps, conversationKey, oldTopMessageId } = action.payload; const { messagesProps, conversationKey, oldTopMessageId } = action.payload;
// double check that this update is for the shown convo // double check that this update is for the shown convo
@ -807,6 +843,32 @@ const conversationsSlice = createSlice({
builder.addCase(fetchTopMessagesForConversation.rejected, (state: ConversationsStateType) => { builder.addCase(fetchTopMessagesForConversation.rejected, (state: ConversationsStateType) => {
state.areMoreTopMessagesBeingFetched = false; state.areMoreTopMessagesBeingFetched = false;
}); });
builder.addCase(
fetchBottomMessagesForConversation.fulfilled,
(state: ConversationsStateType, action: PayloadAction<FetchedBottomMessageResults>) => {
// this is called once the messages are loaded from the db for the currently selected conversation
const { messagesProps, conversationKey, oldBottomMessageId } = action.payload;
// double check that this update is for the shown convo
if (conversationKey === state.selectedConversation) {
return {
...state,
oldBottomMessageId,
messages: messagesProps,
areMoreBottomMessagesBeingFetched: false,
};
}
return state;
}
);
builder.addCase(fetchBottomMessagesForConversation.pending, (state: ConversationsStateType) => {
state.areMoreBottomMessagesBeingFetched = true;
});
builder.addCase(
fetchBottomMessagesForConversation.rejected,
(state: ConversationsStateType) => {
state.areMoreBottomMessagesBeingFetched = false;
}
);
}, },
}); });
@ -850,6 +912,7 @@ export const {
messageChanged, messageChanged,
messagesChanged, messagesChanged,
resetOldTopMessageId, resetOldTopMessageId,
resetOldBottomMessageId,
updateHaveDoneFirstScroll, updateHaveDoneFirstScroll,
markConversationFullyRead, markConversationFullyRead,
// layout stuff // layout stuff

@ -30,7 +30,7 @@ type SearchResultsPayloadType = {
conversations: Array<string>; conversations: Array<string>;
contacts: Array<string>; contacts: Array<string>;
messages?: Array<string>; messages?: Array<Object>;
}; };
type SearchResultsKickoffActionType = { type SearchResultsKickoffActionType = {
@ -83,17 +83,20 @@ async function doSearch(query: string, options: SearchOptions): Promise<SearchRe
]); ]);
const { conversations, contacts } = discussions; const { conversations, contacts } = discussions;
let filteredMessages = _.compact(messages); let filteredMessages = _.compact(messages);
if (isAdvancedQuery) { if (isAdvancedQuery) {
let senderFilter: Array<string> = [];
if (advancedSearchOptions.from && advancedSearchOptions.from.length > 0) { if (advancedSearchOptions.from && advancedSearchOptions.from.length > 0) {
const senderFilterQuery = await queryConversationsAndContacts( const senderFilterQuery = await queryConversationsAndContacts(
advancedSearchOptions.from, advancedSearchOptions.from,
options options
); );
senderFilter = senderFilterQuery.contacts; filteredMessages = filterMessages(
filteredMessages,
advancedSearchOptions,
senderFilterQuery.contacts
);
} else {
filteredMessages = filterMessages(filteredMessages, advancedSearchOptions, []);
} }
filteredMessages = filterMessages(filteredMessages, advancedSearchOptions, senderFilter);
} }
return { return {
query, query,
@ -201,7 +204,7 @@ function getAdvancedSearchOptionsFromQuery(query: string): AdvancedSearchOptions
async function queryMessages(query: string) { async function queryMessages(query: string) {
try { try {
const normalized = cleanSearchTerm(query); const normalized = cleanSearchTerm(query);
return searchMessages(normalized); return searchMessages(normalized, 1000);
} catch (e) { } catch (e) {
return []; return [];
} }

@ -597,6 +597,11 @@ export const areMoreTopMessagesBeingFetched = createSelector(
(state: ConversationsStateType): boolean => state.areMoreTopMessagesBeingFetched || false (state: ConversationsStateType): boolean => state.areMoreTopMessagesBeingFetched || false
); );
export const areMoreBottomMessagesBeingFetched = createSelector(
getConversations,
(state: ConversationsStateType): boolean => state.areMoreBottomMessagesBeingFetched || false
);
export const getHaveDoneFirstScroll = createSelector( export const getHaveDoneFirstScroll = createSelector(
getConversations, getConversations,
(state: ConversationsStateType): boolean => state.haveDoneFirstScroll (state: ConversationsStateType): boolean => state.haveDoneFirstScroll
@ -696,6 +701,15 @@ export const getOldestMessageId = createSelector(
} }
); );
export const getYoungestMessageId = createSelector(
getSortedMessagesOfSelectedConversation,
(messages: Array<MessageModelPropsWithoutConvoProps>): string | undefined => {
const youngest = messages.length > 0 ? messages[0].propsForMessage.id : undefined;
return youngest;
}
);
export const getLoadedMessagesLength = createSelector( export const getLoadedMessagesLength = createSelector(
getConversations, getConversations,
(state: ConversationsStateType): number => { (state: ConversationsStateType): number => {
@ -1123,3 +1137,8 @@ export const getOldTopMessageId = createSelector(
getConversations, getConversations,
(state: ConversationsStateType): string | null => state.oldTopMessageId || null (state: ConversationsStateType): string | null => state.oldTopMessageId || null
); );
export const getOldBottomMessageId = createSelector(
getConversations,
(state: ConversationsStateType): string | null => state.oldBottomMessageId || null
);

@ -11,11 +11,6 @@ export const getSearch = (state: StateType): SearchStateType => state.search;
export const getQuery = createSelector(getSearch, (state: SearchStateType): string => state.query); export const getQuery = createSelector(getSearch, (state: SearchStateType): string => state.query);
export const getSelectedMessage = createSelector(
getSearch,
(state: SearchStateType): string | undefined => state.selectedMessage
);
export const isSearching = createSelector(getSearch, (state: SearchStateType) => { export const isSearching = createSelector(getSearch, (state: SearchStateType) => {
const { query } = state; const { query } = state;
@ -23,13 +18,8 @@ export const isSearching = createSelector(getSearch, (state: SearchStateType) =>
}); });
export const getSearchResults = createSelector( export const getSearchResults = createSelector(
[getSearch, getConversationLookup, getSelectedConversationKey, getSelectedMessage], [getSearch, getConversationLookup, getSelectedConversationKey],
( (searchState: SearchStateType, lookup: ConversationLookupType, selectedConversation?: string) => {
searchState: SearchStateType,
lookup: ConversationLookupType,
selectedConversation?: string,
selectedMessage?: string
) => {
return { return {
contacts: compact( contacts: compact(
searchState.contacts.map(id => { searchState.contacts.map(id => {
@ -65,20 +55,7 @@ export const getSearchResults = createSelector(
return value; return value;
}) })
), ),
messages: compact( messages: compact(searchState.messages),
searchState.messages?.map(message => {
if (message.id === selectedMessage) {
return {
...message,
isSelected: true,
};
}
return message;
})
),
hideMessagesHeader: false,
searchTerm: searchState.query, searchTerm: searchState.query,
}; };
} }

Loading…
Cancel
Save