snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,535 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { act, renderHook, waitFor } from '@testing-library/react';
import { useDraft } from './useDraft';
const storageKey = (rid: string, tmid?: string) => `messagebox_${rid}${tmid ? `-${tmid}` : ''}`;
const seedLocalDraft = (value: string, rid: string, tmid?: string) => {
localStorage.setItem(storageKey(rid, tmid), value);
};
const readLocalDraft = (rid: string, tmid?: string) => localStorage.getItem(storageKey(rid, tmid));
type RenderUseDraftProps = {
serverDraft?: string;
threadExists?: boolean;
};
const renderUseDraft = ({
rid = 'rid',
serverDraft,
tmid,
threadExists,
endpointHandler = jest.fn(() => null),
}: {
rid?: string;
tmid?: string;
endpointHandler?: jest.Mock;
} & RenderUseDraftProps = {}) => {
const { result, rerender } = renderHook<ReturnType<typeof useDraft>, RenderUseDraftProps>(
({ serverDraft, threadExists }) => useDraft(rid, serverDraft, tmid, threadExists),
{
initialProps: { serverDraft, threadExists },
wrapper: mockAppRoot().withEndpoint('POST', '/v1/rooms.saveDraft', endpointHandler).build(),
},
);
return { result, rerender, endpointHandler };
};
afterEach(() => {
localStorage.clear();
jest.restoreAllMocks();
});
describe('initialValue', () => {
it('should be an empty string when there is no server draft nor local draft', () => {
const { result } = renderUseDraft();
expect(result.current.initialValue).toBe('');
});
it('should restore the local draft when there is no server draft', () => {
seedLocalDraft('local draft', 'rid');
const { result } = renderUseDraft();
expect(result.current.initialValue).toBe('local draft');
});
it('should restore the server draft when provided', () => {
const { result } = renderUseDraft({ serverDraft: 'server draft' });
expect(result.current.initialValue).toBe('server draft');
});
it('should prefer the server draft over the local draft', () => {
seedLocalDraft('local draft', 'rid');
const { result } = renderUseDraft({ serverDraft: 'server draft' });
expect(result.current.initialValue).toBe('server draft');
});
it('should fall back to the local draft when the server draft is an empty string', () => {
seedLocalDraft('local draft', 'rid');
const { result } = renderUseDraft({ serverDraft: '' });
expect(result.current.initialValue).toBe('local draft');
});
it('should restore the thread-scoped local draft when a tmid is provided', () => {
seedLocalDraft('room draft', 'rid');
seedLocalDraft('thread draft', 'rid', 'tmid');
const { result } = renderUseDraft({ tmid: 'tmid' });
expect(result.current.initialValue).toBe('thread draft');
});
it('should not restore the room draft in a thread composer', () => {
seedLocalDraft('room draft', 'rid');
const { result } = renderUseDraft({ tmid: 'tmid' });
expect(result.current.initialValue).toBe('');
});
it('should remain stable across rerenders even if the server draft changes', () => {
const { result, rerender } = renderUseDraft({ serverDraft: 'first' });
rerender({ serverDraft: 'second' });
expect(result.current.initialValue).toBe('first');
});
});
describe('persistLocal', () => {
it('should write the draft to local storage under the room-scoped key', () => {
const { result } = renderUseDraft();
act(() => {
result.current.persistLocal('typed message');
});
expect(readLocalDraft('rid')).toBe('typed message');
});
it('should write the draft to local storage under the thread-scoped key when a tmid is provided', () => {
const { result } = renderUseDraft({ tmid: 'tmid' });
act(() => {
result.current.persistLocal('typed in thread');
});
expect(readLocalDraft('rid', 'tmid')).toBe('typed in thread');
expect(readLocalDraft('rid')).toBe(null);
});
it('should overwrite a previously persisted draft', () => {
const { result } = renderUseDraft();
act(() => {
result.current.persistLocal('first');
result.current.persistLocal('second');
});
expect(readLocalDraft('rid')).toBe('second');
});
it('should remove the local entry instead of storing an empty string', () => {
seedLocalDraft('local draft', 'rid');
const { result } = renderUseDraft();
act(() => {
result.current.persistLocal('');
});
expect(readLocalDraft('rid')).toBe(null);
});
it('should not call the save draft endpoint', () => {
const { result, endpointHandler } = renderUseDraft();
act(() => {
result.current.persistLocal('typed message');
});
expect(endpointHandler).not.toHaveBeenCalled();
});
});
describe('flushDraft', () => {
it('should not call the save draft endpoint when no draft was persisted', () => {
const { result, endpointHandler } = renderUseDraft();
act(() => {
result.current.flushDraft();
});
expect(endpointHandler).not.toHaveBeenCalled();
});
it('should not flush a draft restored from local storage that was never re-persisted', () => {
seedLocalDraft('restored draft', 'rid');
const { result, endpointHandler } = renderUseDraft();
act(() => {
result.current.flushDraft();
});
expect(endpointHandler).not.toHaveBeenCalled();
});
it('should save the persisted draft to the server', async () => {
const { result, endpointHandler } = renderUseDraft();
act(() => {
result.current.persistLocal('typed message');
result.current.flushDraft();
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(1));
expect(endpointHandler).toHaveBeenCalledWith({ rid: 'rid', draft: 'typed message' });
});
it('should remove the local copy once the draft is saved to the server', async () => {
const { result } = renderUseDraft();
act(() => {
result.current.persistLocal('typed message');
result.current.flushDraft();
});
await waitFor(() => expect(readLocalDraft('rid')).toBe(null));
});
it('should include the tmid when flushing a thread draft', async () => {
const { result, endpointHandler } = renderUseDraft({ tmid: 'tmid' });
act(() => {
result.current.persistLocal('typed in thread');
result.current.flushDraft();
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(1));
expect(endpointHandler).toHaveBeenCalledWith({ rid: 'rid', draft: 'typed in thread', tmid: 'tmid' });
});
it('should flush only the latest persisted draft', async () => {
const { result, endpointHandler } = renderUseDraft();
act(() => {
result.current.persistLocal('first');
result.current.persistLocal('second');
result.current.flushDraft();
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(1));
expect(endpointHandler).toHaveBeenCalledWith({ rid: 'rid', draft: 'second' });
});
it('should flush an empty draft to clear an existing server draft', async () => {
const { result, endpointHandler } = renderUseDraft({ serverDraft: 'server draft' });
act(() => {
result.current.persistLocal('');
result.current.flushDraft();
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(1));
expect(endpointHandler).toHaveBeenCalledWith({ rid: 'rid', draft: '' });
});
it('should not call the endpoint when flushing an empty draft with no existing server draft', () => {
const { result, endpointHandler } = renderUseDraft();
act(() => {
result.current.persistLocal('');
result.current.flushDraft();
});
expect(endpointHandler).not.toHaveBeenCalled();
});
it('should not re-save a restored server draft that was not edited', () => {
const { result, endpointHandler } = renderUseDraft({ serverDraft: 'server draft' });
act(() => {
result.current.persistLocal('server draft');
result.current.flushDraft();
});
expect(endpointHandler).not.toHaveBeenCalled();
});
it('should not flush the same draft twice', async () => {
const { result, endpointHandler } = renderUseDraft();
act(() => {
result.current.persistLocal('typed message');
result.current.flushDraft();
result.current.flushDraft();
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(1));
});
it('should flush again after a new draft is persisted', async () => {
const { result, endpointHandler } = renderUseDraft();
act(() => {
result.current.persistLocal('first');
result.current.flushDraft();
result.current.persistLocal('second');
result.current.flushDraft();
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(2));
expect(endpointHandler).toHaveBeenNthCalledWith(1, { rid: 'rid', draft: 'first' });
expect(endpointHandler).toHaveBeenNthCalledWith(2, { rid: 'rid', draft: 'second' });
});
it('should not save the draft to the server when the thread does not exist', () => {
const { result, endpointHandler } = renderUseDraft({ tmid: 'tmid', threadExists: false });
act(() => {
result.current.persistLocal('typed in deleted thread');
result.current.flushDraft();
});
expect(endpointHandler).not.toHaveBeenCalled();
});
it('should keep the draft in local storage when the thread does not exist', () => {
const { result } = renderUseDraft({ tmid: 'tmid', threadExists: false });
act(() => {
result.current.persistLocal('typed in deleted thread');
result.current.flushDraft();
});
expect(readLocalDraft('rid', 'tmid')).toBe('typed in deleted thread');
});
it('should keep discarding drafts persisted after a flush while the thread does not exist', () => {
const { result, endpointHandler } = renderUseDraft({ tmid: 'tmid', threadExists: false });
act(() => {
result.current.persistLocal('first');
result.current.flushDraft();
result.current.persistLocal('second');
result.current.flushDraft();
});
expect(endpointHandler).not.toHaveBeenCalled();
});
it('should not save the draft when the thread stops existing after mount', () => {
const { result, rerender, endpointHandler } = renderUseDraft({ tmid: 'tmid', threadExists: true });
act(() => {
result.current.persistLocal('typed before thread deletion');
});
rerender({ threadExists: false });
act(() => {
result.current.flushDraft();
});
expect(endpointHandler).not.toHaveBeenCalled();
});
it('should save the draft when the thread starts existing after mount', async () => {
const { result, rerender, endpointHandler } = renderUseDraft({ tmid: 'tmid', threadExists: false });
rerender({ threadExists: true });
act(() => {
result.current.persistLocal('typed in new thread');
result.current.flushDraft();
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(1));
expect(endpointHandler).toHaveBeenCalledWith({ rid: 'rid', draft: 'typed in new thread', tmid: 'tmid' });
});
it('should still save the draft when threadExists is false but there is no tmid', async () => {
const { result, endpointHandler } = renderUseDraft({ threadExists: false });
act(() => {
result.current.persistLocal('typed message');
result.current.flushDraft();
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(1));
expect(endpointHandler).toHaveBeenCalledWith({ rid: 'rid', draft: 'typed message' });
});
it('should flush the given value instead of the persisted draft', async () => {
const { result, endpointHandler } = renderUseDraft();
act(() => {
result.current.persistLocal('typed message');
result.current.flushDraft('edited message');
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(1));
expect(endpointHandler).toHaveBeenCalledWith({ rid: 'rid', draft: 'edited message' });
});
it('should clear the server draft when flushed with an empty value', async () => {
const { result, endpointHandler } = renderUseDraft({ serverDraft: 'server draft' });
act(() => {
result.current.persistLocal('typed message');
result.current.flushDraft('');
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(1));
expect(endpointHandler).toHaveBeenCalledWith({ rid: 'rid', draft: '' });
});
it('should clear the server thread draft when flushed with an empty value', async () => {
const { result, endpointHandler } = renderUseDraft({ tmid: 'tmid', serverDraft: 'server draft' });
act(() => {
result.current.persistLocal('typed in thread');
result.current.flushDraft('');
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(1));
expect(endpointHandler).toHaveBeenCalledWith({ rid: 'rid', draft: '', tmid: 'tmid' });
});
it('should remove the local copy once the draft is cleared on the server', async () => {
const { result } = renderUseDraft({ serverDraft: 'server draft' });
act(() => {
result.current.persistLocal('typed message');
result.current.flushDraft('');
});
await waitFor(() => expect(readLocalDraft('rid')).toBe(null));
});
it('should not call the endpoint when flushing an empty value with no server draft', () => {
const { result, endpointHandler } = renderUseDraft();
act(() => {
result.current.persistLocal('typed message');
result.current.flushDraft('');
result.current.flushDraft('');
});
expect(endpointHandler).not.toHaveBeenCalled();
expect(readLocalDraft('rid')).toBe(null);
});
it('should remove the local copy when the flushed value already matches the server draft', () => {
const { result, endpointHandler } = renderUseDraft({ serverDraft: 'server draft' });
act(() => {
result.current.persistLocal('typed message');
result.current.flushDraft('server draft');
});
expect(endpointHandler).not.toHaveBeenCalled();
expect(readLocalDraft('rid')).toBe(null);
});
it('should keep a draft persisted while the previous save is still in flight', async () => {
let resolveSave: (() => void) | undefined;
const endpointHandler = jest.fn(
() =>
new Promise<null>((resolve) => {
resolveSave = () => resolve(null);
}),
);
const { result } = renderUseDraft({ serverDraft: 'server draft', endpointHandler });
act(() => {
result.current.flushDraft('');
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(1));
act(() => {
result.current.persistLocal('typed while sending');
});
await act(async () => {
resolveSave?.();
});
expect(readLocalDraft('rid')).toBe('typed while sending');
});
it('should keep the local copy when the save fails', async () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
const endpointHandler = jest.fn(() => Promise.reject(new Error('save failed')) as unknown as null);
const { result } = renderUseDraft({ endpointHandler });
await act(async () => {
result.current.persistLocal('typed message');
result.current.flushDraft();
});
expect(warn).toHaveBeenCalledTimes(1);
expect(readLocalDraft('rid')).toBe('typed message');
});
it('should retry a draft whose save failed and drop the local copy once it lands', async () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
const endpointHandler = jest.fn().mockRejectedValueOnce(new Error('save failed')).mockResolvedValueOnce(null);
const { result } = renderUseDraft({ endpointHandler });
await act(async () => {
result.current.persistLocal('typed message');
result.current.flushDraft();
});
expect(readLocalDraft('rid')).toBe('typed message');
await act(async () => {
result.current.persistLocal('typed message');
result.current.flushDraft();
});
expect(warn).toHaveBeenCalledTimes(1);
expect(endpointHandler).toHaveBeenCalledTimes(2);
expect(endpointHandler).toHaveBeenNthCalledWith(2, { rid: 'rid', draft: 'typed message' });
expect(readLocalDraft('rid')).toBe(null);
});
it('should not flush the persisted draft after it was superseded by an explicit flush', async () => {
const { result, endpointHandler } = renderUseDraft();
act(() => {
result.current.persistLocal('typed message');
result.current.flushDraft('sent message');
result.current.flushDraft();
});
await waitFor(() => expect(endpointHandler).toHaveBeenCalledTimes(1));
expect(endpointHandler).toHaveBeenCalledWith({ rid: 'rid', draft: 'sent message' });
});
});
describe('referential stability', () => {
it('should keep persistLocal and flushDraft identities across rerenders', () => {
const { result, rerender } = renderUseDraft();
const { persistLocal, flushDraft } = result.current;
rerender({});
expect(result.current.persistLocal).toBe(persistLocal);
expect(result.current.flushDraft).toBe(flushDraft);
});
});

View File

@@ -0,0 +1,80 @@
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { useCallback, useEffect, useRef } from 'react';
export const useDraft = (rid: string, serverDraft?: string, tmid?: string, threadExists = true) => {
const storageKey = `messagebox_${rid}${tmid ? `-${tmid}` : ''}`;
const saveDraft = useEndpoint('POST', '/v1/rooms.saveDraft');
const setLocalDraft = useCallback(
(value?: string) => {
if (value) {
localStorage.setItem(storageKey, value);
} else {
localStorage.removeItem(storageKey);
}
},
[storageKey],
);
const initialValueRef = useRef(serverDraft || localStorage.getItem(storageKey) || '');
const draftRef = useRef<string | null>(null);
const threadExistsRef = useRef(threadExists);
const serverValueRef = useRef(serverDraft ?? '');
useEffect(() => {
threadExistsRef.current = threadExists;
}, [threadExists]);
const persistLocal = useCallback(
(value: string) => {
draftRef.current = value;
setLocalDraft(value);
},
[setLocalDraft],
);
const flushDraft = useCallback(
(value?: string) => {
const draft = value ?? draftRef.current;
if (draft === null) {
return;
}
draftRef.current = null;
if (tmid && !threadExistsRef.current && draft) {
return;
}
if (draft === serverValueRef.current) {
setLocalDraft();
return;
}
const previousServerValue = serverValueRef.current;
serverValueRef.current = draft;
void saveDraft({ rid, draft, ...(tmid && { tmid }) })
.then(() => {
if (draftRef.current === null) {
setLocalDraft();
}
})
.catch((error) => {
if (serverValueRef.current === draft) {
serverValueRef.current = previousServerValue;
}
console.warn(error);
});
},
[saveDraft, rid, tmid, setLocalDraft],
);
return {
initialValue: initialValueRef.current,
persistLocal,
flushDraft,
};
};

View File

@@ -0,0 +1,34 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const useMediaActionTitle = (
media: 'audio' | 'video',
isPermissionDenied: boolean,
isFileUploadEnabled: boolean,
isMediaEnabled: boolean,
isAllowed: boolean,
) => {
const { t } = useTranslation();
const getMediaActionTitle = useMemo(() => {
if (isPermissionDenied) {
return media === 'audio' ? t('Microphone_access_not_allowed') : t('Camera_access_not_allowed');
}
if (!isFileUploadEnabled) {
return t('File_Upload_Disabled');
}
if (!isMediaEnabled) {
return media === 'audio' ? t('Message_Audio_Recording_Disabled') : t('Message_Video_Recording_Disabled');
}
if (!isAllowed) {
return t('error-not-allowed');
}
return media === 'audio' ? t('Audio_message') : t('Video_message');
}, [media, isPermissionDenied, isFileUploadEnabled, isMediaEnabled, isAllowed, t]);
return getMediaActionTitle;
};

View File

@@ -0,0 +1,59 @@
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { useQuery, useQueryClient } from '@tanstack/react-query';
type MediaDevices = 'camera' | 'microphone';
const getDeviceKind = (name: MediaDevices): MediaDeviceKind => {
switch (name) {
case 'camera':
return 'videoinput';
case 'microphone':
return 'audioinput';
}
};
export const useMediaPermissions = (name: MediaDevices): [isPermissionDenied: boolean, setIsPermissionDenied: (state: boolean) => void] => {
const queryClient = useQueryClient();
const queryKey = ['media-permissions', name];
const setIsPermissionDenied = useStableCallback((isDenied: boolean) => {
queryClient.setQueryData(queryKey, isDenied);
});
const { data } = useQuery({
queryKey,
queryFn: async () => {
if (navigator.permissions) {
try {
const permissionStatus = await navigator.permissions.query({ name: name as PermissionName });
permissionStatus.onchange = (): void => {
queryClient.setQueryData(queryKey, permissionStatus.state === 'denied');
};
return permissionStatus.state === 'denied';
} catch (error) {
console.warn(error);
}
}
if (!navigator.mediaDevices?.enumerateDevices) {
return true;
}
try {
if (!(await navigator.mediaDevices.enumerateDevices()).some(({ kind }) => kind === getDeviceKind(name))) {
return true;
}
} catch (error) {
console.warn(error);
}
},
refetchOnMount: true,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
staleTime: Infinity,
placeholderData: false,
});
return [Boolean(data), setIsPermissionDenied];
};

View File

@@ -0,0 +1,64 @@
import type { Ref } from 'react';
import { useCallback, useEffect, useRef } from 'react';
/**
* if the user is types outside the message box and its not actually typing in any input field
* then the message box should be focused
* @returns callbackRef to bind the logic to the message box
*/
export const useMessageBoxAutoFocus = (enabled: boolean): Ref<HTMLElement> => {
const ref = useRef<HTMLElement>(undefined);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const { current: node } = ref;
const { target } = e;
if (!target) {
return;
}
if (target === node) {
return;
}
if (!((e.keyCode > 45 && e.keyCode < 91) || e.keyCode === 8)) {
return;
}
if (/input|textarea|select/i.test((target as HTMLElement).tagName)) {
return;
}
if (e.ctrlKey === true || e.metaKey === true) {
return;
}
node?.focus();
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, []);
return useCallback(
(node: HTMLElement | null) => {
if (!node) {
return;
}
ref.current = node;
if (!enabled) {
return;
}
if (ref.current) {
ref.current.focus();
}
},
[enabled, ref],
);
};

View File

@@ -0,0 +1,18 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { isDirectMessageRoom } from '@rocket.chat/core-typings';
import { useRoomName } from '../../../../../hooks/useRoomName';
export const useMessageBoxPlaceholder = (placeholder: string, room?: IRoom) => {
if (!room) {
throw new Error('In order to get the placeholder a `room` must be provided');
}
const roomName = useRoomName(room);
if (isDirectMessageRoom(room)) {
return `${placeholder} @${roomName}`;
}
return `${placeholder} #${roomName}`;
};