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',
this.showMessageDetail
);
this.listenTo(this.model.messageCollection, 'navigate-to', url => {
window.location = url;
});
this.lazyUpdateVerified = _.debounce(
this.model.updateVerified.bind(this.model),

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

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

@ -19,7 +19,6 @@ import { ToastUtils } from '../../../session/utils';
interface State {
showScrollButton: boolean;
doneInitialScroll: boolean;
}
interface Props {
@ -45,23 +44,25 @@ interface Props {
export class SessionMessagesList extends React.Component<Props, State> {
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) {
super(props);
this.state = {
showScrollButton: false,
doneInitialScroll: false,
};
this.renderMessage = this.renderMessage.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.scrollToUnread = this.scrollToUnread.bind(this);
this.scrollToBottom = this.scrollToBottom.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.ignoreScrollEvents = true;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -83,11 +84,11 @@ export class SessionMessagesList extends React.Component<Props, State> {
(prevProps.messages.length === 0 && this.props.messages.length !== 0)
) {
// 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(
{
showScrollButton: false,
doneInitialScroll: false,
},
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 (isSameConvo && messageLengthChanged) {
// Keep scrolled to bottom unless user scrolls up
if (this.getScrollOffsetPx() === 0) {
if (this.getScrollOffsetBottomPx() === 0) {
this.scrollToBottom();
} else {
const messageContainer = this.messageContainerRef?.current;
if (messageContainer) {
global.setTimeout(() => {
const scrollHeight = messageContainer.scrollHeight;
const clientHeight = messageContainer.clientHeight;
messageContainer.scrollTop =
scrollHeight - clientHeight - this.scrollOffsetPx;
}, 10);
const scrollHeight = messageContainer.scrollHeight;
const clientHeight = messageContainer.clientHeight;
this.ignoreScrollEvents = true;
messageContainer.scrollTop =
scrollHeight - clientHeight - this.scrollOffsetBottomPx;
this.ignoreScrollEvents = false;
}
}
}
@ -133,40 +134,48 @@ export class SessionMessagesList extends React.Component<Props, State> {
);
}
private renderMessages(messages: Array<MessageModel>) {
private displayUnreadBannerIndex(messages: Array<MessageModel>) {
const { conversation } = this.props;
const multiSelectMode = Boolean(this.props.selectedMessages.length);
let currentMessageIndex = 0;
// find the first unread message in the list of messages. We use this to display the
// unread banner so this is at all times at the correct index.
// Our messages are marked read, so be sure to skip those.
// the messages variable is ordered with most recent message being on index 0.
// so we need to start from the end to find our first message unread
if (conversation.unreadCount === 0) {
return -1;
}
// conversation.unreadCount is the number of messages we incoming we did not read yet.
// also, unreacCount is updated only when the conversation is marked as read.
// 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).
// those messages should not be counted to display the unread banner.
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];
if (
message.attributes.type === 'incoming' &&
message.attributes.unread !== undefined
) {
findFirstUnreadIndex = index;
break;
if (message.attributes.type === 'incoming') {
incomingMessagesSoFar++;
// message.attributes.unread is !== undefined if the message is unread.
if (
message.attributes.unread !== undefined &&
incomingMessagesSoFar >= unreadCount
) {
findFirstUnreadIndex = index;
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
// at the top of the screen
if (findFirstUnreadIndex === -1 && conversation.unreadCount !== 0) {
findFirstUnreadIndex = messages.length - 1;
}
if (conversation.unreadCount === 0) {
findFirstUnreadIndex = -1;
//
if (findFirstUnreadIndex === -1 && conversation.unreadCount >= 0) {
return conversation.unreadCount - 1;
}
return findFirstUnreadIndex;
}
private renderMessages(messages: Array<MessageModel>) {
const multiSelectMode = Boolean(this.props.selectedMessages.length);
let currentMessageIndex = 0;
const displayUnreadBannerIndex = this.displayUnreadBannerIndex(messages);
return (
<>
@ -186,12 +195,12 @@ export class SessionMessagesList extends React.Component<Props, State> {
// AND we are not scrolled all the way to the bottom
// THEN, show the unread banner for the current message
const showUnreadIndicator =
findFirstUnreadIndex >= 0 &&
currentMessageIndex === findFirstUnreadIndex &&
this.getScrollOffsetPx() !== 0;
displayUnreadBannerIndex >= 0 &&
currentMessageIndex === displayUnreadBannerIndex &&
this.getScrollOffsetBottomPx() !== 0;
const unreadIndicator = (
<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}
key={`unread-indicator-${message.id}`}
/>
@ -327,11 +336,11 @@ export class SessionMessagesList extends React.Component<Props, State> {
return;
}
if (!this.state.doneInitialScroll) {
if (this.ignoreScrollEvents) {
return;
}
if (this.getScrollOffsetPx() === 0) {
if (this.getScrollOffsetBottomPx() === 0) {
void conversation.markRead(messages[0].attributes.received_at);
}
}
@ -348,7 +357,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
}
contextMenu.hideAll();
if (!this.state.doneInitialScroll) {
if (this.ignoreScrollEvents) {
return;
}
@ -357,9 +366,9 @@ export class SessionMessagesList extends React.Component<Props, State> {
const scrollButtonViewShowLimit = 0.75;
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
if (
@ -377,7 +386,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
}
// Scrolled to bottom
const isScrolledToBottom = this.getScrollOffsetPx() === 0;
const isScrolledToBottom = this.getScrollOffsetBottomPx() === 0;
if (isScrolledToBottom) {
// Mark messages read
this.updateReadMessages();
@ -418,19 +427,18 @@ export class SessionMessagesList extends React.Component<Props, State> {
}
}
if (!this.state.doneInitialScroll && messages.length > 0) {
this.setState(
{
doneInitialScroll: true,
},
this.updateReadMessages
);
if (this.ignoreScrollEvents && messages.length > 0) {
this.ignoreScrollEvents = false;
this.updateReadMessages();
}
}
private scrollToMessage(messageId: string) {
private scrollToMessage(messageId: string, smooth: boolean = false) {
const topUnreadMessage = document.getElementById(messageId);
topUnreadMessage?.scrollIntoView(false);
topUnreadMessage?.scrollIntoView({
behavior: smooth ? 'smooth' : 'auto',
block: 'center',
});
const messageContainer = this.messageContainerRef.current;
if (!messageContainer) {
@ -510,11 +518,11 @@ export class SessionMessagesList extends React.Component<Props, State> {
// return;
// }
// 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)
private getScrollOffsetPx() {
private getScrollOffsetBottomPx() {
const messageContainer = this.messageContainerRef?.current;
if (!messageContainer) {
@ -524,8 +532,6 @@ export class SessionMessagesList extends React.Component<Props, State> {
const scrollTop = messageContainer.scrollTop;
const scrollHeight = messageContainer.scrollHeight;
const clientHeight = messageContainer.clientHeight;
const scrollOffsetPx = scrollHeight - scrollTop - clientHeight;
return scrollOffsetPx;
return scrollHeight - scrollTop - clientHeight;
}
}

@ -568,11 +568,17 @@ export function reducer(
m => m.id === messageId
);
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 = [
...state.messages.slice(0, messageInStoreIndex),
...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 {
...state,
messages: editedMessages,

Loading…
Cancel
Save