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,56 @@
import { useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import type { RoomToolboxActionConfig } from '@rocket.chat/ui-contexts';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Utilities } from '../../../../../ee/lib/misc/Utilities';
import { useAppActionButtons } from '../../../../hooks/useAppActionButtons';
import { useApplyButtonFilters } from '../../../../hooks/useApplyButtonFilters';
import { UiKitTriggerTimeoutError } from '../../../../lib/errors/UiKitTriggerTimeoutError';
import { useUiKitActionManager } from '../../../../uikit/hooks/useUiKitActionManager';
import { useRoom } from '../../contexts/RoomContext';
export const useAppsRoomActions = () => {
const result = useAppActionButtons('roomAction');
const actionManager = useUiKitActionManager();
const applyButtonFilters = useApplyButtonFilters();
const room = useRoom();
const { t } = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
return useMemo(
() =>
result.data?.filter(applyButtonFilters).map((action): RoomToolboxActionConfig => ({
id: action.actionId,
variant: action.variant,
icon: undefined as unknown as RoomToolboxActionConfig['icon'],
order: 300,
title: Utilities.getI18nKeyForApp(action.labelI18n, action.appId),
groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'],
// Filters were applied in the applyButtonFilters function
// if the code made it this far, the button should be shown
action: () => {
void actionManager
.emitInteraction(action.appId, {
type: 'actionButton',
actionId: action.actionId,
rid: room._id,
payload: { context: action.context },
})
.catch(async (reason) => {
if (reason instanceof UiKitTriggerTimeoutError) {
dispatchToastMessage({
type: 'error',
message: t('UIKit_Interaction_Timeout'),
});
return;
}
return reason;
});
},
type: 'apps',
})) ?? [],
[actionManager, applyButtonFilters, dispatchToastMessage, result.data, room._id, t],
);
};

View File

@@ -0,0 +1,64 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { useEndpoint, usePermission, useSetting, useStream, useUserId } from '@rocket.chat/ui-contexts';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { cannedResponsesQueryKeys } from '../../../../lib/queryKeys';
type CannedResponse = { _id: string; shortcut: string; text: string };
const useCannedResponsesQuery = (room: IRoom) => {
const isOmnichannel = isOmnichannelRoom(room);
const uid = useUserId();
const isCannedResponsesEnabled = useSetting('Canned_Responses_Enable', true);
const canViewCannedResponses = usePermission('view-canned-responses');
const subscribeToCannedResponses = useStream('canned-responses');
const enabled = isOmnichannel && !!uid && isCannedResponsesEnabled && canViewCannedResponses;
const getCannedResponses = useEndpoint('GET', '/v1/canned-responses.get');
const queryClient = useQueryClient();
useEffect(() => {
if (!enabled) return;
return subscribeToCannedResponses('canned-responses', (...[response, options]) => {
const { agentsId } = options || {};
if (Array.isArray(agentsId) && !agentsId.includes(uid)) {
return;
}
switch (response.type) {
case 'changed': {
const { _id, shortcut, text } = response;
queryClient.setQueryData<CannedResponse[]>(cannedResponsesQueryKeys.all, (responses) =>
responses?.filter((response) => response._id !== _id).concat([{ _id, shortcut, text }]),
);
break;
}
case 'removed': {
const { _id } = response;
queryClient.setQueryData<CannedResponse[]>(cannedResponsesQueryKeys.all, (responses) =>
responses?.filter((response) => response._id !== _id),
);
break;
}
}
});
}, [enabled, getCannedResponses, queryClient, subscribeToCannedResponses, uid]);
return useQuery<CannedResponse[]>({
queryKey: cannedResponsesQueryKeys.all,
queryFn: async () => {
const { responses } = await getCannedResponses();
return responses.map(({ _id, shortcut, text }) => ({ _id, shortcut, text }));
},
enabled,
staleTime: Infinity,
});
};
export default useCannedResponsesQuery;

View File

@@ -0,0 +1,219 @@
import type { ISubscription } from '@rocket.chat/core-typings';
import type { IActionManager } from '@rocket.chat/ui-contexts';
import { useUserId } from '@rocket.chat/ui-contexts';
import { renderHook } from '@testing-library/react';
import { useChatMessagesInstance } from './useChatMessagesInstance';
import { useEmojiPicker } from '../../../../contexts/EmojiPickerContext';
import { ChatMessages } from '../../../../lib/ChatMessages';
import type { E2ERoomState } from '../../../../lib/e2ee/E2ERoomState';
import { useUiKitActionManager } from '../../../../uikit/hooks/useUiKitActionManager';
import { useRoomSubscription } from '../../contexts/RoomContext';
import { useE2EERoomState } from '../../hooks/useE2EERoomState';
jest.mock('@rocket.chat/ui-contexts', () => ({
useUserId: jest.fn(),
}));
jest.mock('../../contexts/RoomContext', () => ({
useRoomSubscription: jest.fn(),
}));
jest.mock('../../../../uikit/hooks/useUiKitActionManager', () => ({
useUiKitActionManager: jest.fn(),
}));
jest.mock('../../hooks/useE2EERoomState', () => ({
useE2EERoomState: jest.fn(),
}));
jest.mock('../../../../contexts/EmojiPickerContext', () => ({
useEmojiPicker: jest.fn(),
}));
const updateSubscriptionMock = jest.fn();
jest.mock('../../../../lib/ChatMessages', () => {
return {
ChatMessages: jest.fn().mockImplementation(() => {
return {
release: jest.fn(),
readStateManager: {
updateSubscription: updateSubscriptionMock,
subscribeToMessages: jest.fn(),
},
};
}),
};
});
describe('useChatMessagesInstance', () => {
let mockUid: string;
let mockSubscription: Pick<ISubscription, 'u' | 't' | 'rid'>;
let mockActionManager: IActionManager | undefined;
let mockE2EERoomState: E2ERoomState;
let mockEmojiPicker: {
open: jest.Mock;
isOpen: boolean;
close: jest.Mock;
};
beforeEach(() => {
jest.clearAllMocks();
mockUid = 'mockUid';
mockSubscription = {
u: {
_id: mockUid,
username: 'usernameMock',
name: 'nameMock',
},
t: 'p',
rid: 'roomId',
};
mockActionManager = undefined;
mockE2EERoomState = 'READY';
mockEmojiPicker = {
open: jest.fn(),
isOpen: false,
close: jest.fn(),
};
(useUserId as jest.Mock).mockReturnValue(mockUid);
(useRoomSubscription as jest.Mock).mockReturnValue(mockSubscription);
(useUiKitActionManager as jest.Mock).mockReturnValue(mockActionManager);
(useE2EERoomState as jest.Mock).mockReturnValue(mockE2EERoomState);
(useEmojiPicker as jest.Mock).mockReturnValue(mockEmojiPicker);
});
it('should initialize ChatMessages instance with correct arguments', () => {
const { result } = renderHook(() =>
useChatMessagesInstance({
rid: mockSubscription.rid,
tmid: 'threadId',
encrypted: false,
}),
);
expect(ChatMessages).toHaveBeenCalledWith({
rid: mockSubscription.rid,
tmid: 'threadId',
uid: mockUid,
actionManager: mockActionManager,
});
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
expect(ChatMessages).toHaveBeenCalledTimes(1);
expect(updateSubscriptionMock).toHaveBeenCalledTimes(1);
});
it('should update ChatMessages subscription', () => {
const { result, rerender } = renderHook(() =>
useChatMessagesInstance({
rid: mockSubscription.rid,
tmid: 'threadId',
encrypted: false,
}),
);
expect(ChatMessages).toHaveBeenCalledWith({
rid: mockSubscription.rid,
tmid: 'threadId',
uid: mockUid,
actionManager: mockActionManager,
});
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
expect(ChatMessages).toHaveBeenCalledTimes(1);
expect(updateSubscriptionMock).toHaveBeenCalledTimes(1);
(useRoomSubscription as jest.Mock).mockReturnValue({ ...mockSubscription, rid: 'newRoomId' });
rerender();
expect(ChatMessages).toHaveBeenCalledWith({
rid: mockSubscription.rid,
tmid: 'threadId',
uid: mockUid,
actionManager: mockActionManager,
});
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
expect(ChatMessages).toHaveBeenCalledTimes(1);
expect(updateSubscriptionMock).toHaveBeenCalledTimes(2);
});
it('should update ChatMessages instance when dependencies changes', () => {
const { result, rerender } = renderHook(() =>
useChatMessagesInstance({
rid: mockSubscription.rid,
tmid: 'threadId',
encrypted: false,
}),
);
expect(ChatMessages).toHaveBeenCalledWith({
rid: mockSubscription.rid,
tmid: 'threadId',
uid: mockUid,
actionManager: mockActionManager,
});
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
expect(ChatMessages).toHaveBeenCalledTimes(1);
expect(updateSubscriptionMock).toHaveBeenCalledTimes(1);
(useE2EERoomState as jest.Mock).mockReturnValue('WAITING_KEYS');
rerender();
expect(ChatMessages).toHaveBeenCalledWith({
rid: mockSubscription.rid,
tmid: 'threadId',
uid: mockUid,
actionManager: mockActionManager,
});
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
expect(updateSubscriptionMock).toHaveBeenCalledTimes(2);
expect(ChatMessages).toHaveBeenCalledTimes(2);
});
it('should update ChatMessages instance when hook props changes', () => {
const initialProps = {
rid: mockSubscription.rid,
tmid: 'threadId',
encrypted: false,
};
const { result, rerender } = renderHook((props = initialProps) => useChatMessagesInstance(props as any));
expect(ChatMessages).toHaveBeenCalledWith({
rid: mockSubscription.rid,
tmid: 'threadId',
uid: mockUid,
actionManager: mockActionManager,
});
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
expect(ChatMessages).toHaveBeenCalledTimes(1);
expect(updateSubscriptionMock).toHaveBeenCalledTimes(1);
rerender({
rid: mockSubscription.rid,
tmid: 'threadId',
encrypted: true,
});
expect(ChatMessages).toHaveBeenCalledWith({
rid: mockSubscription.rid,
tmid: 'threadId',
uid: mockUid,
actionManager: mockActionManager,
});
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
expect(updateSubscriptionMock).toHaveBeenCalledTimes(2);
expect(ChatMessages).toHaveBeenCalledTimes(2);
});
});

View File

@@ -0,0 +1,48 @@
import type { IMessage, IRoom } from '@rocket.chat/core-typings';
import { useUserId } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { useInstance } from './useInstance';
import { useEmojiPicker } from '../../../../contexts/EmojiPickerContext';
import { ChatMessages } from '../../../../lib/ChatMessages';
import type { ChatAPI } from '../../../../lib/chats/ChatAPI';
import { useUiKitActionManager } from '../../../../uikit/hooks/useUiKitActionManager';
import { useRoomSubscription } from '../../contexts/RoomContext';
import { useE2EERoomState } from '../../hooks/useE2EERoomState';
export function useChatMessagesInstance({
rid,
tmid,
encrypted,
}: {
rid: IRoom['_id'];
tmid?: IMessage['_id'];
encrypted: IRoom['encrypted'];
}): ChatAPI {
const uid = useUserId();
const subscription = useRoomSubscription();
const actionManager = useUiKitActionManager();
const e2eRoomState = useE2EERoomState(rid);
const chatMessages = useInstance(() => {
const instance = new ChatMessages({ rid, tmid, uid, actionManager });
return [instance, () => instance.release()];
}, [rid, tmid, uid, encrypted, e2eRoomState]);
useEffect(() => {
if (subscription?.rid) {
return chatMessages?.readStateManager.subscribeToMessages();
}
}, [subscription?.rid, chatMessages?.readStateManager]);
useEffect(() => {
if (subscription) {
chatMessages?.readStateManager.updateSubscription(subscription);
}
}, [subscription, chatMessages?.readStateManager]);
chatMessages.emojiPicker = useEmojiPicker();
return chatMessages;
}

View File

@@ -0,0 +1,10 @@
import { useStableArray } from '@rocket.chat/fuselage-hooks';
import type { RoomToolboxActionConfig } from '@rocket.chat/ui-contexts';
import { roomActionHooks } from '../../../../ui';
export const useCoreRoomActions = () => {
return useStableArray(
roomActionHooks.map((roomActionHook) => roomActionHook()).filter((roomAction): roomAction is RoomToolboxActionConfig => !!roomAction),
);
};

View File

@@ -0,0 +1,19 @@
import type { RoomToolboxActionConfig } from '@rocket.chat/ui-contexts';
import MediaCallHistoryContextualbarRoom from '../../../mediaCallHistory/MediaCallHistoryContextualbarRoom';
const mediaCallHistoryRoute: RoomToolboxActionConfig = {
id: 'media-call-history',
title: 'Call_Information',
tabComponent: MediaCallHistoryContextualbarRoom,
icon: 'info-circled',
groups: ['direct'],
};
const coreRoomRoutes = [mediaCallHistoryRoute];
// This isn't really a proper hook, but it could be extended in the future
// So we're maitaning the same pattern as `useCoreRoomActions`
export const useCoreRoomRoutes = (): Array<RoomToolboxActionConfig> => {
return coreRoomRoutes;
};

View File

@@ -0,0 +1,41 @@
import { renderHook } from '@testing-library/react';
import { useDepsMatch } from './useDepsMatch';
describe('useDepsMatch', () => {
it('should return true when dependencies match', () => {
const { result, rerender } = renderHook(({ deps }) => useDepsMatch(deps), {
initialProps: { deps: ['dep1', 'dep2'] },
});
expect(result.current).toBe(true);
rerender({ deps: ['dep1', 'dep2'] });
expect(result.current).toBe(true);
});
it('should return false when dependencies do not match', () => {
const { result, rerender } = renderHook(({ deps }) => useDepsMatch(deps), {
initialProps: { deps: ['dep1', 'dep2'] },
});
expect(result.current).toBe(true);
rerender({ deps: ['dep1', 'dep3'] });
expect(result.current).toBe(false);
});
it('should return false when dependencies length changes', () => {
const { result, rerender } = renderHook(({ deps }) => useDepsMatch(deps), {
initialProps: { deps: ['dep1', 'dep2'] },
});
expect(result.current).toBe(true);
rerender({ deps: ['dep1'] });
expect(result.current).toBe(false);
});
});

View File

@@ -0,0 +1,14 @@
import { useRef } from 'react';
const depsMatch = (a: unknown[], b: unknown[]): boolean => a.every((value, index) => Object.is(value, b[index]));
export const useDepsMatch = (deps: unknown[]): boolean => {
const prevDepsRef = useRef(deps);
const { current: prevDeps } = prevDepsRef;
const match = depsMatch(prevDeps, deps);
prevDepsRef.current = deps;
return match;
};

View File

@@ -0,0 +1,61 @@
import { renderHook } from '@testing-library/react';
import { useInstance } from './useInstance';
class MockChatMessages {
release() {
return 'released';
}
}
describe('useInstance', () => {
let factory: jest.Mock;
let release: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
release = jest.fn();
factory = jest.fn().mockReturnValue([{ instance: new MockChatMessages() }, release]);
});
it('should create a new instance when dependencies change', () => {
const { result, rerender } = renderHook(({ deps }) => useInstance(factory, deps), {
initialProps: { deps: ['initial-dep'] },
});
expect(result.current).toEqual({ instance: new MockChatMessages() });
expect(factory).toHaveBeenCalledTimes(1);
expect(release).toHaveBeenCalledTimes(0);
rerender({ deps: ['new-dep'] });
expect(result.current).toEqual({ instance: new MockChatMessages() });
expect(factory).toHaveBeenCalledTimes(2);
expect(release).toHaveBeenCalledTimes(1);
});
it('should not create a new instance when dependencies do not change', () => {
const { result, rerender } = renderHook(({ deps }) => useInstance(factory, deps), {
initialProps: { deps: ['initial-dep'] },
});
expect(result.current).toEqual({ instance: new MockChatMessages() });
expect(factory).toHaveBeenCalledTimes(1);
expect(release).toHaveBeenCalledTimes(0);
rerender({ deps: ['initial-dep'] });
expect(result.current).toEqual({ instance: new MockChatMessages() });
expect(factory).toHaveBeenCalledTimes(1);
expect(release).toHaveBeenCalledTimes(0);
});
it('should call release function when component unmounts', () => {
const { unmount } = renderHook(() => useInstance(factory, ['initial-dep']));
unmount();
expect(release).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,23 @@
import { useRef, useEffect } from 'react';
import { useDepsMatch } from './useDepsMatch';
export function useInstance<T>(factory: () => [instance: T, release?: () => void], deps: unknown[]): T {
const ref = useRef<[instance: T, release?: () => void]>(undefined);
useEffect(
() => () => {
ref.current?.[1]?.();
},
[],
);
const depsMatch = useDepsMatch(deps);
if (!ref.current || !depsMatch) {
ref.current?.[1]?.();
ref.current = factory();
}
return ref.current[0];
}

View File

@@ -0,0 +1,52 @@
import type { ISubscription } from '@rocket.chat/core-typings';
import { useRouter } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { LegacyRoomManager } from '../../../../lib/LegacyRoomManager';
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
const routeNameToRoomTypeMap: Record<string, string> = {
channel: 'c',
group: 'p',
direct: 'd',
live: 'l',
};
export const useRedirectOnSettingsChanged = (subscription?: ISubscription | null) => {
const router = useRouter();
const subExists = !!subscription;
useEffect(() => {
if (!subExists) {
return;
}
const redirect = async () => {
const routeConfig = roomCoordinator.getRoomDirectives(subscription.t).config.route;
const channelName = router.getRouteParameters().name;
const routeName = router.getRouteName() as string;
if (!routeConfig?.path || !routeName || !channelName) {
return;
}
if (routeConfig.name === routeName && channelName === subscription.name) {
return;
}
const routeRoomType = routeNameToRoomTypeMap[routeName];
if (routeRoomType) {
await LegacyRoomManager.close(routeRoomType + routeName);
}
router.navigate({
pattern: routeConfig.path,
params: { ...router.getRouteParameters(), name: subscription.name },
search: router.getSearchParameters(),
});
};
redirect();
}, [subscription?.t, subscription?.name, router, subExists]);
};

View File

@@ -0,0 +1,50 @@
import type { IEditedMessage } from '@rocket.chat/core-typings';
import { isEditedMessage } from '@rocket.chat/core-typings';
import { useStream } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { Messages, Subscriptions } from '../../../../stores';
export const useUsersNameChanged = () => {
const notify = useStream('notify-logged');
const updateMessages = Messages.use((state) => state.update);
useEffect(() => {
return notify('Users:NameChanged', ({ _id, name, username }) => {
updateMessages(
(record) => record.u._id === _id,
(record) => ({
...record,
u: {
...record.u,
username: username ?? record.u.username,
name,
},
}),
);
updateMessages(
(record): record is IEditedMessage => isEditedMessage(record) && record.editedBy?._id === _id,
(record) => ({
...record,
editedBy: {
...record.editedBy,
username,
},
}),
);
updateMessages(
(record) => record.mentions?.some((mention) => mention._id === _id) ?? false,
(record) => ({
...record,
mentions: record.mentions?.map((mention) => (mention._id === _id ? { ...mention, username, name } : mention)),
}),
);
Subscriptions.state.update(
(record) => record.name === username && record.t === 'd',
(record) => ({ ...record, fname: name }),
);
});
}, [notify, updateMessages]);
};