Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Jump to last message WPB-6518 #17386

Merged
merged 16 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,12 @@ window.z = {userPermission: {}};
window.URL.createObjectURL = jest.fn();
window.URL.revokeObjectURL = jest.fn();

Object.defineProperty(document, 'elementFromPoint', {
svitovyda marked this conversation as resolved.
Show resolved Hide resolved
writable: true,
value: jest.fn().mockImplementation((x, y) => {
return null;
}),
});

const testLib = require('@testing-library/react');
testLib.configure({testIdAttribute: 'data-uie-name'});
14 changes: 7 additions & 7 deletions src/script/components/Conversation/Conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
Expand All @@ -399,6 +397,7 @@ export const Conversation = ({
const getInViewportCallback = useCallback(
(conversationEntity: ConversationEntity, messageEntity: Message) => {
const messageTimestamp = messageEntity.timestamp();

const callbacks: Function[] = [];

if (!messageEntity.isEphemeral()) {
Expand Down Expand Up @@ -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 ? (
<ReadOnlyConversationMessage reloadApp={reloadApp} conversation={activeConversation} />
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<JumpToLastMessageButton onGoToLastMessage={jest.fn()} conversation={conversation} />),
);

expect(getByTestId('jump-to-last-message-button')).toBeTruthy();
});

it('hidden when last message is shown', () => {
conversation.isLastMessageVisible(true);
const {queryByTestId} = render(
withTheme(<JumpToLastMessageButton onGoToLastMessage={jest.fn()} conversation={conversation} />),
);

expect(queryByTestId('jump-to-last-message-button')).toBeNull();
});
});
65 changes: 65 additions & 0 deletions src/script/components/MessagesList/JumpToLastMessageButton.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement> {
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 (
<IconButton
data-uie-name="jump-to-last-message-button"
onClick={onGoToLastMessage}
css={jumpToLastMessageButtonStyles}
>
<ChevronIcon css={jumpToLastMessageChevronStyles} />
</IconButton>
);
};
10 changes: 9 additions & 1 deletion src/script/components/MessagesList/Message/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface MessageParams extends MessageActions {
};
messageRepository: MessageRepository;
onVisible?: () => void;
onVisibilityLost?: () => void;
selfId: QualifiedId;
shouldShowInvitePeople: boolean;
teamState?: TeamState;
Expand All @@ -88,6 +89,7 @@ export const Message: React.FC<MessageParams & {scrollTo?: ScrollToElement}> = p
isHighlighted,
hideHeader,
onVisible,
onVisibilityLost,
scrollTo,
isFocused,
handleFocus,
Expand Down Expand Up @@ -154,7 +156,13 @@ export const Message: React.FC<MessageParams & {scrollTo?: ScrollToElement}> = p
);

const wrappedContent = onVisible ? (
<InViewport requireFullyInView allowBiggerThanViewport checkOverlay onVisible={onVisible}>
<InViewport
requireFullyInView
allowBiggerThanViewport
checkOverlay
onVisible={onVisible}
onVisibilityLost={onVisibilityLost}
>
{content}
</InViewport>
) : (
Expand Down
41 changes: 41 additions & 0 deletions src/script/components/MessagesList/MessageList.styles.ts
Original file line number Diff line number Diff line change
@@ -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',
},
Comment on lines +29 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@V-Gira I see this value being copied a few times in the app. Not a topic for now, but we should have a single place where the different breakpoints are defined so we avoid hardcoding breakpoints like this (and when possible those breakpoints should not be used in favor of just relying on the natural DOM flow).

I did try to use the DOM flow to have the button placing naturally, but turns out it's not really possible to have a button that is fixed in a scrolling container sadly.

Let's try to optimize this in a followup PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also see that we have lots of hardcoded breakpoints in the code, and they are different. We should define key breakpoints, and then each component should use them and introduce new ones as theme.breakpoints.tablet - COMPONENT_CUSTOM_WIDTH_CONSTANT etc.

};

export const jumpToLastMessageChevronStyles: CSSObject = {
rotate: '90deg',
height: 16,
width: 16,
path: {
fill: 'var(--accent-color)',
},
};
2 changes: 1 addition & 1 deletion src/script/components/MessagesList/MessageList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const getDefaultParams = (): React.ComponentProps<typeof MessagesList> => {
} as any,
getVisibleCallback: jest.fn(),
invitePeople: jest.fn(),
isLastReceivedMessage: jest.fn(),
messageActions: {
deleteMessage: jest.fn(),
deleteMessageEveryone: jest.fn(),
Expand All @@ -65,6 +64,7 @@ const getDefaultParams = (): React.ComponentProps<typeof MessagesList> => {
isMsgElementsFocusable: true,
setMsgElementsFocusable: jest.fn(),
showMessageReactions: jest.fn(),
updateConversationLastRead: jest.fn(),
};
};

Expand Down