don't rely on scrollheight to calculate for ui Updates

as it causes a layout complete refresh
pull/1753/head
audric 4 years ago
parent e72885944b
commit c2b5ac68d6

@ -74,6 +74,7 @@ module.exports = {
getOutgoingWithoutExpiresAt, getOutgoingWithoutExpiresAt,
getNextExpiringMessage, getNextExpiringMessage,
getMessagesByConversation, getMessagesByConversation,
getFirstUnreadMessageIdInConversation,
getUnprocessedCount, getUnprocessedCount,
getAllUnprocessed, getAllUnprocessed,
@ -2113,6 +2114,28 @@ function getMessagesByConversation(
return map(rows, row => jsonToObject(row.json)); 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) { function getMessagesBySentAt(sentAt) {
const rows = globalInstance const rows = globalInstance
.prepare( .prepare(

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

@ -30,7 +30,7 @@ import {
PropsForDataExtractionNotification, PropsForDataExtractionNotification,
QuoteClickOptions, QuoteClickOptions,
} from '../../../models/messageType'; } from '../../../models/messageType';
import { getMessagesBySentAt } from '../../../data/data'; import { getFirstUnreadMessageIdInConversation, getMessagesBySentAt } from '../../../data/data';
import autoBind from 'auto-bind'; import autoBind from 'auto-bind';
import { ConversationTypeEnum } from '../../../models/conversation'; import { ConversationTypeEnum } from '../../../models/conversation';
import { DataExtractionNotification } from '../../conversation/DataExtractionNotification'; import { DataExtractionNotification } from '../../conversation/DataExtractionNotification';
@ -46,8 +46,10 @@ import {
isMessageSelectionMode, isMessageSelectionMode,
getFirstUnreadMessageIndex, getFirstUnreadMessageIndex,
areMoreMessagesBeingFetched, areMoreMessagesBeingFetched,
isFirstUnreadMessageIdAbove,
} from '../../../state/selectors/conversations'; } from '../../../state/selectors/conversations';
import { isElectronWindowFocused } from '../../../session/utils/WindowUtils'; import { isElectronWindowFocused } from '../../../session/utils/WindowUtils';
import useInterval from 'react-use/lib/useInterval';
export type SessionMessageListProps = { export type SessionMessageListProps = {
messageContainerRef: React.RefObject<any>; messageContainerRef: React.RefObject<any>;
@ -175,6 +177,9 @@ const MessageList = (props: {
}) => { }) => {
const messagesProps = useSelector(getSortedMessagesOfSelectedConversation); const messagesProps = useSelector(getSortedMessagesOfSelectedConversation);
const firstUnreadMessageIndex = useSelector(getFirstUnreadMessageIndex); const firstUnreadMessageIndex = useSelector(getFirstUnreadMessageIndex);
const isAbove = useSelector(isFirstUnreadMessageIdAbove);
console.warn('isAbove', isAbove);
let playableMessageIndex = 0; let playableMessageIndex = 0;
return ( return (
@ -295,7 +300,6 @@ class SessionMessagesListInner extends React.Component<Props> {
(prevProps.messagesProps.length === 0 && this.props.messagesProps.length !== 0) (prevProps.messagesProps.length === 0 && this.props.messagesProps.length !== 0)
) { ) {
// 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.scrollOffsetBottomPx = Number.MAX_VALUE;
this.ignoreScrollEvents = true; this.ignoreScrollEvents = true;
this.setupTimeoutResetQuotedHighlightedMessage(true); this.setupTimeoutResetQuotedHighlightedMessage(true);
this.initialMessageLoadingPosition(); this.initialMessageLoadingPosition();
@ -305,16 +309,6 @@ class SessionMessagesListInner extends React.Component<Props> {
// Keep scrolled to bottom unless user scrolls up // Keep scrolled to bottom unless user scrolls up
if (this.getScrollOffsetBottomPx() === 0) { if (this.getScrollOffsetBottomPx() === 0) {
this.scrollToBottom(); 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) { if (this.ignoreScrollEvents) {
return; return;
} }
// nothing to do if there are no message loaded
if (!this.props.messagesProps || this.props.messagesProps.length === 0) {
return;
}
const scrollTop = messageContainer.scrollTop; // ---- First lets see if we need to show the scroll to bottom button, without using clientHeight (which generates a full layout recalculation)
const clientHeight = messageContainer.clientHeight; // get the message the most at the bottom
const bottomMessageId = this.props.messagesProps[0].propsForMessage.id;
const bottomMessageDomElement = document.getElementById(bottomMessageId);
const scrollButtonViewShowLimit = 0.75; // get the message the most at the top
const scrollButtonViewHideLimit = 0.4; const topMessageId = this.props.messagesProps[this.props.messagesProps.length - 1]
this.scrollOffsetBottomPx = this.getScrollOffsetBottomPx(); .propsForMessage.id;
const topMessageDomElement = document.getElementById(topMessageId);
const scrollOffsetPc = this.scrollOffsetBottomPx / clientHeight; const containerTop = messageContainer.getBoundingClientRect().top;
const containerBottom = messageContainer.getBoundingClientRect().bottom;
// Scroll button appears if you're more than 75% scrolled up // First handle what we gotta handle with the bottom message position
if (scrollOffsetPc > scrollButtonViewShowLimit && !this.props.showScrollButton) { // either the showScrollButton or the markRead of all messages
window.inboxStore?.dispatch(showScrollToBottomButton(true)); if (!bottomMessageDomElement) {
} window.log.warn('Could not find dom element for handle scroll');
// Scroll button disappears if you're more less than 40% scrolled up } else {
if (scrollOffsetPc < scrollButtonViewHideLimit && this.props.showScrollButton) { const topOfBottomMessage = bottomMessageDomElement.getBoundingClientRect().top;
window.inboxStore?.dispatch(showScrollToBottomButton(false)); 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();
}
} }
// Scrolled to bottom // Then, see if we need to fetch more messages because the top message it
const isScrolledToBottom = this.getScrollOffsetBottomPx() === 0;
if (isScrolledToBottom) {
// Mark messages read
this.updateReadMessages();
}
// Fetch more messages when nearing the top of the message list if (!topMessageDomElement) {
const shouldFetchMoreMessagesTop = window.log.warn('Could not find dom top element for handle scroll');
scrollTop <= Constants.UI.MESSAGE_CONTAINER_BUFFER_OFFSET_PX && } else {
!this.props.areMoreMessagesBeingFetched; const topTopMessage = topMessageDomElement.getBoundingClientRect().top;
if (shouldFetchMoreMessagesTop) { // this is our limit for the showScrollDownButton.
const { messagesProps } = this.props; const shouldFetchMore =
const numMessages = messagesProps.length + Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT; topTopMessage > containerTop - 10 && !this.props.areMoreMessagesBeingFetched;
const oldLen = messagesProps.length;
const previousTopMessage = messagesProps[oldLen - 1]?.propsForMessage.id;
(window.inboxStore?.dispatch as any)( if (shouldFetchMore) {
fetchMessagesForConversation({ conversationKey, count: numMessages }) const { messagesProps } = this.props;
); const numMessages =
if (previousTopMessage && oldLen !== messagesProps.length) { messagesProps.length + Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT;
this.scrollToMessage(previousTopMessage); 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, getOutgoingWithoutExpiresAt,
getNextExpiringMessage, getNextExpiringMessage,
getMessagesByConversation, getMessagesByConversation,
getFirstUnreadMessageIdInConversation,
getSeenMessagesByHashList, getSeenMessagesByHashList,
getLastHashBySnode, getLastHashBySnode,
@ -753,6 +754,12 @@ export async function getMessagesByConversation(
return new MessageCollection(messages); 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> { export async function getLastHashBySnode(convoId: string, snode: string): Promise<string> {
return channels.getLastHashBySnode(convoId, snode); return channels.getLastHashBySnode(convoId, snode);
} }

@ -79,7 +79,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
void this.setToExpire(); void this.setToExpire();
autoBind(this); autoBind(this);
this.dispatchMessageUpdate = _.debounce(this.dispatchMessageUpdate, 500); this.dispatchMessageUpdate = _.throttle(this.dispatchMessageUpdate, 300);
window.contextMenuShown = false; window.contextMenuShown = false;
@ -1093,6 +1093,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
public markReadNoCommit(readAt: number) { public markReadNoCommit(readAt: number) {
this.set({ unread: 0 }); this.set({ unread: 0 });
console.warn('markReadNoCommit', this.id);
if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) { if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) {
const expirationStartTimestamp = Math.min(Date.now(), readAt || Date.now()); const expirationStartTimestamp = Math.min(Date.now(), readAt || Date.now());

@ -1,8 +1,12 @@
export function perfStart(prefix: string) { export function perfStart(prefix: string) {
performance.mark(`${prefix}-start`); if (typeof performance !== 'undefined') {
performance?.mark(`${prefix}-start`);
}
} }
export function perfEnd(prefix: string, measureName: string) { export function perfEnd(prefix: string, measureName: string) {
performance.mark(`${prefix}-end`); if (typeof performance !== 'undefined') {
performance.measure(measureName, `${prefix}-start`, `${prefix}-end`); performance?.mark(`${prefix}-end`);
performance?.measure(measureName, `${prefix}-start`, `${prefix}-end`);
}
} }

@ -3,7 +3,7 @@ import _, { omit } from 'lodash';
import { Constants } from '../../session'; import { Constants } from '../../session';
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { getConversationController } from '../../session/conversations'; import { getConversationController } from '../../session/conversations';
import { getMessagesByConversation } from '../../data/data'; import { getFirstUnreadMessageIdInConversation, getMessagesByConversation } from '../../data/data';
import { import {
ConversationNotificationSettingType, ConversationNotificationSettingType,
ConversationTypeEnum, ConversationTypeEnum,
@ -237,6 +237,7 @@ export type ConversationsStateType = {
conversationLookup: ConversationLookupType; conversationLookup: ConversationLookupType;
selectedConversation?: string; selectedConversation?: string;
messages: Array<MessageModelProps>; messages: Array<MessageModelProps>;
firstUnreadMessageId: string | undefined;
messageDetailProps?: MessagePropsDetails; messageDetailProps?: MessagePropsDetails;
showRightPanel: boolean; showRightPanel: boolean;
selectedMessageIds: Array<string>; selectedMessageIds: Array<string>;
@ -291,6 +292,7 @@ export type SortedMessageModelProps = MessageModelProps & {
type FetchedMessageResults = { type FetchedMessageResults = {
conversationKey: string; conversationKey: string;
messagesProps: Array<MessageModelProps>; messagesProps: Array<MessageModelProps>;
firstUnreadMessageId: string | undefined;
}; };
export const fetchMessagesForConversation = createAsyncThunk( export const fetchMessagesForConversation = createAsyncThunk(
@ -305,6 +307,8 @@ export const fetchMessagesForConversation = createAsyncThunk(
const beforeTimestamp = Date.now(); const beforeTimestamp = Date.now();
console.time('fetchMessagesForConversation'); console.time('fetchMessagesForConversation');
const messagesProps = await getMessages(conversationKey, count); const messagesProps = await getMessages(conversationKey, count);
const firstUnreadMessageId = await getFirstUnreadMessageIdInConversation(conversationKey);
const afterTimestamp = Date.now(); const afterTimestamp = Date.now();
console.timeEnd('fetchMessagesForConversation'); console.timeEnd('fetchMessagesForConversation');
@ -314,6 +318,7 @@ export const fetchMessagesForConversation = createAsyncThunk(
return { return {
conversationKey, conversationKey,
messagesProps, messagesProps,
firstUnreadMessageId,
}; };
} }
); );
@ -330,6 +335,7 @@ function getEmptyState(): ConversationsStateType {
areMoreMessagesBeingFetched: false, areMoreMessagesBeingFetched: false,
showScrollButton: false, showScrollButton: false,
mentionMembers: [], mentionMembers: [],
firstUnreadMessageId: undefined,
}; };
} }
@ -609,6 +615,7 @@ const conversationsSlice = createSlice({
showScrollButton: false, showScrollButton: false,
animateQuotedMessageId: undefined, animateQuotedMessageId: undefined,
mentionMembers: [], mentionMembers: [],
firstUnreadMessageId: undefined,
}; };
}, },
showLightBox( showLightBox(
@ -653,15 +660,16 @@ 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(
fetchMessagesForConversation.fulfilled, 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 // 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 // double check that this update is for the shown convo
if (conversationKey === state.selectedConversation) { if (conversationKey === state.selectedConversation) {
return { return {
...state, ...state,
messages: messagesProps, messages: messagesProps,
areMoreMessagesBeingFetched: false, areMoreMessagesBeingFetched: false,
firstUnreadMessageId,
}; };
} }
return state; return state;

@ -438,3 +438,26 @@ function getFirstMessageUnreadIndex(messages: Array<MessageModelProps>) {
return -1; 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…
Cancel
Save