use selector to sort and add first of serie flag

This commit is contained in:
audric 2021-07-21 17:14:14 +10:00
parent 6d34a60f94
commit e72885944b
16 changed files with 221 additions and 205 deletions

View File

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

View File

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

View File

@ -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 || ''}

View File

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

View File

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

View File

@ -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} />;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
import { remote } from 'electron';
export function isElectronWindowFocused() {
const [yourBrowserWindow] = remote.BrowserWindow.getAllWindows();
const isFocused = yourBrowserWindow?.isFocused() || false;
return isFocused;
}

View File

@ -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,
};
}

View File

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

View File

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

1
ts/window.d.ts vendored
View File

@ -41,7 +41,6 @@ declare global {
getFriendsFromContacts: any;
getSettingValue: any;
i18n: LocalizerType;
isFocused: () => boolean;
libsignal: LibsignalProtocol;
log: any;
lokiFeatureFlags: {