use selector to sort and add first of serie flag
This commit is contained in:
parent
6d34a60f94
commit
e72885944b
|
@ -34,7 +34,7 @@ export const UserDetailsDialog = (props: Props) => {
|
|||
ConversationTypeEnum.PRIVATE
|
||||
);
|
||||
|
||||
window.inboxStore?.dispatch(openConversationExternal(conversation.id));
|
||||
window.inboxStore?.dispatch(openConversationExternal({ id: conversation.id }));
|
||||
|
||||
closeDialog();
|
||||
}
|
||||
|
|
|
@ -40,11 +40,17 @@ import { getMessageById } from '../../data/data';
|
|||
import { connect } from 'react-redux';
|
||||
import { StateType } from '../../state/reducer';
|
||||
import { getSelectedMessageIds } from '../../state/selectors/conversations';
|
||||
import { showLightBox, toggleSelectedMessageId } from '../../state/ducks/conversations';
|
||||
import {
|
||||
messageExpired,
|
||||
showLightBox,
|
||||
toggleSelectedMessageId,
|
||||
} from '../../state/ducks/conversations';
|
||||
import { saveAttachmentToDisk } from '../../util/attachmentsUtil';
|
||||
import { LightBoxOptions } from '../session/conversation/SessionConversation';
|
||||
import { MessageContextMenu } from './MessageContextMenu';
|
||||
import { ReadableMessage } from './ReadableMessage';
|
||||
import { remote } from 'electron';
|
||||
import { isElectronWindowFocused } from '../../session/utils/WindowUtils';
|
||||
|
||||
// Same as MIN_WIDTH in ImageGrid.tsx
|
||||
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
|
||||
|
@ -161,6 +167,9 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
this.setState({
|
||||
expired: true,
|
||||
});
|
||||
window.inboxStore?.dispatch(
|
||||
messageExpired({ messageId: this.props.id, conversationKey: this.props.convoId })
|
||||
);
|
||||
};
|
||||
this.expiredTimeout = setTimeout(setExpired, EXPIRED_DELAY);
|
||||
}
|
||||
|
@ -597,14 +606,13 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
const onVisible = async (inView: boolean | Object) => {
|
||||
if (inView === true && shouldMarkReadWhenVisible && window.isFocused()) {
|
||||
if (inView === true && shouldMarkReadWhenVisible && isElectronWindowFocused()) {
|
||||
const found = await getMessageById(id);
|
||||
|
||||
if (found && Boolean(found.get('unread'))) {
|
||||
console.warn('marking as read: ', found.id);
|
||||
// mark the message as read.
|
||||
// this will trigger the expire timer.
|
||||
void found?.markRead(Date.now());
|
||||
void found.markRead(Date.now());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -217,7 +217,7 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
|
|||
{descriptionLong && <div className="session-description-long">{descriptionLong}</div>}
|
||||
{isMessageView && false && <h4>{window.i18n('or')}</h4>}
|
||||
{/* FIXME enable back those two items when they are working */}
|
||||
{isOpenGroupView && <SessionJoinableRooms />}
|
||||
{isOpenGroupView && <SessionJoinableRooms onRoomClicked={this.props.onCloseClick} />}
|
||||
{isMessageView && false && (
|
||||
<UserSearchDropdown
|
||||
searchTerm={searchTerm || ''}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
joinOpenGroupV2WithUIEvents,
|
||||
|
@ -81,9 +81,18 @@ const SessionJoinableRoomRow = (props: JoinableRoomProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const SessionJoinableRooms = () => {
|
||||
export const SessionJoinableRooms = (props: { onRoomClicked: () => void }) => {
|
||||
const joinableRooms = useSelector((state: StateType) => state.defaultRooms);
|
||||
|
||||
const onRoomClicked = useCallback(
|
||||
(loading: boolean) => {
|
||||
if (loading) {
|
||||
props.onRoomClicked();
|
||||
}
|
||||
},
|
||||
[props.onRoomClicked]
|
||||
);
|
||||
|
||||
if (!joinableRooms.inProgress && !joinableRooms.rooms?.length) {
|
||||
window?.log?.info('no default joinable rooms yet and not in progress');
|
||||
return <></>;
|
||||
|
@ -101,7 +110,7 @@ export const SessionJoinableRooms = () => {
|
|||
roomId={r.id}
|
||||
base64Data={r.base64Data}
|
||||
onClick={completeUrl => {
|
||||
void joinOpenGroupV2WithUIEvents(completeUrl, true, false);
|
||||
void joinOpenGroupV2WithUIEvents(completeUrl, true, false, onRoomClicked);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -837,7 +837,6 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
|
||||
const { quotedMessageProps } = this.props;
|
||||
|
||||
console.warn('quotedMessageProps', quotedMessageProps);
|
||||
const { stagedLinkPreview } = this.state;
|
||||
|
||||
// Send message
|
||||
|
|
|
@ -33,11 +33,7 @@ import { getPubkeysInPublicConversation } from '../../../data/data';
|
|||
import autoBind from 'auto-bind';
|
||||
|
||||
interface State {
|
||||
unreadCount: number;
|
||||
|
||||
showOverlay: boolean;
|
||||
showRecordingView: boolean;
|
||||
|
||||
stagedAttachments: Array<StagedAttachmentType>;
|
||||
isDraggingFile: boolean;
|
||||
}
|
||||
|
@ -70,10 +66,7 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
const unreadCount = this.props.selectedConversation?.unreadCount || 0;
|
||||
this.state = {
|
||||
unreadCount,
|
||||
showOverlay: false,
|
||||
showRecordingView: false,
|
||||
stagedAttachments: [],
|
||||
isDraggingFile: false,
|
||||
|
@ -136,7 +129,6 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
if (newConversationKey !== oldConversationKey) {
|
||||
void this.loadInitialMessages();
|
||||
this.setState({
|
||||
showOverlay: false,
|
||||
showRecordingView: false,
|
||||
stagedAttachments: [],
|
||||
isDraggingFile: false,
|
||||
|
@ -350,7 +342,6 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
media.length > 1
|
||||
? media.findIndex(mediaMessage => mediaMessage.attachment.path === attachment.path)
|
||||
: 0;
|
||||
console.warn('renderLightBox', { media, attachment });
|
||||
return <LightboxGallery media={media} selectedIndex={selectedIndex} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,15 +37,17 @@ import { DataExtractionNotification } from '../../conversation/DataExtractionNot
|
|||
import { StateType } from '../../../state/reducer';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import {
|
||||
areMoreMessagesBeingFetched,
|
||||
getMessagesOfSelectedConversation,
|
||||
getSortedMessagesOfSelectedConversation,
|
||||
getNextMessageToPlayIndex,
|
||||
getQuotedMessageToAnimate,
|
||||
getSelectedConversation,
|
||||
getSelectedConversationKey,
|
||||
getShowScrollButton,
|
||||
isMessageSelectionMode,
|
||||
getFirstUnreadMessageIndex,
|
||||
areMoreMessagesBeingFetched,
|
||||
} from '../../../state/selectors/conversations';
|
||||
import { isElectronWindowFocused } from '../../../session/utils/WindowUtils';
|
||||
|
||||
export type SessionMessageListProps = {
|
||||
messageContainerRef: React.RefObject<any>;
|
||||
|
@ -58,6 +60,7 @@ type Props = SessionMessageListProps & {
|
|||
conversation?: ReduxConversationType;
|
||||
showScrollButton: boolean;
|
||||
animateQuotedMessageId: string | undefined;
|
||||
areMoreMessagesBeingFetched: boolean;
|
||||
};
|
||||
|
||||
const UnreadIndicator = (props: { messageId: string; show: boolean }) => {
|
||||
|
@ -170,14 +173,13 @@ const MessageList = (props: {
|
|||
scrollToQuoteMessage: (options: QuoteClickOptions) => Promise<void>;
|
||||
playNextMessage?: (value: number) => void;
|
||||
}) => {
|
||||
const messagesProps = useSelector(getMessagesOfSelectedConversation);
|
||||
const isFetchingMore = useSelector(areMoreMessagesBeingFetched);
|
||||
|
||||
const messagesProps = useSelector(getSortedMessagesOfSelectedConversation);
|
||||
const firstUnreadMessageIndex = useSelector(getFirstUnreadMessageIndex);
|
||||
let playableMessageIndex = 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{messagesProps.map((messageProps: SortedMessageModelProps) => {
|
||||
{messagesProps.map((messageProps: SortedMessageModelProps, index: number) => {
|
||||
const timerProps = messageProps.propsForTimerNotification;
|
||||
const propsForGroupInvitation = messageProps.propsForGroupInvitation;
|
||||
const propsForDataExtractionNotification = messageProps.propsForDataExtractionNotification;
|
||||
|
@ -187,7 +189,8 @@ const MessageList = (props: {
|
|||
// IF we found the first unread message
|
||||
// AND we are not scrolled all the way to the bottom
|
||||
// THEN, show the unread banner for the current message
|
||||
const showUnreadIndicator = Boolean(messageProps.firstUnread);
|
||||
const showUnreadIndicator =
|
||||
Boolean(firstUnreadMessageIndex) && firstUnreadMessageIndex === index;
|
||||
|
||||
if (groupNotificationProps) {
|
||||
return (
|
||||
|
@ -375,7 +378,7 @@ class SessionMessagesListInner extends React.Component<Props> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.getScrollOffsetBottomPx() === 0 && window.isFocused()) {
|
||||
if (this.getScrollOffsetBottomPx() === 0 && isElectronWindowFocused()) {
|
||||
void conversation.markRead(messagesProps[0].propsForMessage.receivedAt || 0);
|
||||
}
|
||||
}
|
||||
|
@ -449,7 +452,9 @@ class SessionMessagesListInner extends React.Component<Props> {
|
|||
}
|
||||
|
||||
// Fetch more messages when nearing the top of the message list
|
||||
const shouldFetchMoreMessagesTop = scrollTop <= Constants.UI.MESSAGE_CONTAINER_BUFFER_OFFSET_PX;
|
||||
const shouldFetchMoreMessagesTop =
|
||||
scrollTop <= Constants.UI.MESSAGE_CONTAINER_BUFFER_OFFSET_PX &&
|
||||
!this.props.areMoreMessagesBeingFetched;
|
||||
|
||||
if (shouldFetchMoreMessagesTop) {
|
||||
const { messagesProps } = this.props;
|
||||
|
@ -475,11 +480,17 @@ class SessionMessagesListInner extends React.Component<Props> {
|
|||
return;
|
||||
}
|
||||
if (conversation.unreadCount > 0 && messagesProps.length) {
|
||||
// just scroll to the middle of the loaded messages list. so the user can chosse to go up or down from there
|
||||
|
||||
const middle = messagesProps.length / 2;
|
||||
messagesProps[middle].propsForMessage.id;
|
||||
this.scrollToMessage(messagesProps[middle].propsForMessage.id);
|
||||
if (conversation.unreadCount < messagesProps.length) {
|
||||
// if we loaded all unread messages, scroll to the first one unread
|
||||
const firstUnread = Math.max(conversation.unreadCount, 0);
|
||||
messagesProps[firstUnread].propsForMessage.id;
|
||||
this.scrollToMessage(messagesProps[firstUnread].propsForMessage.id);
|
||||
} else {
|
||||
// if we did not load all unread messages, just scroll to the middle of the loaded messages list. so the user can choose to go up or down from there
|
||||
const middle = Math.floor(messagesProps.length / 2);
|
||||
messagesProps[middle].propsForMessage.id;
|
||||
this.scrollToMessage(messagesProps[middle].propsForMessage.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.ignoreScrollEvents && messagesProps.length > 0) {
|
||||
|
@ -530,13 +541,6 @@ class SessionMessagesListInner extends React.Component<Props> {
|
|||
if (!messageContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollHeight = messageContainer.scrollHeight;
|
||||
const clientHeight = messageContainer.clientHeight;
|
||||
|
||||
if (scrollHeight !== 0 && scrollHeight === clientHeight) {
|
||||
this.updateReadMessages();
|
||||
}
|
||||
}
|
||||
|
||||
private scrollToBottom() {
|
||||
|
@ -551,8 +555,10 @@ class SessionMessagesListInner extends React.Component<Props> {
|
|||
return;
|
||||
}
|
||||
|
||||
const conversation = getConversationController().getOrThrow(conversationKey);
|
||||
void conversation.markRead(messagesProps[0].propsForMessage.receivedAt || 0);
|
||||
const conversation = getConversationController().get(conversationKey);
|
||||
if (isElectronWindowFocused()) {
|
||||
void conversation.markRead(messagesProps[0].propsForMessage.receivedAt || 0);
|
||||
}
|
||||
}
|
||||
|
||||
private async scrollToQuoteMessage(options: QuoteClickOptions) {
|
||||
|
@ -623,9 +629,10 @@ const mapStateToProps = (state: StateType) => {
|
|||
return {
|
||||
conversationKey: getSelectedConversationKey(state),
|
||||
conversation: getSelectedConversation(state),
|
||||
messagesProps: getMessagesOfSelectedConversation(state),
|
||||
messagesProps: getSortedMessagesOfSelectedConversation(state),
|
||||
showScrollButton: getShowScrollButton(state),
|
||||
animateQuotedMessageId: getQuotedMessageToAnimate(state),
|
||||
areMoreMessagesBeingFetched: areMoreMessagesBeingFetched(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -184,8 +184,6 @@ export const SessionRightPanelWithDetails = () => {
|
|||
|
||||
if (isShowing && selectedConversation) {
|
||||
void getMediaGalleryProps(selectedConversation.id).then(results => {
|
||||
console.warn('results2', results);
|
||||
|
||||
if (isRunning) {
|
||||
if (!_.isEqual(documents, results.documents)) {
|
||||
setDocuments(results.documents);
|
||||
|
|
|
@ -192,7 +192,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
this.throttledBumpTyping = _.throttle(this.bumpTyping, 300);
|
||||
this.updateLastMessage = _.throttle(this.bouncyUpdateLastMessage.bind(this), 1000, {
|
||||
trailing: true,
|
||||
leading: true,
|
||||
});
|
||||
this.triggerUIRefresh = _.throttle(this.triggerUIRefresh, 1000, {
|
||||
trailing: true,
|
||||
|
@ -212,7 +211,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
this.typingRefreshTimer = null;
|
||||
this.typingPauseTimer = null;
|
||||
this.lastReadTimestamp = 0;
|
||||
|
||||
window.inboxStore?.dispatch(conversationChanged({ id: this.id, data: this.getProps() }));
|
||||
}
|
||||
|
||||
|
|
|
@ -1088,8 +1088,6 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
public async markRead(readAt: number) {
|
||||
this.markReadNoCommit(readAt);
|
||||
|
||||
// this.getConversation()?.markRead(this.attributes.received_at);
|
||||
|
||||
await this.commit();
|
||||
}
|
||||
|
||||
|
|
|
@ -434,15 +434,7 @@ export async function handleMessageJob(
|
|||
const id = await message.commit();
|
||||
|
||||
message.set({ id });
|
||||
// this updates the redux store.
|
||||
// if the convo on which this message should become visible,
|
||||
// it will be shown to the user, and might as well be read right away
|
||||
window.inboxStore?.dispatch(
|
||||
conversationActions.messageAdded({
|
||||
conversationKey: conversation.id,
|
||||
messageModelProps: message.getProps(),
|
||||
})
|
||||
);
|
||||
|
||||
getMessageController().register(message.id, message);
|
||||
|
||||
// Note that this can save the message again, if jobs were queued. We need to
|
||||
|
@ -481,6 +473,16 @@ export async function handleMessageJob(
|
|||
window?.log?.warn('handleDataMessage: Message', message.idForLogging(), 'was deleted');
|
||||
}
|
||||
|
||||
// this updates the redux store.
|
||||
// if the convo on which this message should become visible,
|
||||
// it will be shown to the user, and might as well be read right away
|
||||
window.inboxStore?.dispatch(
|
||||
conversationActions.messageAdded({
|
||||
conversationKey: conversation.id,
|
||||
messageModelProps: message.getProps(),
|
||||
})
|
||||
);
|
||||
|
||||
if (message.get('unread')) {
|
||||
await conversation.throttledNotify(message);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { remote } from 'electron';
|
||||
|
||||
export function isElectronWindowFocused() {
|
||||
const [yourBrowserWindow] = remote.BrowserWindow.getAllWindows();
|
||||
const isFocused = yourBrowserWindow?.isFocused() || false;
|
||||
|
||||
return isFocused;
|
||||
}
|
|
@ -236,7 +236,7 @@ export type ConversationLookupType = {
|
|||
export type ConversationsStateType = {
|
||||
conversationLookup: ConversationLookupType;
|
||||
selectedConversation?: string;
|
||||
messages: Array<SortedMessageModelProps>;
|
||||
messages: Array<MessageModelProps>;
|
||||
messageDetailProps?: MessagePropsDetails;
|
||||
showRightPanel: boolean;
|
||||
selectedMessageIds: Array<string>;
|
||||
|
@ -258,17 +258,15 @@ export type MentionsMembersType = Array<{
|
|||
|
||||
async function getMessages(
|
||||
conversationKey: string,
|
||||
numMessages: number
|
||||
): Promise<Array<SortedMessageModelProps>> {
|
||||
numMessagesToFetch: number
|
||||
): Promise<Array<MessageModelProps>> {
|
||||
const conversation = getConversationController().get(conversationKey);
|
||||
if (!conversation) {
|
||||
// no valid conversation, early return
|
||||
window?.log?.error('Failed to get convo on reducer.');
|
||||
return [];
|
||||
}
|
||||
const unreadCount = await conversation.getUnreadCount();
|
||||
let msgCount =
|
||||
numMessages || Number(Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT) + unreadCount;
|
||||
let msgCount = numMessagesToFetch;
|
||||
msgCount =
|
||||
msgCount > Constants.CONVERSATION.MAX_MESSAGE_FETCH_COUNT
|
||||
? Constants.CONVERSATION.MAX_MESSAGE_FETCH_COUNT
|
||||
|
@ -282,80 +280,17 @@ async function getMessages(
|
|||
limit: msgCount,
|
||||
});
|
||||
|
||||
// Set first member of series here.
|
||||
const messageModelsProps: Array<SortedMessageModelProps> = [];
|
||||
messageSet.models.forEach(m => {
|
||||
messageModelsProps.push({ ...m.getProps(), firstMessageOfSeries: true });
|
||||
});
|
||||
|
||||
const isPublic = conversation.isPublic();
|
||||
|
||||
const sortedMessageProps = sortMessages(messageModelsProps, isPublic);
|
||||
|
||||
// no need to do that `firstMessageOfSeries` on a private chat
|
||||
if (conversation.isPrivate()) {
|
||||
return sortedMessageProps;
|
||||
}
|
||||
return updateFirstMessageOfSeriesAndUnread(sortedMessageProps);
|
||||
const messageProps: Array<MessageModelProps> = messageSet.models.map(m => m.getProps());
|
||||
return messageProps;
|
||||
}
|
||||
|
||||
export type SortedMessageModelProps = MessageModelProps & {
|
||||
firstMessageOfSeries: boolean;
|
||||
firstUnread?: boolean;
|
||||
};
|
||||
|
||||
const updateFirstMessageOfSeriesAndUnread = (
|
||||
messageModelsProps: Array<SortedMessageModelProps>
|
||||
): Array<SortedMessageModelProps> => {
|
||||
// messages are got from the more recent to the oldest, so we need to check if
|
||||
// the next messages in the list is still the same author.
|
||||
// The message is the first of the series if the next message is not from the same author
|
||||
const sortedMessageProps: Array<SortedMessageModelProps> = [];
|
||||
const firstUnreadIndex = getFirstMessageUnreadIndex(messageModelsProps);
|
||||
|
||||
for (let i = 0; i < messageModelsProps.length; i++) {
|
||||
// Handle firstMessageOfSeries for conditional avatar rendering
|
||||
let firstMessageOfSeries = true;
|
||||
let firstUnread = false;
|
||||
const currentSender = messageModelsProps[i].propsForMessage?.authorPhoneNumber;
|
||||
const nextSender =
|
||||
i < messageModelsProps.length - 1
|
||||
? messageModelsProps[i + 1].propsForMessage?.authorPhoneNumber
|
||||
: undefined;
|
||||
if (i >= 0 && currentSender === nextSender) {
|
||||
firstMessageOfSeries = false;
|
||||
}
|
||||
if (i === firstUnreadIndex) {
|
||||
firstUnread = true;
|
||||
}
|
||||
|
||||
sortedMessageProps.push({ ...messageModelsProps[i], firstMessageOfSeries, firstUnread });
|
||||
}
|
||||
return sortedMessageProps;
|
||||
};
|
||||
|
||||
type FetchedMessageResults = {
|
||||
conversationKey: string;
|
||||
messagesProps: Array<SortedMessageModelProps>;
|
||||
};
|
||||
|
||||
const getFirstMessageUnreadIndex = (messages: Array<SortedMessageModelProps>) => {
|
||||
if (!messages || messages.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// iterate over the incoming messages from the oldest one. the first one with isUnread !== undefined is our first unread
|
||||
for (let index = messages.length - 1; index > 0; index--) {
|
||||
const message = messages[index];
|
||||
if (
|
||||
message.propsForMessage.direction === 'incoming' &&
|
||||
message.propsForMessage.isUnread === true
|
||||
) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
messagesProps: Array<MessageModelProps>;
|
||||
};
|
||||
|
||||
export const fetchMessagesForConversation = createAsyncThunk(
|
||||
|
@ -370,30 +305,15 @@ export const fetchMessagesForConversation = createAsyncThunk(
|
|||
const beforeTimestamp = Date.now();
|
||||
console.time('fetchMessagesForConversation');
|
||||
const messagesProps = await getMessages(conversationKey, count);
|
||||
const firstUnreadIndex = getFirstMessageUnreadIndex(messagesProps);
|
||||
const afterTimestamp = Date.now();
|
||||
console.timeEnd('fetchMessagesForConversation');
|
||||
|
||||
const time = afterTimestamp - beforeTimestamp;
|
||||
window?.log?.info(`Loading ${messagesProps.length} messages took ${time}ms to load.`);
|
||||
|
||||
const mapped = messagesProps.map((m, index) => {
|
||||
if (index === firstUnreadIndex) {
|
||||
return {
|
||||
...m,
|
||||
firstMessageOfSeries: true,
|
||||
firstUnread: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...m,
|
||||
firstMessageOfSeries: true,
|
||||
firstUnread: false,
|
||||
};
|
||||
});
|
||||
return {
|
||||
conversationKey,
|
||||
messagesProps: mapped,
|
||||
messagesProps,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -413,32 +333,6 @@ function getEmptyState(): ConversationsStateType {
|
|||
};
|
||||
}
|
||||
|
||||
function sortMessages(
|
||||
messages: Array<SortedMessageModelProps>,
|
||||
isPublic: boolean
|
||||
): Array<SortedMessageModelProps> {
|
||||
// we order by serverTimestamp for public convos
|
||||
// be sure to update the sorting order to fetch messages from the DB too at getMessagesByConversation
|
||||
if (isPublic) {
|
||||
return messages.sort((a, b) => {
|
||||
return (b.propsForMessage.serverTimestamp || 0) - (a.propsForMessage.serverTimestamp || 0);
|
||||
});
|
||||
}
|
||||
if (messages.some(n => !n.propsForMessage.timestamp && !n.propsForMessage.receivedAt)) {
|
||||
throw new Error('Found some messages without any timestamp set');
|
||||
}
|
||||
|
||||
// for non public convos, we order by sent_at or received_at timestamp.
|
||||
// we assume that a message has either a sent_at or a received_at field set.
|
||||
const messagesSorted = messages.sort(
|
||||
(a, b) =>
|
||||
(b.propsForMessage.timestamp || b.propsForMessage.receivedAt || 0) -
|
||||
(a.propsForMessage.timestamp || a.propsForMessage.receivedAt || 0)
|
||||
);
|
||||
|
||||
return messagesSorted;
|
||||
}
|
||||
|
||||
function handleMessageAdded(
|
||||
state: ConversationsStateType,
|
||||
action: PayloadAction<{
|
||||
|
@ -449,32 +343,21 @@ function handleMessageAdded(
|
|||
const { messages } = state;
|
||||
const { conversationKey, messageModelProps: addedMessageProps } = action.payload;
|
||||
if (conversationKey === state.selectedConversation) {
|
||||
const messagesWithNewMessage = [
|
||||
...messages,
|
||||
{ ...addedMessageProps, firstMessageOfSeries: true },
|
||||
];
|
||||
const convo = state.conversationLookup[state.selectedConversation];
|
||||
const isPublic = convo?.isPublic || false;
|
||||
const messagesWithNewMessage = [...messages, addedMessageProps];
|
||||
|
||||
if (convo) {
|
||||
const sortedMessage = sortMessages(messagesWithNewMessage, isPublic);
|
||||
const updatedWithFirstMessageOfSeries = updateFirstMessageOfSeriesAndUnread(sortedMessage);
|
||||
|
||||
return {
|
||||
...state,
|
||||
messages: updatedWithFirstMessageOfSeries,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
messages: messagesWithNewMessage,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function handleMessageChanged(state: ConversationsStateType, payload: MessageModelProps) {
|
||||
function handleMessageChanged(state: ConversationsStateType, changedMessage: MessageModelProps) {
|
||||
const messageInStoreIndex = state?.messages?.findIndex(
|
||||
m => m.propsForMessage.id === payload.propsForMessage.id
|
||||
m => m.propsForMessage.id === changedMessage.propsForMessage.id
|
||||
);
|
||||
if (messageInStoreIndex >= 0) {
|
||||
const changedMessage = { ...payload, firstMessageOfSeries: true };
|
||||
// we cannot edit the array directly, so slice the first part, insert our edited message, and slice the second part
|
||||
const editedMessages = [
|
||||
...state.messages.slice(0, messageInStoreIndex),
|
||||
|
@ -482,15 +365,9 @@ function handleMessageChanged(state: ConversationsStateType, payload: MessageMod
|
|||
...state.messages.slice(messageInStoreIndex + 1),
|
||||
];
|
||||
|
||||
const convo = state.conversationLookup[payload.propsForMessage.convoId];
|
||||
const isPublic = convo?.isPublic || false;
|
||||
// reorder the messages depending on the timestamp (we might have an updated serverTimestamp now)
|
||||
const sortedMessage = sortMessages(editedMessages, isPublic);
|
||||
const updatedWithFirstMessageOfSeries = updateFirstMessageOfSeriesAndUnread(sortedMessage);
|
||||
|
||||
return {
|
||||
...state,
|
||||
messages: updatedWithFirstMessageOfSeries,
|
||||
messages: editedMessages,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -526,15 +403,13 @@ function handleMessageExpiredOrDeleted(
|
|||
...state.messages.slice(messageInStoreIndex + 1),
|
||||
];
|
||||
|
||||
const updatedWithFirstMessageOfSeries = updateFirstMessageOfSeriesAndUnread(editedMessages);
|
||||
|
||||
// FIXME two other thing we have to do:
|
||||
// * update the last message text if the message deleted was the last one
|
||||
// * update the unread count of the convo if the message was the one counted as an unread
|
||||
|
||||
return {
|
||||
...state,
|
||||
messages: updatedWithFirstMessageOfSeries,
|
||||
messages: editedMessages,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
ConversationLookupType,
|
||||
ConversationsStateType,
|
||||
MentionsMembersType,
|
||||
MessageModelProps,
|
||||
MessagePropsDetails,
|
||||
ReduxConversationType,
|
||||
SortedMessageModelProps,
|
||||
|
@ -21,6 +22,7 @@ import {
|
|||
import { LightBoxOptions } from '../../components/session/conversation/SessionConversation';
|
||||
import { ReplyingToMessageProps } from '../../components/session/conversation/SessionCompositionBox';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { getConversationController } from '../../session/conversations';
|
||||
|
||||
export const getConversations = (state: StateType): ConversationsStateType => state.conversations;
|
||||
|
||||
|
@ -53,9 +55,32 @@ export const getOurPrimaryConversation = createSelector(
|
|||
state.conversationLookup[window.storage.get('primaryDevicePubKey')]
|
||||
);
|
||||
|
||||
export const getMessagesOfSelectedConversation = createSelector(
|
||||
const getMessagesOfSelectedConversation = createSelector(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): Array<SortedMessageModelProps> => state.messages
|
||||
(state: ConversationsStateType): Array<MessageModelProps> => state.messages
|
||||
);
|
||||
|
||||
// Redux recommends to do filtered and deriving state in a selector rather than ourself
|
||||
export const getSortedMessagesOfSelectedConversation = createSelector(
|
||||
getMessagesOfSelectedConversation,
|
||||
(messages: Array<MessageModelProps>): Array<SortedMessageModelProps> => {
|
||||
if (messages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const convoId = messages[0].propsForMessage.convoId;
|
||||
const convo = getConversationController().get(convoId);
|
||||
|
||||
if (!convo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isPublic = convo.isPublic() || false;
|
||||
const isPrivate = convo.isPrivate() || false;
|
||||
const sortedMessage = sortMessages(messages, isPublic);
|
||||
|
||||
return updateFirstMessageOfSeries(sortedMessage, { isPublic, isPrivate });
|
||||
}
|
||||
);
|
||||
|
||||
function getConversationTitle(
|
||||
|
@ -314,3 +339,102 @@ export const getMentionsInput = createSelector(
|
|||
getConversations,
|
||||
(state: ConversationsStateType): MentionsMembersType => state.mentionMembers
|
||||
);
|
||||
|
||||
/// Those calls are just related to ordering messages in the redux store.
|
||||
|
||||
function updateFirstMessageOfSeries(
|
||||
messageModelsProps: Array<MessageModelProps>,
|
||||
convoOpts: { isPrivate: boolean; isPublic: boolean }
|
||||
): Array<SortedMessageModelProps> {
|
||||
// messages are got from the more recent to the oldest, so we need to check if
|
||||
// the next messages in the list is still the same author.
|
||||
// The message is the first of the series if the next message is not from the same author
|
||||
const sortedMessageProps: Array<SortedMessageModelProps> = [];
|
||||
|
||||
if (convoOpts.isPrivate) {
|
||||
// we don't really care do do that logic for private chats
|
||||
return messageModelsProps.map(p => {
|
||||
return { ...p, firstMessageOfSeries: true };
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < messageModelsProps.length; i++) {
|
||||
const currentSender = messageModelsProps[i].propsForMessage?.authorPhoneNumber;
|
||||
const nextSender =
|
||||
i < messageModelsProps.length - 1
|
||||
? messageModelsProps[i + 1].propsForMessage?.authorPhoneNumber
|
||||
: undefined;
|
||||
|
||||
// Handle firstMessageOfSeries for conditional avatar rendering
|
||||
|
||||
if (i >= 0 && currentSender === nextSender) {
|
||||
sortedMessageProps.push({ ...messageModelsProps[i], firstMessageOfSeries: false });
|
||||
} else {
|
||||
sortedMessageProps.push({ ...messageModelsProps[i], firstMessageOfSeries: true });
|
||||
}
|
||||
}
|
||||
return sortedMessageProps;
|
||||
}
|
||||
|
||||
function sortMessages(
|
||||
messages: Array<MessageModelProps>,
|
||||
isPublic: boolean
|
||||
): Array<MessageModelProps> {
|
||||
// we order by serverTimestamp for public convos
|
||||
// be sure to update the sorting order to fetch messages from the DB too at getMessagesByConversation
|
||||
if (isPublic) {
|
||||
return messages.slice().sort((a, b) => {
|
||||
return (b.propsForMessage.serverTimestamp || 0) - (a.propsForMessage.serverTimestamp || 0);
|
||||
});
|
||||
}
|
||||
if (messages.some(n => !n.propsForMessage.timestamp && !n.propsForMessage.receivedAt)) {
|
||||
throw new Error('Found some messages without any timestamp set');
|
||||
}
|
||||
|
||||
// for non public convos, we order by sent_at or received_at timestamp.
|
||||
// we assume that a message has either a sent_at or a received_at field set.
|
||||
const messagesSorted = messages.sort(
|
||||
(a, b) =>
|
||||
(b.propsForMessage.timestamp || b.propsForMessage.receivedAt || 0) -
|
||||
(a.propsForMessage.timestamp || a.propsForMessage.receivedAt || 0)
|
||||
);
|
||||
|
||||
return messagesSorted;
|
||||
}
|
||||
|
||||
export const getFirstUnreadMessageIndex = createSelector(
|
||||
getSortedMessagesOfSelectedConversation,
|
||||
(messageModelsProps: Array<MessageModelProps>): number | undefined => {
|
||||
const firstUnreadIndex = getFirstMessageUnreadIndex(messageModelsProps);
|
||||
return firstUnreadIndex;
|
||||
}
|
||||
);
|
||||
|
||||
function getFirstMessageUnreadIndex(messages: Array<MessageModelProps>) {
|
||||
if (!messages || messages.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// this is to handle the case where 50 messages are loaded, some of them are already read at the top, but some not loaded yet are still unread.
|
||||
if (
|
||||
messages.length <
|
||||
getConversationController()
|
||||
.get(messages[0].propsForMessage.convoId)
|
||||
?.get('unreadCount')
|
||||
) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
// iterate over the incoming messages from the oldest one. the first one with isUnread !== undefined is our first unread
|
||||
for (let index = messages.length - 1; index > 0; index--) {
|
||||
const message = messages[index];
|
||||
if (
|
||||
message.propsForMessage.direction === 'incoming' &&
|
||||
message.propsForMessage.isUnread === true
|
||||
) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { StateType } from '../reducer';
|
|||
import { getTheme } from '../selectors/theme';
|
||||
import {
|
||||
getLightBoxOptions,
|
||||
getMessagesOfSelectedConversation,
|
||||
getSortedMessagesOfSelectedConversation,
|
||||
getSelectedConversation,
|
||||
getSelectedConversationKey,
|
||||
getSelectedMessageIds,
|
||||
|
@ -19,7 +19,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
selectedConversation: getSelectedConversation(state),
|
||||
selectedConversationKey: getSelectedConversationKey(state),
|
||||
theme: getTheme(state),
|
||||
messagesProps: getMessagesOfSelectedConversation(state),
|
||||
messagesProps: getSortedMessagesOfSelectedConversation(state),
|
||||
ourNumber: getOurNumber(state),
|
||||
showMessageDetails: isMessageDetailView(state),
|
||||
isRightPanelShowing: isRightPanelShowing(state),
|
||||
|
|
|
@ -41,7 +41,6 @@ declare global {
|
|||
getFriendsFromContacts: any;
|
||||
getSettingValue: any;
|
||||
i18n: LocalizerType;
|
||||
isFocused: () => boolean;
|
||||
libsignal: LibsignalProtocol;
|
||||
log: any;
|
||||
lokiFeatureFlags: {
|
||||
|
|
Loading…
Reference in New Issue