don't rely on scrollheight to calculate for ui Updates
as it causes a layout complete refresh
This commit is contained in:
parent
e72885944b
commit
c2b5ac68d6
23
app/sql.js
23
app/sql.js
|
@ -74,6 +74,7 @@ module.exports = {
|
|||
getOutgoingWithoutExpiresAt,
|
||||
getNextExpiringMessage,
|
||||
getMessagesByConversation,
|
||||
getFirstUnreadMessageIdInConversation,
|
||||
|
||||
getUnprocessedCount,
|
||||
getAllUnprocessed,
|
||||
|
@ -2113,6 +2114,28 @@ function getMessagesByConversation(
|
|||
return map(rows, row => jsonToObject(row.json));
|
||||
}
|
||||
|
||||
function getFirstUnreadMessageIdInConversation(conversationId) {
|
||||
const rows = globalInstance
|
||||
.prepare(
|
||||
`
|
||||
SELECT id FROM ${MESSAGES_TABLE} WHERE
|
||||
conversationId = $conversationId AND
|
||||
unread = $unread
|
||||
ORDER BY serverTimestamp ASC, serverId ASC, sent_at ASC, received_at ASC
|
||||
LIMIT 1;
|
||||
`
|
||||
)
|
||||
.all({
|
||||
conversationId,
|
||||
unread: 1,
|
||||
});
|
||||
|
||||
if (rows.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return rows[0].id;
|
||||
}
|
||||
|
||||
function getMessagesBySentAt(sentAt) {
|
||||
const rows = globalInstance
|
||||
.prepare(
|
||||
|
|
|
@ -112,6 +112,7 @@ export class SessionInboxView extends React.Component<any, State> {
|
|||
nextMessageToPlay: undefined,
|
||||
quotedMessage: undefined,
|
||||
mentionMembers: [],
|
||||
firstUnreadMessageId: undefined,
|
||||
},
|
||||
user: {
|
||||
ourNumber: UserUtils.getOurPubKeyStrFromCache(),
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
PropsForDataExtractionNotification,
|
||||
QuoteClickOptions,
|
||||
} from '../../../models/messageType';
|
||||
import { getMessagesBySentAt } from '../../../data/data';
|
||||
import { getFirstUnreadMessageIdInConversation, getMessagesBySentAt } from '../../../data/data';
|
||||
import autoBind from 'auto-bind';
|
||||
import { ConversationTypeEnum } from '../../../models/conversation';
|
||||
import { DataExtractionNotification } from '../../conversation/DataExtractionNotification';
|
||||
|
@ -46,8 +46,10 @@ import {
|
|||
isMessageSelectionMode,
|
||||
getFirstUnreadMessageIndex,
|
||||
areMoreMessagesBeingFetched,
|
||||
isFirstUnreadMessageIdAbove,
|
||||
} from '../../../state/selectors/conversations';
|
||||
import { isElectronWindowFocused } from '../../../session/utils/WindowUtils';
|
||||
import useInterval from 'react-use/lib/useInterval';
|
||||
|
||||
export type SessionMessageListProps = {
|
||||
messageContainerRef: React.RefObject<any>;
|
||||
|
@ -175,6 +177,9 @@ const MessageList = (props: {
|
|||
}) => {
|
||||
const messagesProps = useSelector(getSortedMessagesOfSelectedConversation);
|
||||
const firstUnreadMessageIndex = useSelector(getFirstUnreadMessageIndex);
|
||||
const isAbove = useSelector(isFirstUnreadMessageIdAbove);
|
||||
|
||||
console.warn('isAbove', isAbove);
|
||||
let playableMessageIndex = 0;
|
||||
|
||||
return (
|
||||
|
@ -295,7 +300,6 @@ class SessionMessagesListInner extends React.Component<Props> {
|
|||
(prevProps.messagesProps.length === 0 && this.props.messagesProps.length !== 0)
|
||||
) {
|
||||
// displayed conversation changed. We have a bit of cleaning to do here
|
||||
this.scrollOffsetBottomPx = Number.MAX_VALUE;
|
||||
this.ignoreScrollEvents = true;
|
||||
this.setupTimeoutResetQuotedHighlightedMessage(true);
|
||||
this.initialMessageLoadingPosition();
|
||||
|
@ -305,16 +309,6 @@ class SessionMessagesListInner extends React.Component<Props> {
|
|||
// Keep scrolled to bottom unless user scrolls up
|
||||
if (this.getScrollOffsetBottomPx() === 0) {
|
||||
this.scrollToBottom();
|
||||
} else {
|
||||
const messageContainer = this.props.messageContainerRef?.current;
|
||||
|
||||
if (messageContainer) {
|
||||
const scrollHeight = messageContainer.scrollHeight;
|
||||
const clientHeight = messageContainer.clientHeight;
|
||||
this.ignoreScrollEvents = true;
|
||||
messageContainer.scrollTop = scrollHeight - clientHeight - this.scrollOffsetBottomPx;
|
||||
this.ignoreScrollEvents = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -425,48 +419,68 @@ class SessionMessagesListInner extends React.Component<Props> {
|
|||
if (this.ignoreScrollEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollTop = messageContainer.scrollTop;
|
||||
const clientHeight = messageContainer.clientHeight;
|
||||
|
||||
const scrollButtonViewShowLimit = 0.75;
|
||||
const scrollButtonViewHideLimit = 0.4;
|
||||
this.scrollOffsetBottomPx = this.getScrollOffsetBottomPx();
|
||||
|
||||
const scrollOffsetPc = this.scrollOffsetBottomPx / clientHeight;
|
||||
|
||||
// Scroll button appears if you're more than 75% scrolled up
|
||||
if (scrollOffsetPc > scrollButtonViewShowLimit && !this.props.showScrollButton) {
|
||||
window.inboxStore?.dispatch(showScrollToBottomButton(true));
|
||||
}
|
||||
// Scroll button disappears if you're more less than 40% scrolled up
|
||||
if (scrollOffsetPc < scrollButtonViewHideLimit && this.props.showScrollButton) {
|
||||
window.inboxStore?.dispatch(showScrollToBottomButton(false));
|
||||
// nothing to do if there are no message loaded
|
||||
if (!this.props.messagesProps || this.props.messagesProps.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scrolled to bottom
|
||||
const isScrolledToBottom = this.getScrollOffsetBottomPx() === 0;
|
||||
if (isScrolledToBottom) {
|
||||
// Mark messages read
|
||||
this.updateReadMessages();
|
||||
// ---- First lets see if we need to show the scroll to bottom button, without using clientHeight (which generates a full layout recalculation)
|
||||
// get the message the most at the bottom
|
||||
const bottomMessageId = this.props.messagesProps[0].propsForMessage.id;
|
||||
const bottomMessageDomElement = document.getElementById(bottomMessageId);
|
||||
|
||||
// get the message the most at the top
|
||||
const topMessageId = this.props.messagesProps[this.props.messagesProps.length - 1]
|
||||
.propsForMessage.id;
|
||||
const topMessageDomElement = document.getElementById(topMessageId);
|
||||
|
||||
const containerTop = messageContainer.getBoundingClientRect().top;
|
||||
const containerBottom = messageContainer.getBoundingClientRect().bottom;
|
||||
|
||||
// First handle what we gotta handle with the bottom message position
|
||||
// either the showScrollButton or the markRead of all messages
|
||||
if (!bottomMessageDomElement) {
|
||||
window.log.warn('Could not find dom element for handle scroll');
|
||||
} else {
|
||||
const topOfBottomMessage = bottomMessageDomElement.getBoundingClientRect().top;
|
||||
const bottomOfBottomMessage = bottomMessageDomElement.getBoundingClientRect().bottom;
|
||||
|
||||
// this is our limit for the showScrollDownButton.
|
||||
const showScrollButton = topOfBottomMessage > window.innerHeight;
|
||||
window.inboxStore?.dispatch(showScrollToBottomButton(showScrollButton));
|
||||
|
||||
// trigger markRead if we hit the bottom
|
||||
const isScrolledToBottom = bottomOfBottomMessage >= containerBottom - 5;
|
||||
if (isScrolledToBottom) {
|
||||
// Mark messages read
|
||||
this.updateReadMessages();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch more messages when nearing the top of the message list
|
||||
const shouldFetchMoreMessagesTop =
|
||||
scrollTop <= Constants.UI.MESSAGE_CONTAINER_BUFFER_OFFSET_PX &&
|
||||
!this.props.areMoreMessagesBeingFetched;
|
||||
// Then, see if we need to fetch more messages because the top message it
|
||||
|
||||
if (shouldFetchMoreMessagesTop) {
|
||||
const { messagesProps } = this.props;
|
||||
const numMessages = messagesProps.length + Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT;
|
||||
const oldLen = messagesProps.length;
|
||||
const previousTopMessage = messagesProps[oldLen - 1]?.propsForMessage.id;
|
||||
if (!topMessageDomElement) {
|
||||
window.log.warn('Could not find dom top element for handle scroll');
|
||||
} else {
|
||||
const topTopMessage = topMessageDomElement.getBoundingClientRect().top;
|
||||
|
||||
(window.inboxStore?.dispatch as any)(
|
||||
fetchMessagesForConversation({ conversationKey, count: numMessages })
|
||||
);
|
||||
if (previousTopMessage && oldLen !== messagesProps.length) {
|
||||
this.scrollToMessage(previousTopMessage);
|
||||
// this is our limit for the showScrollDownButton.
|
||||
const shouldFetchMore =
|
||||
topTopMessage > containerTop - 10 && !this.props.areMoreMessagesBeingFetched;
|
||||
|
||||
if (shouldFetchMore) {
|
||||
const { messagesProps } = this.props;
|
||||
const numMessages =
|
||||
messagesProps.length + Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT;
|
||||
const oldLen = messagesProps.length;
|
||||
const previousTopMessage = messagesProps[oldLen - 1]?.propsForMessage.id;
|
||||
|
||||
(window.inboxStore?.dispatch as any)(
|
||||
fetchMessagesForConversation({ conversationKey, count: numMessages })
|
||||
);
|
||||
if (previousTopMessage && oldLen !== messagesProps.length) {
|
||||
this.scrollToMessage(previousTopMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,6 +123,7 @@ const channelsToMake = {
|
|||
getOutgoingWithoutExpiresAt,
|
||||
getNextExpiringMessage,
|
||||
getMessagesByConversation,
|
||||
getFirstUnreadMessageIdInConversation,
|
||||
getSeenMessagesByHashList,
|
||||
getLastHashBySnode,
|
||||
|
||||
|
@ -753,6 +754,12 @@ export async function getMessagesByConversation(
|
|||
return new MessageCollection(messages);
|
||||
}
|
||||
|
||||
export async function getFirstUnreadMessageIdInConversation(
|
||||
conversationId: string
|
||||
): Promise<string | undefined> {
|
||||
return channels.getFirstUnreadMessageIdInConversation(conversationId);
|
||||
}
|
||||
|
||||
export async function getLastHashBySnode(convoId: string, snode: string): Promise<string> {
|
||||
return channels.getLastHashBySnode(convoId, snode);
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
void this.setToExpire();
|
||||
autoBind(this);
|
||||
|
||||
this.dispatchMessageUpdate = _.debounce(this.dispatchMessageUpdate, 500);
|
||||
this.dispatchMessageUpdate = _.throttle(this.dispatchMessageUpdate, 300);
|
||||
|
||||
window.contextMenuShown = false;
|
||||
|
||||
|
@ -1093,6 +1093,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
|
||||
public markReadNoCommit(readAt: number) {
|
||||
this.set({ unread: 0 });
|
||||
console.warn('markReadNoCommit', this.id);
|
||||
|
||||
if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) {
|
||||
const expirationStartTimestamp = Math.min(Date.now(), readAt || Date.now());
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
export function perfStart(prefix: string) {
|
||||
performance.mark(`${prefix}-start`);
|
||||
if (typeof performance !== 'undefined') {
|
||||
performance?.mark(`${prefix}-start`);
|
||||
}
|
||||
}
|
||||
|
||||
export function perfEnd(prefix: string, measureName: string) {
|
||||
performance.mark(`${prefix}-end`);
|
||||
performance.measure(measureName, `${prefix}-start`, `${prefix}-end`);
|
||||
if (typeof performance !== 'undefined') {
|
||||
performance?.mark(`${prefix}-end`);
|
||||
performance?.measure(measureName, `${prefix}-start`, `${prefix}-end`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import _, { omit } from 'lodash';
|
|||
import { Constants } from '../../session';
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { getConversationController } from '../../session/conversations';
|
||||
import { getMessagesByConversation } from '../../data/data';
|
||||
import { getFirstUnreadMessageIdInConversation, getMessagesByConversation } from '../../data/data';
|
||||
import {
|
||||
ConversationNotificationSettingType,
|
||||
ConversationTypeEnum,
|
||||
|
@ -237,6 +237,7 @@ export type ConversationsStateType = {
|
|||
conversationLookup: ConversationLookupType;
|
||||
selectedConversation?: string;
|
||||
messages: Array<MessageModelProps>;
|
||||
firstUnreadMessageId: string | undefined;
|
||||
messageDetailProps?: MessagePropsDetails;
|
||||
showRightPanel: boolean;
|
||||
selectedMessageIds: Array<string>;
|
||||
|
@ -291,6 +292,7 @@ export type SortedMessageModelProps = MessageModelProps & {
|
|||
type FetchedMessageResults = {
|
||||
conversationKey: string;
|
||||
messagesProps: Array<MessageModelProps>;
|
||||
firstUnreadMessageId: string | undefined;
|
||||
};
|
||||
|
||||
export const fetchMessagesForConversation = createAsyncThunk(
|
||||
|
@ -305,6 +307,8 @@ export const fetchMessagesForConversation = createAsyncThunk(
|
|||
const beforeTimestamp = Date.now();
|
||||
console.time('fetchMessagesForConversation');
|
||||
const messagesProps = await getMessages(conversationKey, count);
|
||||
|
||||
const firstUnreadMessageId = await getFirstUnreadMessageIdInConversation(conversationKey);
|
||||
const afterTimestamp = Date.now();
|
||||
console.timeEnd('fetchMessagesForConversation');
|
||||
|
||||
|
@ -314,6 +318,7 @@ export const fetchMessagesForConversation = createAsyncThunk(
|
|||
return {
|
||||
conversationKey,
|
||||
messagesProps,
|
||||
firstUnreadMessageId,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -330,6 +335,7 @@ function getEmptyState(): ConversationsStateType {
|
|||
areMoreMessagesBeingFetched: false,
|
||||
showScrollButton: false,
|
||||
mentionMembers: [],
|
||||
firstUnreadMessageId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -609,6 +615,7 @@ const conversationsSlice = createSlice({
|
|||
showScrollButton: false,
|
||||
animateQuotedMessageId: undefined,
|
||||
mentionMembers: [],
|
||||
firstUnreadMessageId: undefined,
|
||||
};
|
||||
},
|
||||
showLightBox(
|
||||
|
@ -653,15 +660,16 @@ const conversationsSlice = createSlice({
|
|||
// Add reducers for additional action types here, and handle loading state as needed
|
||||
builder.addCase(
|
||||
fetchMessagesForConversation.fulfilled,
|
||||
(state: ConversationsStateType, action: any) => {
|
||||
(state: ConversationsStateType, action: PayloadAction<FetchedMessageResults>) => {
|
||||
// this is called once the messages are loaded from the db for the currently selected conversation
|
||||
const { messagesProps, conversationKey } = action.payload as FetchedMessageResults;
|
||||
const { messagesProps, conversationKey, firstUnreadMessageId } = action.payload;
|
||||
// double check that this update is for the shown convo
|
||||
if (conversationKey === state.selectedConversation) {
|
||||
return {
|
||||
...state,
|
||||
messages: messagesProps,
|
||||
areMoreMessagesBeingFetched: false,
|
||||
firstUnreadMessageId,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
|
|
|
@ -438,3 +438,26 @@ function getFirstMessageUnreadIndex(messages: Array<MessageModelProps>) {
|
|||
|
||||
return -1;
|
||||
}
|
||||
|
||||
export const getFirstUnreadMessageId = createSelector(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): string | undefined => {
|
||||
console.warn('getFirstUnreadMessageId', state.firstUnreadMessageId);
|
||||
return state.firstUnreadMessageId;
|
||||
}
|
||||
);
|
||||
|
||||
export const isFirstUnreadMessageIdAbove = createSelector(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): boolean => {
|
||||
if (!state.firstUnreadMessageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isNotPresent = !state.messages.some(
|
||||
m => m.propsForMessage.id === state.firstUnreadMessageId
|
||||
);
|
||||
|
||||
return isNotPresent;
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue