diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 5fcfa9e0f..96e846869 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -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), diff --git a/ts/components/conversation/ExpireTimer.tsx b/ts/components/conversation/ExpireTimer.tsx index 43f18016a..290f90eaf 100644 --- a/ts/components/conversation/ExpireTimer.tsx +++ b/ts/components/conversation/ExpireTimer.tsx @@ -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 ( { ); } + const bucket = getTimerBucket(expirationTimestamp, expirationLength); return (
{ ); 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; } diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index 4903729b6..b018592cb 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -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 { private readonly messageContainerRef: React.RefObject; - 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 { (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 { // 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 { ); } - private renderMessages(messages: Array) { + private displayUnreadBannerIndex(messages: Array) { 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) { + 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 { // 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 = ( @@ -327,11 +336,11 @@ export class SessionMessagesList extends React.Component { 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 { } contextMenu.hideAll(); - if (!this.state.doneInitialScroll) { + if (this.ignoreScrollEvents) { return; } @@ -357,9 +366,9 @@ export class SessionMessagesList extends React.Component { 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 { } // 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 { } } - 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 { // 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 { const scrollTop = messageContainer.scrollTop; const scrollHeight = messageContainer.scrollHeight; const clientHeight = messageContainer.clientHeight; - const scrollOffsetPx = scrollHeight - scrollTop - clientHeight; - - return scrollOffsetPx; + return scrollHeight - scrollTop - clientHeight; } } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 9e6f14e89..c58876f97 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -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,