diff --git a/setupTests.js b/setupTests.js index 8bd74c2829b..8066e685a75 100644 --- a/setupTests.js +++ b/setupTests.js @@ -68,5 +68,12 @@ window.z = {userPermission: {}}; window.URL.createObjectURL = jest.fn(); window.URL.revokeObjectURL = jest.fn(); +Object.defineProperty(document, 'elementFromPoint', { + writable: true, + value: jest.fn().mockImplementation((x, y) => { + return null; + }), +}); + const testLib = require('@testing-library/react'); testLib.configure({testIdAttribute: 'data-uie-name'}); diff --git a/src/script/components/Conversation/Conversation.tsx b/src/script/components/Conversation/Conversation.tsx index 60039ecae5e..b9bb032d725 100644 --- a/src/script/components/Conversation/Conversation.tsx +++ b/src/script/components/Conversation/Conversation.tsx @@ -37,6 +37,7 @@ import {CallingViewMode, CallState} from 'src/script/calling/CallState'; import {Config} from 'src/script/Config'; import {PROPERTIES_TYPE} from 'src/script/properties/PropertiesType'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; +import {isLastReceivedMessage} from 'Util/conversationMessages'; import {allowsAllFiles, getFileExtensionOrName, hasAllowedExtension} from 'Util/FileTypeUtil'; import {isHittingUploadLimit} from 'Util/isHittingUploadLimit'; import {t} from 'Util/LocalizerUtil'; @@ -381,16 +382,13 @@ export const Conversation = ({ } }; - const isLastReceivedMessage = (messageEntity: Message, conversationEntity: ConversationEntity): boolean => { - return !!messageEntity.timestamp() && messageEntity.timestamp() >= conversationEntity.last_event_timestamp(); - }; - - const updateConversationLastRead = (conversationEntity: ConversationEntity, messageEntity: Message): void => { + const updateConversationLastRead = (conversationEntity: ConversationEntity, messageEntity?: Message): void => { const conversationLastRead = conversationEntity.last_read_timestamp(); const lastKnownTimestamp = conversationEntity.getLastKnownTimestamp(repositories.serverTime.toServerTimestamp()); const needsUpdate = conversationLastRead < lastKnownTimestamp; - if (needsUpdate && isLastReceivedMessage(messageEntity, conversationEntity)) { + // if no message provided it means we need to jump to the last message + if (needsUpdate && (!messageEntity || isLastReceivedMessage(messageEntity, conversationEntity))) { conversationEntity.setTimestamp(lastKnownTimestamp, ConversationEntity.TIMESTAMP_TYPE.LAST_READ); repositories.message.markAsRead(conversationEntity); } @@ -399,6 +397,7 @@ export const Conversation = ({ const getInViewportCallback = useCallback( (conversationEntity: ConversationEntity, messageEntity: Message) => { const messageTimestamp = messageEntity.timestamp(); + const callbacks: Function[] = []; if (!messageEntity.isEphemeral()) { @@ -526,11 +525,12 @@ export const Conversation = ({ onClickMessage={handleClickOnMessage} onLoading={loading => setIsConversationLoaded(!loading)} getVisibleCallback={getInViewportCallback} - isLastReceivedMessage={isLastReceivedMessage} isMsgElementsFocusable={isMsgElementsFocusable} setMsgElementsFocusable={setMsgElementsFocusable} isRightSidebarOpen={isRightSidebarOpen} + updateConversationLastRead={updateConversationLastRead} /> + {isConversationLoaded && (isReadOnlyConversation ? ( diff --git a/src/script/components/MessagesList/JumpToLastMessageButton.test.tsx b/src/script/components/MessagesList/JumpToLastMessageButton.test.tsx new file mode 100644 index 00000000000..4db6216490c --- /dev/null +++ b/src/script/components/MessagesList/JumpToLastMessageButton.test.tsx @@ -0,0 +1,47 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {render} from '@testing-library/react'; + +import {JumpToLastMessageButton} from 'Components/MessagesList/JumpToLastMessageButton'; + +import {generateConversation} from '../../../../test/helper/ConversationGenerator'; +import {withTheme} from '../../auth/util/test/TestUtil'; + +describe('JumpToLastMessageButton', () => { + const conversation = generateConversation(); + + it('visible when last message is not shown', () => { + conversation.isLastMessageVisible(false); + const {getByTestId} = render( + withTheme(), + ); + + expect(getByTestId('jump-to-last-message-button')).toBeTruthy(); + }); + + it('hidden when last message is shown', () => { + conversation.isLastMessageVisible(true); + const {queryByTestId} = render( + withTheme(), + ); + + expect(queryByTestId('jump-to-last-message-button')).toBeNull(); + }); +}); diff --git a/src/script/components/MessagesList/JumpToLastMessageButton.tsx b/src/script/components/MessagesList/JumpToLastMessageButton.tsx new file mode 100644 index 00000000000..5ae1d31b68c --- /dev/null +++ b/src/script/components/MessagesList/JumpToLastMessageButton.tsx @@ -0,0 +1,65 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {HTMLProps, useEffect, useState} from 'react'; + +import {debounce} from 'underscore'; + +import {ChevronIcon, IconButton} from '@wireapp/react-ui-kit'; + +import { + jumpToLastMessageButtonStyles, + jumpToLastMessageChevronStyles, +} from 'Components/MessagesList/MessageList.styles'; + +import {Conversation} from '../../entity/Conversation'; + +export interface JumpToLastMessageButtonProps extends HTMLProps { + onGoToLastMessage: () => void; + conversation: Conversation; +} + +export const JumpToLastMessageButton = ({onGoToLastMessage, conversation}: JumpToLastMessageButtonProps) => { + const [isLastMessageVisible, setIsLastMessageVisible] = useState(conversation.isLastMessageVisible()); + + useEffect(() => { + const subscription = conversation.isLastMessageVisible.subscribe( + debounce(value => { + setIsLastMessageVisible(value); + }, 200), + ); + return () => { + subscription.dispose(); + }; + }, [conversation]); + + if (isLastMessageVisible) { + return null; + } + + return ( + + + + ); +}; diff --git a/src/script/components/MessagesList/Message/index.tsx b/src/script/components/MessagesList/Message/index.tsx index a6ba05e002f..da6dab41014 100644 --- a/src/script/components/MessagesList/Message/index.tsx +++ b/src/script/components/MessagesList/Message/index.tsx @@ -69,6 +69,7 @@ export interface MessageParams extends MessageActions { }; messageRepository: MessageRepository; onVisible?: () => void; + onVisibilityLost?: () => void; selfId: QualifiedId; shouldShowInvitePeople: boolean; teamState?: TeamState; @@ -88,6 +89,7 @@ export const Message: React.FC = p isHighlighted, hideHeader, onVisible, + onVisibilityLost, scrollTo, isFocused, handleFocus, @@ -154,7 +156,13 @@ export const Message: React.FC = p ); const wrappedContent = onVisible ? ( - + {content} ) : ( diff --git a/src/script/components/MessagesList/MessageList.styles.ts b/src/script/components/MessagesList/MessageList.styles.ts new file mode 100644 index 00000000000..6b9054b1638 --- /dev/null +++ b/src/script/components/MessagesList/MessageList.styles.ts @@ -0,0 +1,41 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {CSSObject} from '@emotion/react'; + +export const jumpToLastMessageButtonStyles: CSSObject = { + position: 'absolute', + right: '10px', + height: '40px', + borderRadius: '100%', + bottom: '56px', + + '@media (max-width: 768px)': { + bottom: '100px', + }, +}; + +export const jumpToLastMessageChevronStyles: CSSObject = { + rotate: '90deg', + height: 16, + width: 16, + path: { + fill: 'var(--accent-color)', + }, +}; diff --git a/src/script/components/MessagesList/MessageList.test.tsx b/src/script/components/MessagesList/MessageList.test.tsx index b40a9fc1a50..f29d45ef935 100644 --- a/src/script/components/MessagesList/MessageList.test.tsx +++ b/src/script/components/MessagesList/MessageList.test.tsx @@ -45,7 +45,6 @@ const getDefaultParams = (): React.ComponentProps => { } as any, getVisibleCallback: jest.fn(), invitePeople: jest.fn(), - isLastReceivedMessage: jest.fn(), messageActions: { deleteMessage: jest.fn(), deleteMessageEveryone: jest.fn(), @@ -65,6 +64,7 @@ const getDefaultParams = (): React.ComponentProps => { isMsgElementsFocusable: true, setMsgElementsFocusable: jest.fn(), showMessageReactions: jest.fn(), + updateConversationLastRead: jest.fn(), }; }; diff --git a/src/script/components/MessagesList/MessageList.tsx b/src/script/components/MessagesList/MessageList.tsx index 2aab694a600..e063d7912e9 100644 --- a/src/script/components/MessagesList/MessageList.tsx +++ b/src/script/components/MessagesList/MessageList.tsx @@ -23,6 +23,7 @@ import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums'; import cx from 'classnames'; import {FadingScrollbar} from 'Components/FadingScrollbar'; +import {JumpToLastMessageButton} from 'Components/MessagesList/JumpToLastMessageButton'; import {filterMessages} from 'Components/MessagesList/utils/messagesFilter'; import {ConversationRepository} from 'src/script/conversation/ConversationRepository'; import {MessageRepository} from 'src/script/conversation/MessageRepository'; @@ -34,6 +35,7 @@ import {User} from 'src/script/entity/User'; import {useRoveFocus} from 'src/script/hooks/useRoveFocus'; import {ServiceEntity} from 'src/script/integration/ServiceEntity'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; +import {isLastReceivedMessage} from 'Util/conversationMessages'; import {onHitTopOrBottom} from 'Util/DOM/onHitTopOrBottom'; import {useResizeObserver} from 'Util/DOM/resizeObserver'; @@ -43,7 +45,7 @@ import {ScrollToElement} from './Message/types'; import {groupMessagesBySenderAndTime, isMarker} from './utils/messagesGroup'; import {updateScroll, FocusedElement} from './utils/scrollUpdater'; -import {Conversation as ConversationEntity, Conversation} from '../../entity/Conversation'; +import {Conversation} from '../../entity/Conversation'; import {isContentMessage} from '../../guards/Message'; interface MessagesListParams { @@ -66,10 +68,10 @@ interface MessagesListParams { showMessageReactions: (message: MessageEntity, showReactions?: boolean) => void; showParticipants: (users: User[]) => void; showUserDetails: (user: User | ServiceEntity) => void; - isLastReceivedMessage: (messageEntity: MessageEntity, conversationEntity: ConversationEntity) => boolean; isMsgElementsFocusable: boolean; setMsgElementsFocusable: (isMsgElementsFocusable: boolean) => void; isRightSidebarOpen?: boolean; + updateConversationLastRead: (conversation: Conversation) => void; } export const MessagesList: FC = ({ @@ -89,10 +91,10 @@ export const MessagesList: FC = ({ invitePeople, messageActions, onLoading, - isLastReceivedMessage, isMsgElementsFocusable, setMsgElementsFocusable, isRightSidebarOpen = false, + updateConversationLastRead, }) => { const { messages: allMessages, @@ -103,7 +105,6 @@ export const MessagesList: FC = ({ inTeam, isLoadingMessages, hasAdditionalMessages, - initialMessage, } = useKoSubscribableChildren(conversation, [ 'inTeam', 'isActiveParticipant', @@ -113,12 +114,11 @@ export const MessagesList: FC = ({ 'isGuestAndServicesRoom', 'isLoadingMessages', 'hasAdditionalMessages', - 'initialMessage', ]); const messageListRef = useRef(null); const [loaded, setLoaded] = useState(false); - const [highlightedMessage, setHighlightedMessage] = useState(initialMessage?.id); + const [highlightedMessage, setHighlightedMessage] = useState(conversation.initialMessage()?.id); const conversationLastReadTimestamp = useRef(conversation.last_read_timestamp()); const filteredMessages = filterMessages(allMessages); @@ -126,7 +126,7 @@ export const MessagesList: FC = ({ const groupedMessages = groupMessagesBySenderAndTime(filteredMessages, conversationLastReadTimestamp.current); - const [messagesContainer, setMessageContainer] = useState(null); + const [messagesContainer, setMessagesContainer] = useState(null); const shouldShowInvitePeople = isActiveParticipant && inTeam && (isGuestRoom || isGuestAndServicesRoom); @@ -168,7 +168,7 @@ export const MessagesList: FC = ({ scrollHeight.current = newScrollHeight; }, [messagesContainer?.parentElement, loaded, filteredMessages, selfUser?.id]); - // Listen to resizes of the the content element (if it's resized it means something has changed in the message list, link a link preview was generated) + // Listen to resizes of the content element (if it's resized it means something has changed in the message list, link a link preview was generated) useResizeObserver(syncScrollPosition, messagesContainer); // Also listen to the scrolling container resizes (when the window resizes or the inputBar changes) useResizeObserver(syncScrollPosition, messagesContainer?.parentElement); @@ -200,14 +200,19 @@ export const MessagesList: FC = ({ onLoading(true); setLoaded(false); conversationLastReadTimestamp.current = conversation.last_read_timestamp(); - loadConversation(conversation, initialMessage).then(() => { + loadConversation(conversation, conversation.initialMessage()).then(() => { setTimeout(() => { setLoaded(true); onLoading(false); + // if new conversation is loaded but there are unread messages, previous conversation + // last message visibility might not be cleaned as this conversation last message is not loaded yet + if (!conversation.hasLastReceivedMessageLoaded()) { + conversation.isLastMessageVisible(false); + } }, 10); }); return () => conversation.release(); - }, [conversation, initialMessage]); + }, [conversation]); useLayoutEffect(() => { if (loaded && messageListRef.current) { @@ -245,78 +250,118 @@ export const MessagesList: FC = ({ syncScrollPosition(); }; + const jumpToLastMessage = () => { + if (conversation) { + // clean up anything like search result + setHighlightedMessage(undefined); + conversation.initialMessage(undefined); + focusedElement.current = null; + // if there are unloaded messages, the conversation should be marked as read and reloaded + if (!conversation.hasLastReceivedMessageLoaded()) { + updateConversationLastRead(conversation); + conversation.release(); + loadConversation(conversation); + } else { + // we just need to scroll down + messageListRef.current?.scrollTo?.({behavior: 'smooth', top: messageListRef.current.scrollHeight}); + } + } + }; + return ( - -
- {groupedMessages.flatMap(group => { - if (isMarker(group)) { - return ( - - ); - } - const {messages, firstMessageTimestamp} = group; - - return messages.map(message => { - const isLastDeliveredMessage = lastDeliveredMessage?.id === message.id; - - const visibleCallback = getVisibleCallback(conversation, message); - - const key = `${message.id || 'message'}-${message.timestamp()}`; - - const isHighlighted = !!highlightedMessage && highlightedMessage === message.id; - const isFocused = !!focusedId && focusedId === message.id; - - return ( - invitePeople(conversation)} - onClickReactionDetails={message => showMessageReactions(message, true)} - onClickMessage={onClickMessage} - onClickParticipants={showParticipants} - onClickDetails={message => showMessageDetails(message)} - onClickResetSession={resetSession} - onClickTimestamp={async function (messageId: string) { - setHighlightedMessage(messageId); - setTimeout(() => setHighlightedMessage(undefined), 5000); - const messageIsLoaded = conversation.getMessage(messageId); - - if (!messageIsLoaded) { - const messageEntity = await messageRepository.getMessageInConversationById(conversation, messageId); - conversation.removeMessages(); - conversationRepository.getMessagesWithOffset(conversation, messageEntity); + <> + +
+ {groupedMessages.flatMap((group, groupIndex) => { + if (isMarker(group)) { + return ( + + ); + } + const {messages, firstMessageTimestamp} = group; + + return messages.map((message, messageIndex) => { + const isLastDeliveredMessage = lastDeliveredMessage?.id === message.id; + const isLastLoadedMessage = + groupIndex === groupedMessages.length - 1 && messageIndex === messages.length - 1; + + const isLastMessage = isLastLoadedMessage && conversation.hasLastReceivedMessageLoaded(); + + const visibleCallback = () => { + getVisibleCallback(conversation, message)?.(); + if (isLastMessage) { + conversation.isLastMessageVisible(true); + } + }; + + const lastMessageInvisibleCallback = isLastMessage + ? () => { + conversation.isLastMessageVisible(false); } - }} - selfId={selfUser.qualifiedId} - shouldShowInvitePeople={shouldShowInvitePeople} - isFocused={isFocused} - handleFocus={setFocusedId} - handleArrowKeyDown={handleKeyDown} - isMsgElementsFocusable={isMsgElementsFocusable} - setMsgElementsFocusable={setMsgElementsFocusable} - /> - ); - }); - })} -
-
+ : undefined; + + const key = `${message.id || 'message'}-${message.timestamp()}`; + + const isHighlighted = !!highlightedMessage && highlightedMessage === message.id; + const isFocused = !!focusedId && focusedId === message.id; + + return ( + invitePeople(conversation)} + onClickReactionDetails={message => showMessageReactions(message, true)} + onClickMessage={onClickMessage} + onClickParticipants={showParticipants} + onClickDetails={message => showMessageDetails(message)} + onClickResetSession={resetSession} + onClickTimestamp={async function (messageId: string) { + setHighlightedMessage(messageId); + setTimeout(() => setHighlightedMessage(undefined), 5000); + const messageIsLoaded = conversation.getMessage(messageId); + + if (!messageIsLoaded) { + const messageEntity = await messageRepository.getMessageInConversationById( + conversation, + messageId, + ); + conversation.removeMessages(); + conversationRepository.getMessagesWithOffset(conversation, messageEntity); + } + }} + selfId={selfUser.qualifiedId} + shouldShowInvitePeople={shouldShowInvitePeople} + isFocused={isFocused} + handleFocus={setFocusedId} + handleArrowKeyDown={handleKeyDown} + isMsgElementsFocusable={isMsgElementsFocusable} + setMsgElementsFocusable={setMsgElementsFocusable} + /> + ); + }); + })} +
+
+ + ); }; diff --git a/src/script/components/utils/InViewport.tsx b/src/script/components/utils/InViewport.tsx index dd77695419a..9b8cc424934 100644 --- a/src/script/components/utils/InViewport.tsx +++ b/src/script/components/utils/InViewport.tsx @@ -25,6 +25,7 @@ import {viewportObserver} from 'Util/DOM/viewportObserver'; interface InViewportParams { onVisible: () => void; onVisibilityLost?: () => void; + callVisibilityLostOnUnmount?: boolean; requireFullyInView?: boolean; allowBiggerThanViewport?: boolean; /** Will check if the element is overlayed by something else. Can be used to be sure the user could actually see the element. Should not be used to do lazy loading as the overlayObserver has quite a long debounce time */ @@ -38,6 +39,7 @@ const InViewport: React.FC> = requireFullyInView = false, checkOverlay = false, allowBiggerThanViewport = false, + callVisibilityLostOnUnmount = false, ...props }) => { const domNode = useRef(null); @@ -101,7 +103,9 @@ const InViewport: React.FC> = } return () => { // If the element is unmounted, we can trigger the onVisibilityLost callback and release the trackers - onVisibilityLost?.(); + if (callVisibilityLostOnUnmount) { + onVisibilityLost?.(); + } releaseTrackers(); }; }, [allowBiggerThanViewport, requireFullyInView, checkOverlay, onVisible, onVisibilityLost]); diff --git a/src/script/entity/Conversation.ts b/src/script/entity/Conversation.ts index 31810667864..5c3e6c89c75 100644 --- a/src/script/entity/Conversation.ts +++ b/src/script/entity/Conversation.ts @@ -163,6 +163,7 @@ export class Conversation { public readonly receiptMode: ko.Observable; public readonly removed_from_conversation: ko.PureComputed; public readonly roles: ko.Observable>; + public readonly isLastMessageVisible: ko.Observable; public readonly selfUser: ko.Observable; public readonly servicesCount: ko.PureComputed; public readonly showNotificationsEverything: ko.PureComputed; @@ -206,6 +207,7 @@ export class Conversation { this.teamId = undefined; this.type = ko.observable(); + this.isLastMessageVisible = ko.observable(true); this.isLoadingMessages = ko.observable(false); this.isTextInputReady = ko.observable(false); @@ -423,6 +425,7 @@ export class Conversation { return message_a.timestamp() - message_b.timestamp(); }), ); + this.lastDeliveredMessage = ko.pureComputed(() => this.getLastDeliveredMessage()); this.incomingMessages = ko.observableArray(); diff --git a/src/script/page/AppMain.tsx b/src/script/page/AppMain.tsx index 6bd6282dec7..4b285beb443 100644 --- a/src/script/page/AppMain.tsx +++ b/src/script/page/AppMain.tsx @@ -159,7 +159,7 @@ export const AppMain: FC = ({ configureRoutes({ '/': showMostRecentConversation, '/conversation/:conversationId(/:domain)': (conversationId: string, domain: string = apiContext.domain ?? '') => - mainView.content.showConversation({id: conversationId, domain}, {}), + mainView.content.showConversation({id: conversationId, domain}), '/preferences/about': () => mainView.list.openPreferencesAbout(), '/preferences/account': () => mainView.list.openPreferencesAccount(), '/preferences/av': () => mainView.list.openPreferencesAudioVideo(), diff --git a/src/script/util/conversationMessages.ts b/src/script/util/conversationMessages.ts index 68f09de51e0..4350246ea3a 100644 --- a/src/script/util/conversationMessages.ts +++ b/src/script/util/conversationMessages.ts @@ -21,6 +21,8 @@ import {Asset} from 'src/script/entity/message/Asset'; import type {FileAsset as FileAssetType} from 'src/script/entity/message/FileAsset'; import {AssetType} from '../assets/AssetType'; +import {Conversation} from '../entity/Conversation'; +import type {Message} from '../entity/message/Message'; interface MessageDataType { senderName: string; @@ -55,3 +57,7 @@ export function getMessageAriaLabel({senderName, displayTimestampShort, assets}: } }); } + +export const isLastReceivedMessage = (messageEntity: Message, conversationEntity: Conversation): boolean => { + return messageEntity.timestamp() >= conversationEntity.last_event_timestamp(); +}; diff --git a/src/script/view_model/ContentViewModel.ts b/src/script/view_model/ContentViewModel.ts index 14edfb6a18a..8e9a98b7f09 100644 --- a/src/script/view_model/ContentViewModel.ts +++ b/src/script/view_model/ContentViewModel.ts @@ -56,8 +56,8 @@ interface ShowConversationOptions { } interface ShowConversationOverload { - (conversation: Conversation | undefined, options: ShowConversationOptions): Promise; - (conversationId: QualifiedId, options: ShowConversationOptions): Promise; + (conversation: Conversation | undefined, options?: ShowConversationOptions): Promise; + (conversationId: QualifiedId, options?: ShowConversationOptions): Promise; } export class ContentViewModel { @@ -92,7 +92,7 @@ export class ContentViewModel { const showMostRecentConversation = () => { const mostRecentConversation = this.conversationState.getMostRecentConversation(); - this.showConversation(mostRecentConversation, {}); + this.showConversation(mostRecentConversation); }; this.userState.connectRequests.subscribe(requests => { @@ -233,13 +233,13 @@ export class ContentViewModel { */ readonly showConversation: ShowConversationOverload = async ( conversation: Conversation | QualifiedId | undefined, - options: ShowConversationOptions, + options?: ShowConversationOptions, ) => { const { exposeMessage: exposeMessageEntity, openFirstSelfMention = false, openNotificationSettings = false, - } = options; + } = options || {}; if (!conversation) { return this.handleMissingConversation();