Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spicy-wombats-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': minor
---

Introduces message navigability, allowing users to navigate on messages through keyboard
31 changes: 15 additions & 16 deletions apps/meteor/client/components/message/MessageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
MessageNameContainer,
} from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import type { KeyboardEvent, ReactElement } from 'react';
import React, { memo } from 'react';

import { getUserDisplayName } from '../../../lib/getUserDisplayName';
Expand Down Expand Up @@ -45,31 +45,30 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => {

return (
<FuselageMessageHeader>
<MessageNameContainer>
<MessageNameContainer
tabIndex={0}
role='button'
aria-label={getUserDisplayName(user.name, user.username, showRealName)}
{...(user.username !== undefined &&
chat?.userCard && {
onClick: (e) => chat?.userCard.openUserCard(e, message.u.username),
onKeyDown: (e: KeyboardEvent<HTMLSpanElement>) => {
(e.code === 'Enter' || e.code === 'Space') && chat?.userCard.openUserCard(e, message.u.username);
},
style: { cursor: 'pointer' },
})}
>
<MessageName
{...(!showUsername && { 'data-qa-type': 'username' })}
title={!showUsername && !usernameAndRealNameAreSame ? `@${user.username}` : undefined}
data-username={user.username}
{...(user.username !== undefined &&
chat?.userCard && {
onClick: (e) => chat?.userCard.openUserCard(e, message.u.username),
style: { cursor: 'pointer' },
})}
>
{message.alias || getUserDisplayName(user.name, user.username, showRealName)}
</MessageName>
{showUsername && (
<>
{' '}
<MessageUsername
data-username={user.username}
data-qa-type='username'
{...(user.username !== undefined &&
chat?.userCard && {
onClick: (e) => chat?.userCard.openUserCard(e, message.u.username),
style: { cursor: 'pointer' },
})}
>
<MessageUsername data-username={user.username} data-qa-type='username'>
@{user.username}
</MessageUsername>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useToolbar } from '@react-aria/toolbar';
import type { IMessage, IRoom, ISubscription, ITranslatedMessage } from '@rocket.chat/core-typings';
import { isThreadMessage, isRoomFederated, isVideoConfMessage } from '@rocket.chat/core-typings';
import { MessageToolbar as FuselageMessageToolbar, MessageToolbarItem } from '@rocket.chat/fuselage';
import { useFeaturePreview } from '@rocket.chat/ui-client';
import { useUser, useSettings, useTranslation, useMethod, useLayoutHiddenActions } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import type { ReactElement } from 'react';
import React, { memo, useMemo } from 'react';
import type { ComponentProps, ReactElement } from 'react';
import React, { memo, useMemo, useRef } from 'react';

import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction';
import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction';
Expand Down Expand Up @@ -45,19 +46,23 @@ type MessageToolbarProps = {
room: IRoom;
subscription?: ISubscription;
onChangeMenuVisibility: (visible: boolean) => void;
};
} & ComponentProps<typeof FuselageMessageToolbar>;

const MessageToolbar = ({
message,
messageContext,
room,
subscription,
onChangeMenuVisibility,
...props
}: MessageToolbarProps): ReactElement | null => {
const t = useTranslation();
const user = useUser() ?? undefined;
const settings = useSettings();

const toolbarRef = useRef(null);
const { toolbarProps } = useToolbar(props, toolbarRef);

const quickReactionsEnabled = useFeaturePreview('quickReactions');

const setReaction = useMethod('setReaction');
Expand Down Expand Up @@ -106,7 +111,7 @@ const MessageToolbar = ({
};

return (
<FuselageMessageToolbar>
<FuselageMessageToolbar ref={toolbarRef} {...toolbarProps} aria-label={t('Message_actions')} {...props}>
{quickReactionsEnabled &&
isReactionAllowed &&
quickReactions.slice(0, 3).map(({ emoji, image }) => {
Expand Down
11 changes: 7 additions & 4 deletions apps/meteor/client/components/message/variants/RoomMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Message, MessageLeftContainer, MessageContainer, CheckBox } from '@rock
import { useToggle } from '@rocket.chat/fuselage-hooks';
import { MessageAvatar } from '@rocket.chat/ui-avatar';
import { useUserId } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import type { ComponentProps, ReactElement } from 'react';
import React, { useRef, memo } from 'react';

import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction';
Expand Down Expand Up @@ -33,7 +33,7 @@ type RoomMessageProps = {
context?: MessageActionContext;
ignoredUser?: boolean;
searchText?: string;
};
} & ComponentProps<typeof Message>;

const RoomMessage = ({
message,
Expand All @@ -45,6 +45,7 @@ const RoomMessage = ({
context,
ignoredUser,
searchText,
...props
}: RoomMessageProps): ReactElement => {
const uid = useUserId();
const editing = useIsMessageHighlight(message._id);
Expand All @@ -60,10 +61,13 @@ const RoomMessage = ({
useCountSelected();

useJumpToMessage(message._id, messageRef);

return (
<Message
ref={messageRef}
id={message._id}
role='listitem'
tabIndex={0}
onClick={selecting ? toggleSelected : undefined}
isSelected={selected}
isEditing={editing}
Expand All @@ -78,6 +82,7 @@ const RoomMessage = ({
data-own={message.u._id === uid}
data-qa-type='message'
aria-busy={message.temp}
{...props}
>
<MessageLeftContainer>
{!sequential && message.u.username && !selecting && showUserAvatar && (
Expand All @@ -95,10 +100,8 @@ const RoomMessage = ({
{selecting && <CheckBox checked={selected} onChange={toggleSelected} />}
{sequential && <StatusIndicators message={message} />}
</MessageLeftContainer>

<MessageContainer>
{!sequential && <MessageHeader message={message} />}

{ignored ? (
<IgnoredContent onShowMessageIgnored={toggleDisplayIgnoredMessage} />
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { UserAvatar } from '@rocket.chat/ui-avatar';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import type { ComponentProps, ReactElement } from 'react';
import React, { memo } from 'react';

import { MessageTypes } from '../../../../app/ui-utils/client';
Expand All @@ -37,9 +37,9 @@ import { useMessageListShowRealName, useMessageListShowUsername } from '../list/
type SystemMessageProps = {
message: IMessage;
showUserAvatar: boolean;
};
} & ComponentProps<typeof MessageSystem>;

const SystemMessage = ({ message, showUserAvatar }: SystemMessageProps): ReactElement => {
const SystemMessage = ({ message, showUserAvatar, ...props }: SystemMessageProps): ReactElement => {
const t = useTranslation();
const formatTime = useFormatTime();
const formatDateAndTime = useFormatDateAndTime();
Expand All @@ -59,11 +59,14 @@ const SystemMessage = ({ message, showUserAvatar }: SystemMessageProps): ReactEl

return (
<MessageSystem
role='listitem'
tabIndex={0}
onClick={isSelecting ? toggleSelected : undefined}
isSelected={isSelected}
data-qa-selected={isSelected}
data-qa='system-message'
data-system-message-type={message.t}
{...props}
>
<MessageSystemLeftContainer>
{!isSelecting && showUserAvatar && <UserAvatar username={message.u.username} size='x18' />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const ThreadMessage = ({ message, sequential, unread, showUserAvatar }: ThreadMe

return (
<Message
role='listitem'
tabIndex={0}
id={message._id}
ref={messageRef}
isEditing={editing}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '@rocket.chat/fuselage';
import { MessageAvatar } from '@rocket.chat/ui-avatar';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import type { ComponentProps, ReactElement } from 'react';
import React, { memo } from 'react';

import { MessageTypes } from '../../../../app/ui-utils/client';
Expand All @@ -36,7 +36,7 @@ type ThreadMessagePreviewProps = {
message: IThreadMessage;
showUserAvatar: boolean;
sequential: boolean;
};
} & ComponentProps<typeof ThreadMessage>;

const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }: ThreadMessagePreviewProps): ReactElement => {
const parentMessage = useParentMessage(message.tmid);
Expand All @@ -56,23 +56,30 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }:

const goToThread = useGoToThread();

const handleThreadClick = () => {
if (!isSelecting) {
if (!sequential) {
return parentMessage.isSuccess && goToThread({ rid: message.rid, tmid: message.tmid, msg: parentMessage.data?._id });
}

return goToThread({ rid: message.rid, tmid: message.tmid, msg: message._id });
}

return toggleSelected();
};

return (
<ThreadMessage
{...props}
onClick={isSelecting ? toggleSelected : undefined}
tabIndex={0}
onClick={handleThreadClick}
onKeyDown={(e) => e.code === 'Enter' && handleThreadClick()}
isSelected={isSelected}
data-qa-selected={isSelected}
role='link'
{...props}
>
{!sequential && (
<ThreadMessageRow
role='link'
onClick={
!isSelecting && parentMessage.isSuccess
? () => goToThread({ rid: message.rid, tmid: message.tmid, msg: parentMessage.data?._id })
: undefined
}
>
<ThreadMessageRow>
<ThreadMessageLeftContainer>
<ThreadMessageIconThread />
</ThreadMessageLeftContainer>
Expand Down Expand Up @@ -100,7 +107,7 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }:
</ThreadMessageContainer>
</ThreadMessageRow>
)}
<ThreadMessageRow onClick={!isSelecting ? () => goToThread({ rid: message.rid, tmid: message.tmid, msg: message._id }) : undefined}>
<ThreadMessageRow>
<ThreadMessageLeftContainer>
{!isSelecting && showUserAvatar && (
<MessageAvatar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ const EmojiElement = ({ emoji, image, onClick, small = false, ...props }: EmojiE

return (
<IconButton
{...props}
{...(small && { className: emojiSmallClass })}
small={small}
medium={!small}
Expand All @@ -40,6 +39,7 @@ const EmojiElement = ({ emoji, image, onClick, small = false, ...props }: EmojiE
data-emoji={emoji}
aria-label={emoji}
icon={emojiElement}
{...props}
/>
);
};
Expand Down
55 changes: 31 additions & 24 deletions apps/meteor/client/views/room/Room.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { createElement, lazy, memo, Suspense } from 'react';
import { FocusScope } from 'react-aria';
import { ErrorBoundary } from 'react-error-boundary';

import { ContextualbarSkeleton } from '../../components/Contextualbar';
Expand All @@ -25,30 +26,36 @@ const Room = (): ReactElement => {
return (
<ChatProvider>
<MessageHighlightProvider>
<RoomLayout
aria-label={t('Channel')}
data-qa-rc-room={room._id}
header={<Header room={room} />}
body={<RoomBody />}
aside={
(toolbox.tab?.tabComponent && (
<ErrorBoundary fallback={null}>
<SelectedMessagesProvider>
<Suspense fallback={<ContextualbarSkeleton />}>{createElement(toolbox.tab.tabComponent)}</Suspense>
</SelectedMessagesProvider>
</ErrorBoundary>
)) ||
(contextualBarView && (
<ErrorBoundary fallback={null}>
<SelectedMessagesProvider>
<Suspense fallback={<ContextualbarSkeleton />}>
<UiKitContextualBar key={contextualBarView.id} initialView={contextualBarView} />
</Suspense>
</SelectedMessagesProvider>
</ErrorBoundary>
))
}
/>
<FocusScope>
<RoomLayout
data-qa-rc-room={room._id}
aria-label={
room.t === 'd'
? t('Conversation_with__roomName__', { roomName: room.name })
: t('Channel__roomName__', { roomName: room.name })
}
header={<Header room={room} />}
body={<RoomBody />}
aside={
(toolbox.tab?.tabComponent && (
<ErrorBoundary fallback={null}>
<SelectedMessagesProvider>
<Suspense fallback={<ContextualbarSkeleton />}>{createElement(toolbox.tab.tabComponent)}</Suspense>
</SelectedMessagesProvider>
</ErrorBoundary>
)) ||
(contextualBarView && (
<ErrorBoundary fallback={null}>
<SelectedMessagesProvider>
<Suspense fallback={<ContextualbarSkeleton />}>
<UiKitContextualBar key={contextualBarView.id} initialView={contextualBarView} />
</Suspense>
</SelectedMessagesProvider>
</ErrorBoundary>
))
}
/>
</FocusScope>
</MessageHighlightProvider>
</ChatProvider>
);
Expand Down
Loading