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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from '@rocket.chat/fuselage';
import type { Keys as IconName } from '@rocket.chat/icons';
import { useRouter } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

import { links } from '../../../lib/links';
Expand All @@ -26,7 +25,7 @@ type RoomE2EENotAllowedProps = {
icon: IconName;
};

const RoomE2EENotAllowed = ({ title, subTitle, action, btnText, icon }: RoomE2EENotAllowedProps): ReactElement => {
const RoomE2EENotAllowed = ({ title, subTitle, action, btnText, icon }: RoomE2EENotAllowedProps) => {
const router = useRouter();
const { t } = useTranslation();
const handleGoHomeClick = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { HeaderTag, HeaderTagIcon } from '@rocket.chat/ui-client';
import type { ReactElement } from 'react';
import { useMemo } from 'react';

import type { IRoomWithFederationOriginalName } from '../contexts/RoomContext';
Expand All @@ -8,7 +7,7 @@ type FederatedRoomOriginServerProps = {
room: IRoomWithFederationOriginalName;
};

const FederatedRoomOriginServer = ({ room }: FederatedRoomOriginServerProps): ReactElement | null => {
const FederatedRoomOriginServer = ({ room }: FederatedRoomOriginServerProps) => {
const originServerName = useMemo(() => room.federationOriginalName?.split(':')[1], [room.federationOriginalName]);
if (!originServerName) {
return null;
Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/client/views/room/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { isInviteSubscription } from '@rocket.chat/core-typings';
import type { IRoom, ISubscription } from '@rocket.chat/core-typings';
import { useLayout, useSetting } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { lazy, memo } from 'react';

const RoomInviteHeader = lazy(() => import('./RoomInviteHeader'));
Expand All @@ -14,7 +13,7 @@ type HeaderProps = {
subscription?: ISubscription;
};

const Header = ({ room, subscription }: HeaderProps): ReactElement | null => {
const Header = ({ room, subscription }: HeaderProps) => {
const { isEmbedded, showTopNavbarEmbeddedLayout } = useLayout();
const encrypted = Boolean(room.encrypted);
const unencryptedMessagesAllowed = useSetting('E2E_Allow_Unencrypted_Messages', false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { HeaderIcon } from '@rocket.chat/ui-client';
import type { ReactElement } from 'react';

import { OmnichannelRoomIcon } from '../../../components/RoomIcon/OmnichannelRoomIcon';
import { useRoomIcon } from '../../../hooks/useRoomIcon';
Expand All @@ -10,7 +9,7 @@ type HeaderIconWithRoomProps = {
room: IRoom;
};

const HeaderIconWithRoom = ({ room }: HeaderIconWithRoomProps): ReactElement => {
const HeaderIconWithRoom = ({ room }: HeaderIconWithRoomProps) => {
const icon = useRoomIcon(room);
if (isOmnichannelRoom(room)) {
return <OmnichannelRoomIcon source={room.source} status={room.v?.status} size='x20' />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { HeaderToolbarAction } from '@rocket.chat/ui-client';
import { useRouter } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

type BackButtonProps = { routeName?: string };

const BackButton = ({ routeName }: BackButtonProps): ReactElement => {
const BackButton = ({ routeName }: BackButtonProps) => {
const router = useRouter();
const { t } = useTranslation();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export const useDropdownVisibility = <T extends HTMLElement>({
reference,
target,
}: {
reference: RefObject<T>;
target: RefObject<T>;
reference: RefObject<T | null>;
target: RefObject<T | null>;
}): {
isVisible: boolean;
toggle: (state?: boolean) => void;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { States, StatesIcon, StatesTitle, StatesSubtitle, StatesActions, StatesAction, Icon } from '@rocket.chat/fuselage';
import type { ReactElement, ReactNode } from 'react';
import type { ReactNode } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';

import { useRoom } from '../contexts/RoomContext';

const MessageListErrorBoundary = ({ children }: { children: ReactNode }): ReactElement => {
const MessageListErrorBoundary = ({ children }: { children: ReactNode }) => {
const { t } = useTranslation();
const room = useRoom();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { ReactElement, ContextType, ReactNode } from 'react';
import type { ContextType, ReactNode } from 'react';
import { useMemo, useSyncExternalStore } from 'react';

import * as messageHighlightSubscription from './messageHighlightSubscription';
import MessageHighlightContext from '../contexts/MessageHighlightContext';

const MessageHighlightProvider = ({ children }: { children: ReactNode }): ReactElement => {
const MessageHighlightProvider = ({ children }: { children: ReactNode }) => {
const highlightMessageId = useSyncExternalStore(messageHighlightSubscription.subscribe, messageHighlightSubscription.getSnapshot);

const contextValue = useMemo<ContextType<typeof MessageHighlightContext>>(
Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/client/views/room/NotSubscribedRoom.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { Box, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage';
import type { ReactElement } from 'react';
import { Trans, useTranslation } from 'react-i18next';

import RoomLayout from './layout/RoomLayout';
Expand All @@ -12,7 +11,7 @@ type NotSubscribedRoomProps = {
type: IRoom['t'];
};

const NotSubscribedRoom = ({ rid, reference, type }: NotSubscribedRoomProps): ReactElement => {
const NotSubscribedRoom = ({ rid, reference, type }: NotSubscribedRoomProps) => {
const { t } = useTranslation();
const handleJoinClick = useJoinRoom();

Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/client/views/room/Room.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { isInviteSubscription } from '@rocket.chat/core-typings';
import { ContextualbarSkeleton } from '@rocket.chat/ui-client';
import { useSetting, useRoomToolbox, useUserId } from '@rocket.chat/ui-contexts';
import { useMediaCallOpenRoomTracker } from '@rocket.chat/ui-voip';
import type { ReactElement } from 'react';
import { createElement, lazy, memo, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
Expand All @@ -23,7 +22,7 @@ import { SelectedMessagesProvider } from './providers/SelectedMessagesProvider';

const UiKitContextualBar = lazy(() => import('./contextualBar/uikit/UiKitContextualBar'));

const Room = (): ReactElement => {
const Room = () => {
const { t } = useTranslation();
const userId = useUserId();
const room = useRoom();
Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/client/views/room/RoomNotFound.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Box } from '@rocket.chat/fuselage';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

import RoomLayout from './layout/RoomLayout';
import NotFoundState from '../../components/NotFoundState';

const RoomNotFound = (): ReactElement => {
const RoomNotFound = () => {
const { t } = useTranslation();

return (
Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/client/views/room/RoomOpener.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { RoomType } from '@rocket.chat/core-typings';
import { Box, States, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage';
import { Header } from '@rocket.chat/ui-client';
import type { ReactElement } from 'react';
import { lazy, Suspense } from 'react';
import { useTranslation } from 'react-i18next';

Expand All @@ -25,7 +24,7 @@ type RoomOpenerProps = {
reference: string;
};

const RoomOpener = ({ type, reference }: RoomOpenerProps): ReactElement => {
const RoomOpener = ({ type, reference }: RoomOpenerProps) => {
const { data, error, isSuccess, isError, isLoading } = useOpenRoom({ type, reference });
const { t } = useTranslation();

Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/client/views/room/RoomOpenerEmbedded.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { ISubscription, RoomType } from '@rocket.chat/core-typings';
import { Box, States, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage';
import { Header } from '@rocket.chat/ui-client';
import { useStream, useUserId } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { lazy, Suspense, useEffect } from 'react';
import { useTranslation } from 'react-i18next';

Expand All @@ -27,7 +26,7 @@ type RoomOpenerProps = {
reference: string;
};

const RoomOpenerEmbedded = ({ type, reference }: RoomOpenerProps): ReactElement => {
const RoomOpenerEmbedded = ({ type, reference }: RoomOpenerProps) => {
const { data, error, isSuccess, isError, isLoading } = useOpenRoom({ type, reference });
const uid = useUserId();
const subscribeToNotifyUser = useStream('notify-user');
Expand Down
4 changes: 1 addition & 3 deletions apps/meteor/client/views/room/RoomSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type { ReactElement } from 'react';

import HeaderSkeleton from './Header/HeaderSkeleton';
import RoomComposerSkeleton from './composer/RoomComposer/RoomComposerSkeleton';
import RoomLayout from './layout/RoomLayout';
import ListSkeleton from '../../components/ListSkeleton';

const RoomSkeleton = (): ReactElement => (
const RoomSkeleton = () => (
<RoomLayout
header={<HeaderSkeleton />}
body={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings';
import { GenericModal } from '@rocket.chat/ui-client';
import { useEndpoint, useTranslation, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { ReactElement } from 'react';

import { getGeolocationPermission } from './getGeolocationPermission';
import { getGeolocationPosition } from './getGeolocationPosition';
Expand All @@ -14,7 +13,7 @@ type ShareLocationModalProps = {
onClose: () => void;
};

const ShareLocationModal = ({ rid, tmid, onClose }: ShareLocationModalProps): ReactElement => {
const ShareLocationModal = ({ rid, tmid, onClose }: ShareLocationModalProps) => {
const t = useTranslation();
const dispatchToast = useToastMessageDispatch();
const { data: permissionState, isLoading: permissionLoading } = useQuery({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { IRoom } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { GenericMenu } from '@rocket.chat/ui-client';
import { useSetting, useRolesDescription } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

Expand Down Expand Up @@ -90,7 +89,7 @@ const UserCardWithData = ({ username, rid, onOpenUserInfo, onClose }: UserCardWi
}, [menuOptions, onClose, t]);

const actions = useMemo(() => {
const mapAction = ([key, { content, title, icon, onClick, disabled }]: [string, UserInfoAction]): ReactElement => (
const mapAction = ([key, { content, title, icon, onClick, disabled }]: [string, UserInfoAction]) => (
<UserCardAction key={key} label={content || title} aria-label={content || title} onClick={onClick} icon={icon!} disabled={disabled} />
);

Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/views/room/body/DropTargetOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import type { DragEvent, ReactElement, ReactNode } from 'react';
import type { DragEvent, ReactNode } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

Expand All @@ -15,7 +15,7 @@ type DropTargetOverlayProps = {
onDismiss?: () => void;
};

function DropTargetOverlay({ enabled, reason, onFileDrop, visible = true, onDismiss }: DropTargetOverlayProps): ReactElement | null {
function DropTargetOverlay({ enabled, reason, onFileDrop, visible = true, onDismiss }: DropTargetOverlayProps) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH Client-Side SSRF and Same-Origin/Intranet Data Exfiltration via Drag-and-Drop in DropTargetOverlay

In DropTargetOverlay.tsx, when a user performs a drag-and-drop operation, the application checks if the dragged data contains both text/uri-list and text/html. If it does, it parses the HTML data using document.createRange().createContextualFragment(...), queries all <img> elements, and performs a client-side fetch request to the src attribute of each image. The fetched data is then converted into a File object and automatically added to the list of files to be uploaded to the current chat room.

An attacker can exploit this to exfiltrate sensitive data from the Rocket.Chat server (same-origin endpoints requiring the user's active session/cookies) or from any accessible local/intranet services (e.g., local development servers, router admin pages, cloud metadata endpoints).

By hosting a malicious website or sending a crafted message/email that prompts the user to drag an element from the attacker's controlled page into the Rocket.Chat client, the attacker can force the client to:

  1. Fetch sensitive same-origin endpoints (such as API responses containing private keys, list of users, settings, or private messages) using the user's authenticated session.
  2. Fetch sensitive local/intranet resources.
  3. Automatically upload the retrieved data as a file attachment directly into the chat room where the drop occurred. If the attacker is a participant in that room (e.g., a public channel or a shared private room/DM), they can immediately download the uploaded file and access the exfiltrated sensitive data.
Steps to Reproduce
  1. The attacker hosts a web page with the following draggable element:
<div draggable="true" id="drag-me">Drag me to Rocket.Chat to share!</div>
<script>
document.getElementById('drag-me').addEventListener('dragstart', (event) => {
    event.dataTransfer.setData('text/uri-list', 'http://localhost:3000/api/v1/me');
    event.dataTransfer.setData('text/html', '<img src="http://localhost:3000/api/v1/me">');
});
</script>
  1. The victim, logged into Rocket.Chat, drags this element and drops it into a chat room where the attacker is present.
  2. The Rocket.Chat client parses the HTML, finds the <img> tag, and executes fetch('http://localhost:3000/api/v1/me') with the victim's session cookies.
  3. The client receives the JSON response containing the victim's personal profile and session details, packages it as a file, and uploads it to the chat room.
  4. The attacker downloads the uploaded file from the chat room, successfully exfiltrating the victim's sensitive profile data.
Trace
graph TD
    subgraph SG0 ["apps/meteor/client/hooks/useFormatDateAndTime.ts"]
        useFormatDateAndTime["Returns a date and time formatting function based on user preferences and system settings."]
    end
    style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG1 ["apps/meteor/client/lib/utils/dateFormat.ts"]
        momentFormatToDateFns["momentFormatToDateFns"]
        flushLiteral["flushLiteral"]
        safeFormat["Safely formats a date using date-fns with fallback."]
        formatDate["Formats a date based on a format string."]
    end
    style SG1 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG2 ["apps/meteor/client/views/room/Room.tsx"]
        Room["Main component for rendering a chat room, handling E2EE setup, message lists, and contextual bars."]
    end
    style SG2 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG3 ["apps/meteor/client/views/room/body/DropTargetOverlay.tsx"]
        DropTargetOverlay{{"Overlay component that handles file drag-and-drop events, including processing dropped files and image URLs."}}
    end
    style SG3 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG4 ["apps/meteor/client/views/room/body/RoomBody.tsx"]
        RoomBody["Main container component for the chat room, managing message lists, composer, UI state, and room-specific features."]
    end
    style SG4 fill:#2a2a2a,stroke:#444,color:#aaa
    DropTargetOverlay --> useFormatDateAndTime
    useFormatDateAndTime --> formatDate
    formatDate --> safeFormat
    safeFormat --> momentFormatToDateFns
    momentFormatToDateFns --> flushLiteral
    RoomBody --> DropTargetOverlay
    Room --> RoomBody
Loading
Fix with AI

Open in Cursor Open in Claude

A security vulnerability was found by Hacktron.

File: apps/meteor/client/views/room/body/DropTargetOverlay.tsx
Lines: 18
Severity: high

Vulnerability: Client-Side SSRF and Same-Origin/Intranet Data Exfiltration via Drag-and-Drop in DropTargetOverlay

Description:
In `DropTargetOverlay.tsx`, when a user performs a drag-and-drop operation, the application checks if the dragged data contains both `text/uri-list` and `text/html`. If it does, it parses the HTML data using `document.createRange().createContextualFragment(...)`, queries all `<img>` elements, and performs a client-side `fetch` request to the `src` attribute of each image. The fetched data is then converted into a `File` object and automatically added to the list of files to be uploaded to the current chat room.

An attacker can exploit this to exfiltrate sensitive data from the Rocket.Chat server (same-origin endpoints requiring the user's active session/cookies) or from any accessible local/intranet services (e.g., local development servers, router admin pages, cloud metadata endpoints).

By hosting a malicious website or sending a crafted message/email that prompts the user to drag an element from the attacker's controlled page into the Rocket.Chat client, the attacker can force the client to:
1. Fetch sensitive same-origin endpoints (such as API responses containing private keys, list of users, settings, or private messages) using the user's authenticated session.
2. Fetch sensitive local/intranet resources.
3. Automatically upload the retrieved data as a file attachment directly into the chat room where the drop occurred. If the attacker is a participant in that room (e.g., a public channel or a shared private room/DM), they can immediately download the uploaded file and access the exfiltrated sensitive data.

Proof of Concept:
1. The attacker hosts a web page with the following draggable element:
```html
<div draggable="true" id="drag-me">Drag me to Rocket.Chat to share!</div>
<script>
document.getElementById('drag-me').addEventListener('dragstart', (event) => {
    event.dataTransfer.setData('text/uri-list', 'http://localhost:3000/api/v1/me');
    event.dataTransfer.setData('text/html', '<img src="http://localhost:3000/api/v1/me">');
});
</script>
```
2. The victim, logged into Rocket.Chat, drags this element and drops it into a chat room where the attacker is present.
3. The Rocket.Chat client parses the HTML, finds the `<img>` tag, and executes `fetch('http://localhost:3000/api/v1/me')` with the victim's session cookies.
4. The client receives the JSON response containing the victim's personal profile and session details, packages it as a file, and uploads it to the chat room.
5. The attacker downloads the uploaded file from the chat room, successfully exfiltrating the victim's sensitive profile data.

Affected Code:
```typescript
		if (event.dataTransfer.types.includes('text/uri-list') && event.dataTransfer.types.includes('text/html')) {
			const fragment = document.createRange().createContextualFragment(event.dataTransfer.getData('text/html'));
			for await (const { src } of Array.from(fragment.querySelectorAll('img'))) {
				try {
					const response = await fetch(src);
					const data = await response.blob();
					const extension = (await import('../../../../app/utils/lib/mimeTypes')).mime.extension(data.type);
					const filename = `File - ${formatDateAndTime(new Date())}.${extension}`;
					const file = new File([data], filename, { type: data.type });
					files.push(file);
				} catch (error) {
					console.warn(error);
				}
			}
		}
```

Acceptance criteria:
- Acceptance is defined by the **actual reported behavior**, not by tests passing.
- Reproduce the issue, or narrow the exact code path that produces it, *before* changing code. State what you confirmed.
- Fix the underlying cause. Mitigations that paper over the reported behavior do not count as a fix.
- Add a regression test that fails on the unpatched code and passes on the fix. If a regression test is genuinely impractical (e.g. race condition, infra-level issue), say so and explain why.
- Existing tests passing is **not** the bar. Do not declare done on tests-pass theatre.

Only change what is necessary to fix this vulnerability. Do not refactor adjacent code or modify unrelated files.

Triage: Reply !fp <reason> (false positive), !valid (confirmed), or !accepted_risk <reason>. Any other reply is saved as a triage note.
Reason is optional but improves future scans — e.g. !fp internal endpoint, not user-facing.

View finding in Hacktron

const { t } = useTranslation();

const handleDragLeave = useEffectEvent((event: DragEvent) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { css } from '@rocket.chat/css-in-js';
import { Box, Bubble } from '@rocket.chat/fuselage';
import { isTruthy } from '@rocket.chat/tools';
import type { ReactElement } from 'react';
import { useState } from 'react';

type JumpToRecentMessageButtonProps = {
Expand Down Expand Up @@ -39,7 +38,7 @@ const buttonStyle = css`
}
`;

const JumpToRecentMessageButton = ({ visible, onClick, text }: JumpToRecentMessageButtonProps): ReactElement => {
const JumpToRecentMessageButton = ({ visible, onClick, text }: JumpToRecentMessageButtonProps) => {
const [clicked, setClicked] = useState(false);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { ReactElement } from 'react';

import LoadingIndicator from '../../../components/LoadingIndicator';

const LoadingMessagesIndicator = (): ReactElement => <LoadingIndicator />;
const LoadingMessagesIndicator = () => <LoadingIndicator />;

export default LoadingMessagesIndicator;
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { Box, Bubble } from '@rocket.chat/fuselage';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

import { withErrorBoundary } from '../../../components/withErrorBoundary';
import { usePruneWarningMessage } from '../../../hooks/usePruneWarningMessage';

const RetentionPolicyWarning = ({ room }: { room: IRoom }): ReactElement => {
const RetentionPolicyWarning = ({ room }: { room: IRoom }) => {
const { t } = useTranslation();

const message = usePruneWarningMessage(room);
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/views/room/body/RoomBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Box } from '@rocket.chat/fuselage';
import { isTruthy } from '@rocket.chat/tools';
import { CustomVirtuaScrollbars, useEmbeddedLayout } from '@rocket.chat/ui-client';
import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference, useRoomToolbox } from '@rocket.chat/ui-contexts';
import type { MouseEvent, ReactElement } from 'react';
import type { MouseEvent } from 'react';
import { memo, useCallback, useMemo, useRef, useState } from 'react';

import { useMergedRefsV2 } from '../../../hooks/useMergedRefsV2';
Expand Down Expand Up @@ -32,7 +32,7 @@ import { useSelectAllAndScrollToTop } from './hooks/useSelectAllAndScrollToTop';
import { useHandleUnread } from './hooks/useUnreadMessages';
import useTryToJumpToThreadMessage from '../MessageList/hooks/useTryToJumpToThreadMessage';

const RoomBody = (): ReactElement => {
const RoomBody = () => {
const chat = useChat();
if (!chat) {
throw new Error('No ChatContext provided');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings';
import { isDirectMessageRoom } from '@rocket.chat/core-typings';
import { Flex, Box } from '@rocket.chat/fuselage';
import { UserAvatar } from '@rocket.chat/ui-avatar';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

import RoomForewordUsernameList from './RoomForewordUsernameList';

type RoomForewordProps = { user: IUser | null; room: IRoom };

const RoomForeword = ({ user, room }: RoomForewordProps): ReactElement | null => {
const RoomForeword = ({ user, room }: RoomForewordProps) => {
const { t } = useTranslation();

if (!isDirectMessageRoom(room)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { css } from '@rocket.chat/css-in-js';
import { Box, Bubble } from '@rocket.chat/fuselage';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

type UnreadMessagesIndicatorProps = {
Expand All @@ -16,7 +15,7 @@ const indicatorStyle = css`
z-index: 3;
`;

const UnreadMessagesIndicator = ({ count, onJumpButtonClick, onMarkAsReadButtonClick }: UnreadMessagesIndicatorProps): ReactElement => {
const UnreadMessagesIndicator = ({ count, onJumpButtonClick, onMarkAsReadButtonClick }: UnreadMessagesIndicatorProps) => {
const { t } = useTranslation();

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { css } from '@rocket.chat/css-in-js';
import { Box, Bubble } from '@rocket.chat/fuselage';
import type { ReactElement } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

Expand All @@ -10,7 +9,7 @@ type UploadProgressIndicatorProps = {
uploads: readonly Upload[];
};

const UploadProgressIndicator = ({ uploads }: UploadProgressIndicatorProps): ReactElement | null => {
const UploadProgressIndicator = ({ uploads }: UploadProgressIndicatorProps) => {
const { t } = useTranslation();

const { percentage, count } = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Box, Icon } from '@rocket.chat/fuselage';
import { MessageFooterCallout } from '@rocket.chat/ui-composer';
import type { ReactElement } from 'react';
import { Trans } from 'react-i18next';

const ComposerAirGappedRestricted = (): ReactElement => {
const ComposerAirGappedRestricted = () => {
return (
<MessageFooterCallout color='default'>
<Icon name='warning' size={20} mie={4} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import {
useMethod,
} from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import type { ReactElement } from 'react';

const ComposerAnonymous = (): ReactElement => {
const ComposerAnonymous = () => {
const t = useTranslation();
const dispatch = useToastMessageDispatch();
const isAnonymousWriteEnabled = useSetting('Accounts_AllowAnonymousWrite');
Expand Down
Loading
Loading