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
6 changes: 6 additions & 0 deletions .changeset/add-tagalog-language.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/meteor': patch
'@rocket.chat/i18n': minor
---

Added Tagalog (`tl`) as a selectable interface language. It appears in the user's **Account → Preferences → Localization → Language** dropdown; interface strings fall back to English until translations are contributed (same approach as other not-yet-translated locales).
6 changes: 6 additions & 0 deletions .changeset/all-readers-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": minor
---

Shows a confirmation modal when switching attribute store setting
5 changes: 5 additions & 0 deletions .changeset/few-lies-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes an issue with embedded layout where users were able to receive VideoConf calls, causing the application to crash
6 changes: 6 additions & 0 deletions .changeset/tasty-deserts-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/core-typings': patch
'@rocket.chat/meteor': patch
---

Fixes an issue where ui crashes when message attachment fields contains non-string `value` field
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const DefaultAttachment = (attachment: DefaultAttachmentProps) => {
<MarkdownText variant='inline' parseEmoji content={title.replace(/(.*)/g, (line: string) => `${line} `)} />
) : null,
value: value ? (
<MarkdownText variant='inline' parseEmoji content={value.replace(/(.*)/g, (line: string) => `${line} `)} />
<MarkdownText variant='inline' parseEmoji content={String(value).replace(/(.*)/g, (line: string) => `${line} `)} />
) : null,
};
})}
Expand Down
72 changes: 72 additions & 0 deletions apps/meteor/client/hooks/useShowSettingAlerts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { Box, Callout } from '@rocket.chat/fuselage';
import { GenericModal } from '@rocket.chat/ui-client';
import { useSetModal } from '@rocket.chat/ui-contexts';
import { useCallback } from 'react';
import { Trans, useTranslation } from 'react-i18next';

export const useShowSettingAlerts = () => {
const { t, i18n } = useTranslation();
const setModal = useSetModal();

const showAlerts = useCallback(
(persistedSettingsWithAlert: ISetting[]) => {
return new Promise<boolean>((resolve) => {
setModal(
<GenericModal
variant='danger'
icon={null}
maxHeight='x600'
title={t('Confirm_settings_change')}
confirmText={t('Save_changes')}
cancelText={t('Cancel')}
onConfirm={() => {
resolve(true);
setModal(null);
}}
onCancel={() => {
resolve(false);
return setModal(null);
}}
onClose={() => {
resolve(false);
return setModal(null);
}}
>
{persistedSettingsWithAlert.map(({ _id, i18nLabel, alert }) => {
if (!alert) {
return null;
}

const labelText = (i18n.exists(i18nLabel) && t(i18nLabel)) || (i18n.exists(_id) && t(_id)) || i18nLabel || _id;

return (
<Box key={_id} mbe={24}>
<Box fontScale='h4' mbe={8}>
{labelText}
</Box>
<Callout type='warning'>
<Trans
i18nKey={i18n.exists(alert) ? alert : undefined}
defaults={alert}
components={{
b: <b />,
strong: <strong />,
br: <br />,
ul: <ul />,
li: <li />,
}}
/>
</Callout>
</Box>
);
})}
</GenericModal>,
);
});
},
[t, i18n, setModal],
);

return showAlerts;
};
4 changes: 4 additions & 0 deletions apps/meteor/client/lib/VideoConfManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter<Vide
}

public updateUser(userId: string | null, isLoggingIn: boolean, isConnected: boolean): void {
if (userId === this.userId && !isLoggingIn && !isConnected) {
return;
}

this.debugLog(`[VideoConf] Logged user or connection status has changed.`);

if (this.userId) {
Expand Down
48 changes: 24 additions & 24 deletions apps/meteor/client/providers/MeteorProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ type MeteorProviderProps = {
const MeteorProvider = ({ children }: MeteorProviderProps) => (
<ServerProvider>
<RouterProvider>
<SettingsProvider>
<TranslationProvider>
<SessionProvider>
<TooltipProvider>
<ToastMessagesProvider>
<AvatarUrlProvider>
<UserProvider>
<LayoutProvider>
<AuthenticationProvider>
<CustomSoundProvider>
<DeviceProvider>
<ModalProvider>
<ModalProvider>
<SettingsProvider>
<TranslationProvider>
<SessionProvider>
<TooltipProvider>
<ToastMessagesProvider>
<AvatarUrlProvider>
<UserProvider>
<LayoutProvider>
<AuthenticationProvider>
<CustomSoundProvider>
<DeviceProvider>
<AuthorizationProvider>
<EmojiPickerProvider>
<OmnichannelRoomIconProvider>
Expand All @@ -56,18 +56,18 @@ const MeteorProvider = ({ children }: MeteorProviderProps) => (
</OmnichannelRoomIconProvider>
</EmojiPickerProvider>
</AuthorizationProvider>
</ModalProvider>
</DeviceProvider>
</CustomSoundProvider>
</AuthenticationProvider>
</LayoutProvider>
</UserProvider>
</AvatarUrlProvider>
</ToastMessagesProvider>
</TooltipProvider>
</SessionProvider>
</TranslationProvider>
</SettingsProvider>
</DeviceProvider>
</CustomSoundProvider>
</AuthenticationProvider>
</LayoutProvider>
</UserProvider>
</AvatarUrlProvider>
</ToastMessagesProvider>
</TooltipProvider>
</SessionProvider>
</TranslationProvider>
</SettingsProvider>
</ModalProvider>
</RouterProvider>
</ServerProvider>
);
Expand Down
22 changes: 20 additions & 2 deletions apps/meteor/client/providers/SettingsProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter';
import { isTruthy } from '@rocket.chat/tools';
import type { SettingsContextQuery, SettingsContextValue } from '@rocket.chat/ui-contexts';
import { SettingsContext, useAtLeastOnePermission, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQueryClient } from '@tanstack/react-query';
import type { ReactNode } from 'react';
import { useCallback, useMemo } from 'react';

import { PublicSettingsCachedStore, PrivateSettingsCachedStore } from '../cachedStores';
import { useShowSettingAlerts } from '../hooks/useShowSettingAlerts';
import { PrivateCachedStore } from '../lib/cachedStores/CachedStore';
import { applyQueryOptions } from '../lib/cachedStores/applyQueryOptions';

Expand Down Expand Up @@ -96,19 +98,35 @@ const SettingsProvider = ({ children }: SettingsProviderProps) => {

const queryClient = useQueryClient();

const showAlerts = useShowSettingAlerts();

const saveSettings = useEndpoint('POST', '/v1/settings');
const dispatch = useCallback(
async (changes: Partial<ISetting>[]) => {
async (changes: Partial<ISetting>[], onSaved?: () => void) => {
const changedSettingIds = changes.map((s) => s._id).filter(isTruthy);
const alerts = cachedCollection.store
.getState()
.filter((setting) => Boolean(setting.alert && changedSettingIds.includes(setting._id)));
// FIXME: This is a temporary solution to invalidate queries when settings change
changes.forEach((val) => {
if (val._id === 'Enterprise_License') {
queryClient.invalidateQueries({ queryKey: ['licenses'] });
}
});

if (alerts.length) {
const accepted = await showAlerts(alerts);

if (!accepted) {
return;
}
}

await saveSettings({ settings: changes as Pick<ISetting, '_id' | 'value'>[] });

onSaved?.();
},
[queryClient, saveSettings],
[queryClient, saveSettings, showAlerts, cachedCollection.store],
);

const contextValue = useMemo<SettingsContextValue>(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, screen, waitFor } from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import SettingField from './SettingField';
Expand Down Expand Up @@ -31,6 +31,7 @@ jest.mock('@rocket.chat/core-typings', () => ({
describe('SettingField', () => {
beforeEach(() => {
jest.useFakeTimers();
dispatchMock.mockClear();
});

afterEach(() => {
Expand All @@ -55,4 +56,23 @@ describe('SettingField', () => {
expect(dispatchMock).toHaveBeenCalled();
});
});

it('should dispatch when the setting has an alert (confirmation is handled by the settings dispatch)', async () => {
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });

render(<SettingField settingId='Test_Setting' />, {
wrapper: mockAppRoot()
.wrap((children) => <EditableSettingsProvider>{children}</EditableSettingsProvider>)
.withSetting('Test_Setting', false, { ...settingStructure, alert: 'Test_Setting_Alert' })
.build(),
});

await user.click(screen.getByRole('checkbox'));

await act(async () => {
jest.runOnlyPendingTimers();
});

expect(dispatchMock).toHaveBeenCalledWith([{ _id: 'Test_Setting', value: true }]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ const SettingsGroupPage = ({
}

try {
await dispatch(changes);
dispatchToastMessage({ type: 'success', message: t('Settings_updated') });
await dispatch(changes, () => dispatchToastMessage({ type: 'success', message: t('Settings_updated') }));
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEmbeddedLayout } from '@rocket.chat/ui-client';
import { useConnectionStatus, useIsLoggingIn } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';

Expand All @@ -6,8 +7,10 @@ import { VideoConfManager } from '../../../../lib/VideoConfManager';
export const useUpdateVideoConfUser = (userId: string) => {
const { connected } = useConnectionStatus();
const isLoggingIn = useIsLoggingIn();
const embeddedLayout = useEmbeddedLayout();

useEffect(() => {
VideoConfManager.updateUser(userId, isLoggingIn, connected);
}, [userId, isLoggingIn, connected]);
// Videconf should not be available in embedded layout
VideoConfManager.updateUser(embeddedLayout ? null : userId, isLoggingIn, connected);
}, [userId, isLoggingIn, connected, embeddedLayout]);
};
22 changes: 15 additions & 7 deletions apps/meteor/ee/server/hooks/abac/scopeAdminRoomsForAbac.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import { Abac } from '@rocket.chat/core-services';
import type { IRoom, IRoomAbacRedaction } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { Users } from '@rocket.chat/models';

import { scopeAdminRoomsForAbac } from '../../../../app/api/server/lib/scopeAdminRoomsForAbac';
import { isABACManagedRoom } from '../../../../app/authorization/server/lib/isABACManagedRoom';

const redact = <T extends Pick<IRoom, 't' | 'abacAttributes'>>(rooms: T[]): Array<T & IRoomAbacRedaction> =>
rooms.map((room) => (isABACManagedRoom(room) ? { ...room, abacAttributes: [], abacAttributesRedacted: true } : room));

scopeAdminRoomsForAbac.patch(async (next, rooms, uid) => {
const managed = License.hasModule('abac') ? rooms.filter(isABACManagedRoom) : [];
if (!managed.length) {
return next(rooms, uid);
}

const user = await Users.findOneById(uid, { projection: { _id: 1, username: 1, name: 1 } });
if (!user) {
return rooms.map((room) => (isABACManagedRoom(room) ? { ...room, abacAttributes: [], abacAttributesRedacted: true } : room));
}
try {
const user = await Users.findOneById(uid, { projection: { _id: 1, username: 1, name: 1 } });
if (!user) {
return redact(rooms);
}

const scoped = await Abac.scopeRoomsForAdmin(managed, user);
const scopedById = new Map(scoped.map((room) => [room._id, room]));
return rooms.map((room) => scopedById.get(room._id) ?? room);
const scoped = await Abac.scopeRoomsForAdmin(managed, user);
const scopedById = new Map(scoped.map((room) => [room._id, room]));
return rooms.map((room) => scopedById.get(room._id) ?? room);
} catch (err) {
return redact(rooms);
}
});
2 changes: 1 addition & 1 deletion apps/meteor/ee/server/services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"ws": "~8.21.0"
},
"devDependencies": {
"@rocket.chat/icons": "~0.47.0",
"@rocket.chat/icons": "^0.48.0",
"@types/cookie": "^0.5.4",
"@types/cookie-parser": "^1.4.10",
"@types/ejson": "^2.2.2",
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/ee/server/settings/abac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function addSettings(): Promise<void> {
{ key: 'local', i18nLabel: 'ABAC_PDP_Type_Local' },
{ key: 'virtru', i18nLabel: 'ABAC_PDP_Type_Virtru' },
],
alert: 'ABAC_PDP_Type_Switch_Alert',
enableQuery: abacEnabledQuery,
});
await this.add('ABAC_Attribute_Store', 'local', {
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
"@rocket.chat/gazzodown": "workspace:^",
"@rocket.chat/http-router": "workspace:^",
"@rocket.chat/i18n": "workspace:^",
"@rocket.chat/icons": "~0.47.0",
"@rocket.chat/icons": "^0.48.0",
"@rocket.chat/instance-status": "workspace:^",
"@rocket.chat/jwt": "workspace:^",
"@rocket.chat/layout": "^0.34.3",
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/server/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ if (false) {
import('@rocket.chat/i18n/dist/resources/sv.i18n.json');
import('@rocket.chat/i18n/dist/resources/ta-IN.i18n.json');
import('@rocket.chat/i18n/dist/resources/th-TH.i18n.json');
import('@rocket.chat/i18n/dist/resources/tl.i18n.json');
import('@rocket.chat/i18n/dist/resources/tr.i18n.json');
import('@rocket.chat/i18n/dist/resources/ug.i18n.json');
import('@rocket.chat/i18n/dist/resources/uk.i18n.json');
Expand Down
Loading
Loading