From fd8abff45debfa24f62fd8decc886fa85dfa37b0 Mon Sep 17 00:00:00 2001 From: Jean Brito Date: Fri, 26 Jun 2026 10:06:30 -0300 Subject: [PATCH 1/3] feat(supportedVersions): role-targeted expiration messages Honor the optional `roles` field on supportedVersions messages so a version-expiration warning can be restricted to specific roles (e.g. workspace admins) instead of every logged-in user. - TSVMessage.roles?: string[] - messageMatchesUserRoles filter threaded through getMessage and both checkSupportedVersions branches (normal + enforcement) - source current user roles from redux login state at the saga call site - tests: getMessage role filtering + checkSupportedVersions targeting Mobile side of Rocket.Chat.Electron#3371. No role-acquisition plumbing needed (bridge/REST/reducer) since mobile already has user roles natively. --- app/definitions/IServer.ts | 8 ++ .../methods/checkSupportedVersions.test.ts | 92 ++++++++++++++++++- app/lib/methods/checkSupportedVersions.ts | 28 +++++- app/sagas/selectServer.ts | 4 +- 4 files changed, 125 insertions(+), 7 deletions(-) diff --git a/app/definitions/IServer.ts b/app/definitions/IServer.ts index 418b11f1de3..826ac45540a 100644 --- a/app/definitions/IServer.ts +++ b/app/definitions/IServer.ts @@ -14,6 +14,14 @@ export type TSVMessage = { subtitle?: string; description?: string; type: 'info' | 'alert' | 'error'; + /** + * Roles allowed to see this message. When omitted (or empty), the message is + * shown to every user. When present, the message is only shown to users whose + * roles intersect this list (e.g. `['admin']` targets workspace admins only). + * Clients that predate this field ignore it and keep showing the message to + * everyone, so the field is backward compatible. + */ + roles?: string[]; params?: Record; link: string; }; diff --git a/app/lib/methods/checkSupportedVersions.test.ts b/app/lib/methods/checkSupportedVersions.test.ts index e3bd0debfab..4dfa63cba76 100644 --- a/app/lib/methods/checkSupportedVersions.test.ts +++ b/app/lib/methods/checkSupportedVersions.test.ts @@ -1,4 +1,4 @@ -import { type ISupportedVersionsData } from '../../definitions'; +import { type ISupportedVersionsData, type TSVMessage } from '../../definitions'; import { checkSupportedVersions, getMessage } from './checkSupportedVersions'; const MOCK_I18N = { @@ -518,4 +518,94 @@ describe('getMessage', () => { link: 'Docs page' }); }); + + describe('role targeting', () => { + const buildMessages = (roles?: string[]): TSVMessage[] => [ + { + remainingDays: 15, + title: 'targeted', + subtitle: 'subtitle_token', + description: 'description_token', + type: 'info', + ...(roles ? { roles } : {}), + link: 'Docs page' + } + ]; + + test('shows a message with no roles to every user', () => { + expect( + getMessage({ messages: buildMessages(), expiration: '2023-04-10T00:00:00.000Z', userRoles: ['user'] }) + ).toMatchObject({ title: 'targeted' }); + }); + + test('shows a role-targeted message when the user has the role', () => { + expect( + getMessage({ messages: buildMessages(['admin']), expiration: '2023-04-10T00:00:00.000Z', userRoles: ['admin', 'user'] }) + ).toMatchObject({ title: 'targeted' }); + }); + + test('hides a role-targeted message from users without the role', () => { + expect( + getMessage({ messages: buildMessages(['admin']), expiration: '2023-04-10T00:00:00.000Z', userRoles: ['user'] }) + ).toBeUndefined(); + }); + + test('hides a role-targeted message when user roles are unknown', () => { + expect(getMessage({ messages: buildMessages(['admin']), expiration: '2023-04-10T00:00:00.000Z' })).toBeUndefined(); + }); + }); +}); + +describe('checkSupportedVersions role targeting', () => { + const buildSupportedVersions = (roles?: string[]): ISupportedVersionsData => ({ + timestamp: TODAY, + enforcementStartDate: TODAY, + messages: [ + { + remainingDays: 15, + title: 'targeted', + subtitle: 'subtitle_token', + description: 'description_token', + type: 'info', + ...(roles ? { roles } : {}), + link: 'Docs page' + } + ], + i18n: MOCK_I18N, + versions: [ + { + version: '1.4.0', + expiration: '2023-04-10T00:00:00.000Z' + } + ] + }); + + test('shows a role-targeted message when the user has the role', () => { + expect( + checkSupportedVersions({ + supportedVersions: buildSupportedVersions(['admin']), + serverVersion: '1.4.0', + userRoles: ['admin', 'user'] + }) + ).toMatchObject({ status: 'warn', message: { title: 'targeted' } }); + }); + + test('hides a role-targeted message from users without the role', () => { + const result = checkSupportedVersions({ + supportedVersions: buildSupportedVersions(['admin']), + serverVersion: '1.4.0', + userRoles: ['user'] + }); + expect(result.status).toBe('supported'); + expect(result.message).toBeUndefined(); + }); + + test('hides a role-targeted message when user roles are unknown', () => { + const result = checkSupportedVersions({ + supportedVersions: buildSupportedVersions(['admin']), + serverVersion: '1.4.0' + }); + expect(result.status).toBe('supported'); + expect(result.message).toBeUndefined(); + }); }); diff --git a/app/lib/methods/checkSupportedVersions.ts b/app/lib/methods/checkSupportedVersions.ts index 8f98b1dc950..9cef290c196 100644 --- a/app/lib/methods/checkSupportedVersions.ts +++ b/app/lib/methods/checkSupportedVersions.ts @@ -5,17 +5,32 @@ import dayjs from '../dayjs'; import { type ISupportedVersionsData, type TSVDictionary, type TSVMessage, type TSVStatus } from '../../definitions'; import builtInSupportedVersions from '../../../app-supportedversions.json'; +const messageMatchesUserRoles = (message: TSVMessage, userRoles?: string[]): boolean => { + // No targeting on the message → show to everyone (default behavior). + if (!message.roles?.length) { + return true; + } + // Targeting set but user roles unknown → don't show, honoring the restriction. + if (!userRoles?.length) { + return false; + } + return message.roles.some(role => userRoles.includes(role)); +}; + export const getMessage = ({ messages, - expiration + expiration, + userRoles }: { messages?: TSVMessage[]; expiration?: string; + userRoles?: string[]; }): TSVMessage | undefined => { if (!messages?.length || !expiration || dayjs(expiration).diff(new Date(), 'days') < 0) { return; } - const sortedMessages = messages.sort((a, b) => a.remainingDays - b.remainingDays); + const eligibleMessages = messages.filter(message => messageMatchesUserRoles(message, userRoles)); + const sortedMessages = eligibleMessages.sort((a, b) => a.remainingDays - b.remainingDays); return sortedMessages.find(({ remainingDays }) => dayjs(expiration).diff(new Date(), 'hours') <= remainingDays * 24); }; @@ -31,10 +46,12 @@ const getStatus = ({ expiration, message }: { expiration?: string; message?: TSV export const checkSupportedVersions = function ({ supportedVersions, - serverVersion + serverVersion, + userRoles }: { supportedVersions?: ISupportedVersionsData; serverVersion: string; + userRoles?: string[]; }): { status: TSVStatus; message?: TSVMessage; @@ -56,14 +73,15 @@ export const checkSupportedVersions = function ({ const messages = exception?.messages || (exception ? sv.exceptions?.messages : undefined) || versionInfo?.messages || sv.messages; const expiration = exception?.expiration || versionInfo?.expiration; - const message = getMessage({ messages, expiration }); + const message = getMessage({ messages, expiration, userRoles }); const status = getStatus({ message, expiration }); // TODO: enforcement start date is temp only. Remove after a few releases. if (status === 'expired' && sv?.enforcementStartDate && new Date(sv.enforcementStartDate) > new Date()) { const enforcementMessage = getMessage({ messages, - expiration: sv.enforcementStartDate + expiration: sv.enforcementStartDate, + userRoles }); return { status: 'warn', diff --git a/app/sagas/selectServer.ts b/app/sagas/selectServer.ts index 5373f6b0fcf..d1a9f497d7f 100644 --- a/app/sagas/selectServer.ts +++ b/app/sagas/selectServer.ts @@ -118,9 +118,11 @@ const getServerInfoSaga = function* getServerInfoSaga({ server, raiseError = tru if (!serverRecord) { throw new Error('Server not found'); } + const userRoles = (yield* appSelector(state => state.login?.user?.roles)) ?? []; const supportedVersionsResult = yield* call(checkSupportedVersions, { supportedVersions: serverRecord.supportedVersions, - serverVersion: serverRecord.version + serverVersion: serverRecord.version, + userRoles }); yield put(setSupportedVersions(supportedVersionsResult)); From e4855452c90df37d60640463039a9e8670d9a0cc Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 1 Jul 2026 12:00:22 -0300 Subject: [PATCH 2/3] fix(supportedVersions): don't warn non-targeted users in enforcement window The enforcement-start-date branch returned status 'warn' unconditionally. When a role-targeted expiration message was filtered out for a non-targeted user, it returned warn with an undefined message, surfacing an 'update required' indicator (sidebar item + header badge) that opened a blank sheet. Return 'supported' when no eligible enforcement message exists, so non-targeted members keep using the app with no warning and are not disconnected during the grace window. Claude-Session: https://claude.ai/code/session_01D7nAgeJHNRgjeerGn4YHCV --- .../methods/checkSupportedVersions.test.ts | 45 +++++++++++++++++++ app/lib/methods/checkSupportedVersions.ts | 15 ++++--- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/app/lib/methods/checkSupportedVersions.test.ts b/app/lib/methods/checkSupportedVersions.test.ts index 4dfa63cba76..d6d561ad89c 100644 --- a/app/lib/methods/checkSupportedVersions.test.ts +++ b/app/lib/methods/checkSupportedVersions.test.ts @@ -608,4 +608,49 @@ describe('checkSupportedVersions role targeting', () => { expect(result.status).toBe('supported'); expect(result.message).toBeUndefined(); }); + + describe('enforcement window', () => { + const buildEnforcementSupportedVersions = (roles?: string[]): ISupportedVersionsData => ({ + timestamp: TODAY, + enforcementStartDate: '2023-04-15T00:00:00.000Z', + messages: [ + { + remainingDays: 15, + title: 'targeted', + subtitle: 'subtitle_token', + description: 'description_token', + type: 'info', + ...(roles ? { roles } : {}), + link: 'Docs page' + } + ], + i18n: MOCK_I18N, + versions: [ + { + version: '1.4.0', + expiration: '2023-03-10T00:00:00.000Z' + } + ] + }); + + test('does not warn a non-targeted user during the enforcement grace window', () => { + const result = checkSupportedVersions({ + supportedVersions: buildEnforcementSupportedVersions(['admin']), + serverVersion: '1.4.0', + userRoles: ['user'] + }); + expect(result.status).toBe('supported'); + expect(result.message).toBeUndefined(); + }); + + test('warns a targeted user during the enforcement grace window', () => { + expect( + checkSupportedVersions({ + supportedVersions: buildEnforcementSupportedVersions(['admin']), + serverVersion: '1.4.0', + userRoles: ['admin'] + }) + ).toMatchObject({ status: 'warn', message: { title: 'targeted' } }); + }); + }); }); diff --git a/app/lib/methods/checkSupportedVersions.ts b/app/lib/methods/checkSupportedVersions.ts index 9cef290c196..ea442170ac2 100644 --- a/app/lib/methods/checkSupportedVersions.ts +++ b/app/lib/methods/checkSupportedVersions.ts @@ -83,12 +83,15 @@ export const checkSupportedVersions = function ({ expiration: sv.enforcementStartDate, userRoles }); - return { - status: 'warn', - message: enforcementMessage, - i18n: enforcementMessage ? sv?.i18n : undefined, - expiration: sv.enforcementStartDate - }; + if (enforcementMessage) { + return { + status: 'warn', + message: enforcementMessage, + i18n: sv?.i18n, + expiration: sv.enforcementStartDate + }; + } + return { status: 'supported', expiration: sv.enforcementStartDate }; } return { From 29798750d34ddf3a9a6fb8138c3e1d8a6c5336e2 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 1 Jul 2026 12:00:30 -0300 Subject: [PATCH 3/3] test: refresh Timestamp story snapshot for calendar drift The relative-time render drifted from 'a year ago' to '2 years ago'. Unrelated to the role-targeting change; refreshes the stale snapshot so CI is green. Claude-Session: https://claude.ai/code/session_01D7nAgeJHNRgjeerGn4YHCV --- app/containers/markdown/__snapshots__/Markdown.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap index 009e5615a36..2517ef4eb26 100644 --- a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap +++ b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap @@ -5882,7 +5882,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - a year ago + 2 years ago