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,224 @@
import { VisuallyHidden } from '@react-aria/visually-hidden';
import { css } from '@rocket.chat/css-in-js';
import type { SelectOption } from '@rocket.chat/fuselage';
import { Accordion, AccordionItem, Box, Button, ButtonGroup } from '@rocket.chat/fuselage';
import {
FieldDescription,
Field,
FieldGroup,
FieldHint,
FieldLabel,
FieldRow,
RadioButton,
Select,
ToggleSwitch,
} from '@rocket.chat/fuselage-forms';
import { ExternalLink, Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '@rocket.chat/ui-client';
import { useTranslation, useToastMessageDispatch, useEndpoint, useSetting, useLocationHash } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import { useId, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { fontSizes } from './fontSizes';
import type { AccessibilityPreferencesData } from './hooks/useAcessibilityPreferencesValues';
import { useAccessiblityPreferencesValues } from './hooks/useAcessibilityPreferencesValues';
import { useCreateFontStyleElement } from './hooks/useCreateFontStyleElement';
import { themeItems as themes } from './themeItems';
import { getDirtyFields } from '../../../lib/getDirtyFields';
import { links } from '../../../lib/links';
const AccessibilityPage = () => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const preferencesValues = useAccessiblityPreferencesValues();
const createFontStyleElement = useCreateFontStyleElement();
const displayRolesEnabled = useSetting('UI_DisplayRoles');
const shouldExpand = useLocationHash().length > 1;
const timeFormatOptions = useMemo(
(): SelectOption[] => [
['0', t('Default')],
['1', t('12_Hour')],
['2', t('24_Hour')],
],
[t],
);
const pageFormId = useId();
const linkListId = useId();
const {
formState: { isDirty, dirtyFields, isSubmitting },
handleSubmit,
control,
reset,
watch,
} = useForm({
defaultValues: preferencesValues,
});
const currentData = watch();
const setUserPreferencesEndpoint = useEndpoint('POST', '/v1/users.setPreferences');
const setPreferencesAction = useMutation({
mutationFn: setUserPreferencesEndpoint,
onSuccess: () => dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }),
onError: (error) => dispatchToastMessage({ type: 'error', message: error }),
onSettled: (_data, _error, { data: { fontSize } }) => {
reset(currentData);
dirtyFields.fontSize && fontSize && createFontStyleElement(fontSize);
},
});
const handleSaveData = (formData: AccessibilityPreferencesData) => {
const data = getDirtyFields(formData, dirtyFields);
setPreferencesAction.mutateAsync({ data });
};
return (
<Page>
<PageHeader title={t('Accessibility_and_Appearance')} />
<PageScrollableContentWithShadow>
<Box
is='form'
id={pageFormId}
onSubmit={handleSubmit(handleSaveData)}
maxWidth='x600'
width='full'
alignSelf='center'
marginBlock={40}
marginInline={36}
>
<Box fontScale='p1' marginBlockEnd={24}>
<Box paddingBlock={16} is='p'>
{t('Accessibility_activation')}
</Box>
<p id={linkListId}>{t('Learn_more_about_accessibility')}</p>
<ul aria-labelledby={linkListId}>
<li>
<ExternalLink to={links.go.accessibilityStatement}>{t('Accessibility_statement')}</ExternalLink>
</li>
<li>
<ExternalLink to={links.go.glossary}>{t('Glossary_of_simplified_terms')}</ExternalLink>
</li>
<li>
<ExternalLink to={links.go.accessibilityAndAppearance}>{t('Accessibility_feature_documentation')}</ExternalLink>
</li>
</ul>
</Box>
<Accordion>
<AccordionItem defaultExpanded={true} title={t('Theme')}>
{themes.map(({ id, title, description }, index) => {
return (
<Field
key={id}
paddingBlockEnd={themes.length - 1 ? undefined : 'x28'}
paddingBlockStart={index === 0 ? undefined : 'x28'}
>
<FieldRow>
<FieldLabel display='flex' alignItems='center'>
{t(title)}
</FieldLabel>
<Controller
control={control}
name='themeAppearence'
render={({ field: { value, onChange, ...field } }) => (
<RadioButton id={id} {...field} onChange={() => onChange(id)} checked={value === id} />
)}
/>
</FieldRow>
<FieldHint marginBlockStart={12} style={{ whiteSpace: 'break-spaces' }}>
{t(description)}
</FieldHint>
</Field>
);
})}
</AccordionItem>
<AccordionItem defaultExpanded={shouldExpand} title={t('Adjustable_layout')}>
<FieldGroup>
<VisuallyHidden>
<legend>{t('Adjustable_layout')}</legend>
</VisuallyHidden>
<Field>
<FieldLabel marginBlockEnd={12}>{t('Font_size')}</FieldLabel>
<FieldRow>
<Controller control={control} name='fontSize' render={({ field }) => <Select {...field} options={fontSizes(t)} />} />
</FieldRow>
<FieldDescription marginBlock={12}>{t('Adjustable_font_size_description')}</FieldDescription>
</Field>
<Field>
<FieldRow>
<FieldLabel>{t('Mentions_with_@_symbol')}</FieldLabel>
<Controller
control={control}
name='mentionsWithSymbol'
render={({ field: { value, ...field } }) => <ToggleSwitch {...field} checked={value} />}
/>
</FieldRow>
<FieldDescription
className={css`
white-space: break-spaces;
`}
marginBlock={12}
>
{t('Mentions_with_@_symbol_description')}
</FieldDescription>
</Field>
<Field id='clockMode'>
<FieldLabel>{t('Message_TimeFormat')}</FieldLabel>
<FieldRow>
<Controller
name='clockMode'
control={control}
render={({ field: { value, ...field } }) => <Select {...field} value={`${value}`} options={timeFormatOptions} />}
/>
</FieldRow>
</Field>
<Field id='hideUsernames'>
<FieldRow>
<FieldLabel>{t('Show_usernames')}</FieldLabel>
<Controller
name='hideUsernames'
control={control}
render={({ field: { value, onChange, ...field } }) => (
<ToggleSwitch {...field} checked={!value} onChange={(e) => onChange(!(e.target as HTMLInputElement).checked)} />
)}
/>
</FieldRow>
<FieldDescription>{t('Show_or_hide_the_username_of_message_authors')}</FieldDescription>
</Field>
{displayRolesEnabled && (
<Field id='hideRoles'>
<FieldRow>
<FieldLabel>{t('Show_roles')}</FieldLabel>
<Controller
name='hideRoles'
control={control}
render={({ field: { value, onChange, ...field } }) => (
<ToggleSwitch {...field} checked={!value} onChange={(e) => onChange(!(e.target as HTMLInputElement).checked)} />
)}
/>
</FieldRow>
<FieldDescription>{t('Show_or_hide_the_user_roles_of_message_authors')}</FieldDescription>
</Field>
)}
</FieldGroup>
</AccordionItem>
</Accordion>
</Box>
</PageScrollableContentWithShadow>
<PageFooter isDirty={isDirty}>
<ButtonGroup>
<Button onClick={() => reset(preferencesValues)}>{t('Cancel')}</Button>
<Button primary disabled={!isDirty} loading={isSubmitting} form={pageFormId} type='submit'>
{t('Save_changes')}
</Button>
</ButtonGroup>
</PageFooter>
</Page>
);
};
export default AccessibilityPage;

View File

@@ -0,0 +1,12 @@
import type { SelectOption } from '@rocket.chat/fuselage';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
type TranslationFunction = (key: TranslationKey) => string;
export const fontSizes = (t: TranslationFunction): SelectOption[] => [
[`${(14 / 16) * 100}%`, t('Font_Small')],
['100%', t('Font_Default')],
[`${(18 / 16) * 100}%`, t('Font_Medium')],
[`${(20 / 16) * 100}%`, t('Font_Large')],
[`${(24 / 16) * 100}%`, t('Font_Extra_large')],
];

View File

@@ -0,0 +1,31 @@
import type { ThemePreference } from '@rocket.chat/core-typings';
import type { FontSize } from '@rocket.chat/rest-typings';
import { useUserPreference } from '@rocket.chat/ui-contexts';
export type AccessibilityPreferencesData = {
themeAppearence?: ThemePreference;
fontSize?: FontSize;
fontSizePreference?: FontSize;
mentionsWithSymbol?: boolean;
clockMode?: 0 | 1 | 2;
hideUsernames?: boolean;
hideRoles?: boolean;
};
export const useAccessiblityPreferencesValues = (): AccessibilityPreferencesData => {
const themeAppearence = useUserPreference<ThemePreference>('themeAppearence') || 'auto';
const fontSize = useUserPreference<FontSize>('fontSize') || '100%';
const mentionsWithSymbol = useUserPreference<boolean>('mentionsWithSymbol') || false;
const clockMode = useUserPreference<0 | 1 | 2>('clockMode') ?? 0;
const hideUsernames = useUserPreference<boolean>('hideUsernames');
const hideRoles = useUserPreference<boolean>('hideRoles');
return {
themeAppearence,
fontSize,
mentionsWithSymbol,
clockMode,
hideUsernames,
hideRoles,
};
};

View File

@@ -0,0 +1,20 @@
import { useCallback } from 'react';
const createStyleElement = (id: string) => {
const styleElement = document.getElementById(id);
if (styleElement) {
return styleElement;
}
const newStyleElement = document.createElement('style');
newStyleElement.setAttribute('id', id);
return newStyleElement;
};
export const useCreateFontStyleElement = (): ((fontSize: string) => void) => {
return useCallback((fontSize: string) => {
const styleElement = createStyleElement('rcx-font-size');
const css = `html { font-size: ${fontSize}; }`;
styleElement.innerHTML = css;
document.head.appendChild(styleElement);
}, []);
};

View File

@@ -0,0 +1,29 @@
import type { TranslationKey } from '@rocket.chat/ui-contexts';
type ThemeItem = {
id: string;
title: TranslationKey;
description: TranslationKey;
};
export const themeItems: ThemeItem[] = [
{
id: 'light',
title: 'Theme_light',
description: 'Theme_light_description',
},
{
id: 'dark',
title: 'Theme_dark',
description: 'Theme_dark_description',
},
{
id: 'high-contrast',
title: 'Theme_high_contrast',
description: 'Theme_high_contrast_description',
},
{
id: 'auto',
title: 'Theme_match_system',
description: 'Theme_match_system_description',
},
];