fix scrolling to unread and marking message as read on scrolling

we need to hit the bottom for the convo to update currently

add smooth scrolling on click on quoted message
pull/1381/head
Audric Ackermann 4 years ago
parent d0043ca245
commit a1d4dea845
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -83,9 +83,6 @@
'show-message-detail', 'show-message-detail',
this.showMessageDetail this.showMessageDetail
); );
this.listenTo(this.model.messageCollection, 'navigate-to', url => {
window.location = url;
});
this.lazyUpdateVerified = _.debounce( this.lazyUpdateVerified = _.debounce(
this.model.updateVerified.bind(this.model), this.model.updateVerified.bind(this.model),

@ -12,10 +12,6 @@ type Props = {
}; };
export const ExpireTimer = (props: Props) => { export const ExpireTimer = (props: Props) => {
const [lastUpdated, setLastUpdated] = useState(Date.now());
const update = () => {
setLastUpdated(Date.now());
};
const { const {
direction, direction,
expirationLength, expirationLength,
@ -23,14 +19,27 @@ export const ExpireTimer = (props: Props) => {
withImageNoCaption, withImageNoCaption,
} = props; } = props;
const initialTimeLeft = Math.max(
Math.round((expirationTimestamp - Date.now()) / 1000),
0
);
const [timeLeft, setTimeLeft] = useState(initialTimeLeft);
const update = () => {
const newTimeLeft = Math.max(
Math.round((expirationTimestamp - Date.now()) / 1000),
0
);
if (newTimeLeft !== timeLeft) {
setTimeLeft(newTimeLeft);
}
};
const increment = getIncrement(expirationLength); const increment = getIncrement(expirationLength);
const updateFrequency = Math.max(increment, 500); const updateFrequency = Math.max(increment, 500);
useInterval(update, updateFrequency); useInterval(update, updateFrequency);
const bucket = getTimerBucket(expirationTimestamp, expirationLength);
let timeLeft = Math.round((expirationTimestamp - Date.now()) / 1000);
timeLeft = timeLeft >= 0 ? timeLeft : 0;
if (timeLeft <= 60) { if (timeLeft <= 60) {
return ( return (
<span <span
@ -44,6 +53,7 @@ export const ExpireTimer = (props: Props) => {
</span> </span>
); );
} }
const bucket = getTimerBucket(expirationTimestamp, expirationLength);
return ( return (
<div <div

@ -273,6 +273,7 @@ export class SessionConversation extends React.Component<Props, State> {
); );
if (this.messageContainerRef.current) { if (this.messageContainerRef.current) {
// force scrolling to bottom on message sent // force scrolling to bottom on message sent
// this will mark all messages as read
(this.messageContainerRef (this.messageContainerRef
.current as any).scrollTop = this.messageContainerRef.current?.scrollHeight; .current as any).scrollTop = this.messageContainerRef.current?.scrollHeight;
} }

@ -19,7 +19,6 @@ import { ToastUtils } from '../../../session/utils';
interface State { interface State {
showScrollButton: boolean; showScrollButton: boolean;
doneInitialScroll: boolean;
} }
interface Props { interface Props {
@ -45,23 +44,25 @@ interface Props {
export class SessionMessagesList extends React.Component<Props, State> { export class SessionMessagesList extends React.Component<Props, State> {
private readonly messageContainerRef: React.RefObject<any>; private readonly messageContainerRef: React.RefObject<any>;
private scrollOffsetPx: number = Number.MAX_VALUE; private scrollOffsetBottomPx: number = Number.MAX_VALUE;
private ignoreScrollEvents: boolean;
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
showScrollButton: false, showScrollButton: false,
doneInitialScroll: false,
}; };
this.renderMessage = this.renderMessage.bind(this); this.renderMessage = this.renderMessage.bind(this);
this.handleScroll = this.handleScroll.bind(this); this.handleScroll = this.handleScroll.bind(this);
this.scrollToUnread = this.scrollToUnread.bind(this); this.scrollToUnread = this.scrollToUnread.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this); this.scrollToBottom = this.scrollToBottom.bind(this);
this.scrollToQuoteMessage = this.scrollToQuoteMessage.bind(this); this.scrollToQuoteMessage = this.scrollToQuoteMessage.bind(this);
this.getScrollOffsetPx = this.getScrollOffsetPx.bind(this); this.getScrollOffsetBottomPx = this.getScrollOffsetBottomPx.bind(this);
this.displayUnreadBannerIndex = this.displayUnreadBannerIndex.bind(this);
this.messageContainerRef = this.props.messageContainerRef; this.messageContainerRef = this.props.messageContainerRef;
this.ignoreScrollEvents = true;
} }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -83,11 +84,11 @@ export class SessionMessagesList extends React.Component<Props, State> {
(prevProps.messages.length === 0 && this.props.messages.length !== 0) (prevProps.messages.length === 0 && this.props.messages.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.scrollOffsetPx = Number.MAX_VALUE; this.scrollOffsetBottomPx = Number.MAX_VALUE;
this.ignoreScrollEvents = true;
this.setState( this.setState(
{ {
showScrollButton: false, showScrollButton: false,
doneInitialScroll: false,
}, },
this.scrollToUnread this.scrollToUnread
); );
@ -95,18 +96,18 @@ export class SessionMessagesList extends React.Component<Props, State> {
// if we got new message for this convo, and we are scrolled to bottom // if we got new message for this convo, and we are scrolled to bottom
if (isSameConvo && messageLengthChanged) { if (isSameConvo && messageLengthChanged) {
// Keep scrolled to bottom unless user scrolls up // Keep scrolled to bottom unless user scrolls up
if (this.getScrollOffsetPx() === 0) { if (this.getScrollOffsetBottomPx() === 0) {
this.scrollToBottom(); this.scrollToBottom();
} else { } else {
const messageContainer = this.messageContainerRef?.current; const messageContainer = this.messageContainerRef?.current;
if (messageContainer) { if (messageContainer) {
global.setTimeout(() => {
const scrollHeight = messageContainer.scrollHeight; const scrollHeight = messageContainer.scrollHeight;
const clientHeight = messageContainer.clientHeight; const clientHeight = messageContainer.clientHeight;
this.ignoreScrollEvents = true;
messageContainer.scrollTop = messageContainer.scrollTop =
scrollHeight - clientHeight - this.scrollOffsetPx; scrollHeight - clientHeight - this.scrollOffsetBottomPx;
}, 10); this.ignoreScrollEvents = false;
} }
} }
} }
@ -133,41 +134,49 @@ export class SessionMessagesList extends React.Component<Props, State> {
); );
} }
private renderMessages(messages: Array<MessageModel>) { private displayUnreadBannerIndex(messages: Array<MessageModel>) {
const { conversation } = this.props; const { conversation } = this.props;
if (conversation.unreadCount === 0) {
const multiSelectMode = Boolean(this.props.selectedMessages.length); return -1;
let currentMessageIndex = 0; }
// find the first unread message in the list of messages. We use this to display the // conversation.unreadCount is the number of messages we incoming we did not read yet.
// unread banner so this is at all times at the correct index. // also, unreacCount is updated only when the conversation is marked as read.
// Our messages are marked read, so be sure to skip those. // So we can have an unreadCount for the conversation not correct based on the real number of unread messages.
// some of the messages we have in "messages" are ones we sent ourself (or from another device).
// the messages variable is ordered with most recent message being on index 0. // those messages should not be counted to display the unread banner.
// so we need to start from the end to find our first message unread
let findFirstUnreadIndex = -1; let findFirstUnreadIndex = -1;
let incomingMessagesSoFar = 0;
const { unreadCount } = conversation;
for (let index = messages.length - 1; index >= 0; index--) { // Basically, count the number of incoming messages from the most recent one.
for (let index = 0; index <= messages.length - 1; index++) {
const message = messages[index]; const message = messages[index];
if (message.attributes.type === 'incoming') {
incomingMessagesSoFar++;
// message.attributes.unread is !== undefined if the message is unread.
if ( if (
message.attributes.type === 'incoming' && message.attributes.unread !== undefined &&
message.attributes.unread !== undefined incomingMessagesSoFar >= unreadCount
) { ) {
findFirstUnreadIndex = index; findFirstUnreadIndex = index;
break; break;
} }
} }
}
// if we did not find an unread messsages, but the conversation has some, //
// we must not have enough messages in memory, so just display the unread banner if (findFirstUnreadIndex === -1 && conversation.unreadCount >= 0) {
// at the top of the screen return conversation.unreadCount - 1;
if (findFirstUnreadIndex === -1 && conversation.unreadCount !== 0) {
findFirstUnreadIndex = messages.length - 1;
} }
if (conversation.unreadCount === 0) { return findFirstUnreadIndex;
findFirstUnreadIndex = -1;
} }
private renderMessages(messages: Array<MessageModel>) {
const multiSelectMode = Boolean(this.props.selectedMessages.length);
let currentMessageIndex = 0;
const displayUnreadBannerIndex = this.displayUnreadBannerIndex(messages);
return ( return (
<> <>
{messages.map((message: MessageModel) => { {messages.map((message: MessageModel) => {
@ -186,12 +195,12 @@ export class SessionMessagesList extends React.Component<Props, State> {
// AND we are not scrolled all the way to the bottom // AND we are not scrolled all the way to the bottom
// THEN, show the unread banner for the current message // THEN, show the unread banner for the current message
const showUnreadIndicator = const showUnreadIndicator =
findFirstUnreadIndex >= 0 && displayUnreadBannerIndex >= 0 &&
currentMessageIndex === findFirstUnreadIndex && currentMessageIndex === displayUnreadBannerIndex &&
this.getScrollOffsetPx() !== 0; this.getScrollOffsetBottomPx() !== 0;
const unreadIndicator = ( const unreadIndicator = (
<SessionLastSeenIndicator <SessionLastSeenIndicator
count={findFirstUnreadIndex + 1} // count is used for the 118n of the string count={displayUnreadBannerIndex + 1} // count is used for the 118n of the string
show={showUnreadIndicator} show={showUnreadIndicator}
key={`unread-indicator-${message.id}`} key={`unread-indicator-${message.id}`}
/> />
@ -327,11 +336,11 @@ export class SessionMessagesList extends React.Component<Props, State> {
return; return;
} }
if (!this.state.doneInitialScroll) { if (this.ignoreScrollEvents) {
return; return;
} }
if (this.getScrollOffsetPx() === 0) { if (this.getScrollOffsetBottomPx() === 0) {
void conversation.markRead(messages[0].attributes.received_at); void conversation.markRead(messages[0].attributes.received_at);
} }
} }
@ -348,7 +357,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
} }
contextMenu.hideAll(); contextMenu.hideAll();
if (!this.state.doneInitialScroll) { if (this.ignoreScrollEvents) {
return; return;
} }
@ -357,9 +366,9 @@ export class SessionMessagesList extends React.Component<Props, State> {
const scrollButtonViewShowLimit = 0.75; const scrollButtonViewShowLimit = 0.75;
const scrollButtonViewHideLimit = 0.4; const scrollButtonViewHideLimit = 0.4;
this.scrollOffsetPx = this.getScrollOffsetPx(); this.scrollOffsetBottomPx = this.getScrollOffsetBottomPx();
const scrollOffsetPc = this.scrollOffsetPx / clientHeight; const scrollOffsetPc = this.scrollOffsetBottomPx / clientHeight;
// Scroll button appears if you're more than 75% scrolled up // Scroll button appears if you're more than 75% scrolled up
if ( if (
@ -377,7 +386,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
} }
// Scrolled to bottom // Scrolled to bottom
const isScrolledToBottom = this.getScrollOffsetPx() === 0; const isScrolledToBottom = this.getScrollOffsetBottomPx() === 0;
if (isScrolledToBottom) { if (isScrolledToBottom) {
// Mark messages read // Mark messages read
this.updateReadMessages(); this.updateReadMessages();
@ -418,19 +427,18 @@ export class SessionMessagesList extends React.Component<Props, State> {
} }
} }
if (!this.state.doneInitialScroll && messages.length > 0) { if (this.ignoreScrollEvents && messages.length > 0) {
this.setState( this.ignoreScrollEvents = false;
{ this.updateReadMessages();
doneInitialScroll: true,
},
this.updateReadMessages
);
} }
} }
private scrollToMessage(messageId: string) { private scrollToMessage(messageId: string, smooth: boolean = false) {
const topUnreadMessage = document.getElementById(messageId); const topUnreadMessage = document.getElementById(messageId);
topUnreadMessage?.scrollIntoView(false); topUnreadMessage?.scrollIntoView({
behavior: smooth ? 'smooth' : 'auto',
block: 'center',
});
const messageContainer = this.messageContainerRef.current; const messageContainer = this.messageContainerRef.current;
if (!messageContainer) { if (!messageContainer) {
@ -510,11 +518,11 @@ export class SessionMessagesList extends React.Component<Props, State> {
// return; // return;
// } // }
// this probably does not work for us as we need to call getMessages before // this probably does not work for us as we need to call getMessages before
this.scrollToMessage(databaseId); this.scrollToMessage(databaseId, true);
} }
// basically the offset in px from the bottom of the view (most recent message) // basically the offset in px from the bottom of the view (most recent message)
private getScrollOffsetPx() { private getScrollOffsetBottomPx() {
const messageContainer = this.messageContainerRef?.current; const messageContainer = this.messageContainerRef?.current;
if (!messageContainer) { if (!messageContainer) {
@ -524,8 +532,6 @@ export class SessionMessagesList extends React.Component<Props, State> {
const scrollTop = messageContainer.scrollTop; const scrollTop = messageContainer.scrollTop;
const scrollHeight = messageContainer.scrollHeight; const scrollHeight = messageContainer.scrollHeight;
const clientHeight = messageContainer.clientHeight; const clientHeight = messageContainer.clientHeight;
const scrollOffsetPx = scrollHeight - scrollTop - clientHeight; return scrollHeight - scrollTop - clientHeight;
return scrollOffsetPx;
} }
} }

@ -568,11 +568,17 @@ export function reducer(
m => m.id === messageId m => m.id === messageId
); );
if (messageInStoreIndex >= 0) { if (messageInStoreIndex >= 0) {
// we cannot edit the array directly, so slice the first part, and slice the second part // we cannot edit the array directly, so slice the first part, and slice the second part,
// keeping the index removed out
const editedMessages = [ const editedMessages = [
...state.messages.slice(0, messageInStoreIndex), ...state.messages.slice(0, messageInStoreIndex),
...state.messages.slice(messageInStoreIndex + 1), ...state.messages.slice(messageInStoreIndex + 1),
]; ];
// 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 one one counted as an unread
return { return {
...state, ...state,
messages: editedMessages, messages: editedMessages,

Loading…
Cancel
Save