don't rely on scrollheight to calculate for ui Updates

as it causes a layout complete refresh
This commit is contained in:
audric 2021-07-22 10:20:09 +10:00
parent e72885944b
commit c2b5ac68d6
8 changed files with 136 additions and 55 deletions

View File

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

View File

@ -112,6 +112,7 @@ export class SessionInboxView extends React.Component<any, State> {
nextMessageToPlay: undefined,
quotedMessage: undefined,
mentionMembers: [],
firstUnreadMessageId: undefined,
},
user: {
ourNumber: UserUtils.getOurPubKeyStrFromCache(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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