From f40645a45e4c03d20c578ea37012bd35d825d419 Mon Sep 17 00:00:00 2001 From: Tasso Date: Tue, 2 Sep 2025 14:52:18 -0300 Subject: [PATCH 1/8] Enforce `undefined` instead of `null` in `UserContext` --- apps/meteor/app/authorization/client/hasPermission.ts | 6 +++--- apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts | 2 +- apps/meteor/app/ui/client/lib/ChatMessages.ts | 4 ++-- apps/meteor/app/utils/client/lib/getUserPreference.ts | 4 ++-- .../client/hooks/lists/useStreamUpdatesForMessageList.ts | 2 +- .../client/hooks/roomActions/useMediaCallRoomAction.ts | 2 +- apps/meteor/client/hooks/useIdleConnection.ts | 3 ++- apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- apps/meteor/client/lib/mutationEffects/room.ts | 2 +- apps/meteor/client/lib/utils/getUidDirectMessage.ts | 5 ++++- apps/meteor/client/lib/utils/timeAgo.ts | 2 +- apps/meteor/client/providers/UserProvider/UserProvider.tsx | 2 +- .../UserProvider/hooks/useClearRemovedRoomsHistory.ts | 3 ++- .../contactHistory/MessageList/useHistoryMessageList.ts | 2 +- apps/meteor/client/views/room/MemberListRouter.tsx | 2 +- .../room/contextualBar/Discussions/useDiscussionsList.ts | 2 +- .../MessageSearchTab/hooks/useMessageSearchQuery.ts | 2 +- .../RoomFiles/hooks/useMessageDeletionIsAllowed.ts | 2 +- .../contextualBar/Threads/components/ThreadListItem.tsx | 2 +- .../room/contextualBar/Threads/hooks/useThreadsList.ts | 2 +- packages/mock-providers/src/MockedAppRootBuilder.tsx | 4 ++-- packages/ui-contexts/src/UserContext.ts | 4 ++-- packages/ui-contexts/src/hooks/useUserId.ts | 2 +- 23 files changed, 34 insertions(+), 29 deletions(-) diff --git a/apps/meteor/app/authorization/client/hasPermission.ts b/apps/meteor/app/authorization/client/hasPermission.ts index 4481ef29e45d3..d007a00f8168a 100644 --- a/apps/meteor/app/authorization/client/hasPermission.ts +++ b/apps/meteor/app/authorization/client/hasPermission.ts @@ -47,10 +47,10 @@ const validatePermissions = ( userId: IUser['_id'], scopedRoles?: IPermission['_id'][], ) => boolean, - userId?: IUser['_id'] | null, + userId?: IUser['_id'], scopedRoles?: IPermission['_id'][], ): boolean => { - userId = userId ?? Meteor.userId(); + userId = userId ?? Meteor.userId() ?? undefined; if (!userId) { return false; @@ -75,7 +75,7 @@ export const hasAtLeastOnePermission = (permissions: IPermission['_id'] | IPermi export const userHasAllPermission = ( permissions: IPermission['_id'] | IPermission['_id'][], scope?: string, - userId?: IUser['_id'] | null, + userId?: IUser['_id'], ): boolean => validatePermissions(permissions, scope, all, userId); export const hasPermission = hasAllPermission; diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index b604ae69dac99..214427d7b7bee 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -138,7 +138,7 @@ class RoomHistoryManagerClass extends Emitter { ({ ls } = subscription); } - const showThreadsInMainChannel = getUserPreference(Meteor.userId(), 'showThreadsInMainChannel', false); + const showThreadsInMainChannel = getUserPreference(Meteor.userId() ?? undefined, 'showThreadsInMainChannel', false); const result = await callWithErrorHandling( 'loadHistory', rid, diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 0ff51d07eabdd..44cbf6bdbea43 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -25,7 +25,7 @@ type DeepWritable = T extends (...args: any) => any }; export class ChatMessages implements ChatAPI { - public uid: string | null; + public uid: string | undefined; public tmid?: IMessage['_id']; @@ -142,7 +142,7 @@ export class ChatMessages implements ChatAPI { public flows: DeepWritable; - public constructor(params: { rid: IRoom['_id']; tmid?: IMessage['_id']; uid: IUser['_id'] | null; actionManager: IActionManager }) { + public constructor(params: { rid: IRoom['_id']; tmid?: IMessage['_id']; uid: IUser['_id'] | undefined; actionManager: IActionManager }) { const { rid, tmid } = params; this.tmid = tmid; this.uid = params.uid; diff --git a/apps/meteor/app/utils/client/lib/getUserPreference.ts b/apps/meteor/app/utils/client/lib/getUserPreference.ts index 23172a2a32030..ac3aec3e0c5ac 100644 --- a/apps/meteor/app/utils/client/lib/getUserPreference.ts +++ b/apps/meteor/app/utils/client/lib/getUserPreference.ts @@ -9,7 +9,7 @@ import { Users } from '../../../../client/stores'; * @param key The preference name * @returns The preference value */ -export function getUserPreference(user: IUser['_id'] | null | undefined, key: string): TValue | undefined; +export function getUserPreference(user: IUser['_id'] | undefined, key: string): TValue | undefined; /** * Get a user preference * @param user The user @@ -24,7 +24,7 @@ export function getUserPreference(user: Pick * @param defaultValue The default value * @returns The preference value or the default value */ -export function getUserPreference(user: IUser['_id'] | null | undefined, key: string, defaultValue: TValue): TValue; +export function getUserPreference(user: IUser['_id'] | undefined, key: string, defaultValue: TValue): TValue; /** * Get a user preference * @param user The user diff --git a/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts b/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts index a3675c638d93a..322b6558888dd 100644 --- a/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts +++ b/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts @@ -47,7 +47,7 @@ const createDeleteCriteria = (params: NotifyRoomRidDeleteBulkEvent): ((message: return createPredicateFromFilter(query); }; -export const useStreamUpdatesForMessageList = (messageList: MessageList, uid: IUser['_id'] | null, rid: IRoom['_id'] | null): void => { +export const useStreamUpdatesForMessageList = (messageList: MessageList, uid: IUser['_id'] | undefined, rid: IRoom['_id'] | null): void => { const subscribeToRoomMessages = useStream('room-messages'); const subscribeToNotifyRoom = useStream('notify-room'); diff --git a/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts b/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts index 8b3472bd53785..85eb9651ea9be 100644 --- a/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts @@ -8,7 +8,7 @@ import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomCont import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; import { useUserInfoQuery } from '../useUserInfoQuery'; -const getPeerId = (uids: string[], ownUserId: string | null) => { +const getPeerId = (uids: string[], ownUserId: string | undefined) => { if (!ownUserId) { return undefined; } diff --git a/apps/meteor/client/hooks/useIdleConnection.ts b/apps/meteor/client/hooks/useIdleConnection.ts index d4d5c8d9a7609..4ae667237a55b 100644 --- a/apps/meteor/client/hooks/useIdleConnection.ts +++ b/apps/meteor/client/hooks/useIdleConnection.ts @@ -1,9 +1,10 @@ +import type { IUser } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useConnectionStatus, useSetting } from '@rocket.chat/ui-contexts'; import { useIdleActiveEvents } from './useIdleActiveEvents'; -export const useIdleConnection = (uid: string | null) => { +export const useIdleConnection = (uid: IUser['_id'] | undefined) => { const { status } = useConnectionStatus(); const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead'); const { disconnect: disconnectServer, reconnect: reconnectServer } = useConnectionStatus(); diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 5c341d356995d..ef90b95cbb29f 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -106,7 +106,7 @@ export type UploadsAPI = { }; export type ChatAPI = { - readonly uid: string | null; + readonly uid: string | undefined; readonly composer?: ComposerAPI; readonly setComposerAPI: (composer?: ComposerAPI) => void; readonly data: DataAPI; diff --git a/apps/meteor/client/lib/mutationEffects/room.ts b/apps/meteor/client/lib/mutationEffects/room.ts index 7340a42fa55a9..e7ca713d12a89 100644 --- a/apps/meteor/client/lib/mutationEffects/room.ts +++ b/apps/meteor/client/lib/mutationEffects/room.ts @@ -1,6 +1,6 @@ import { Subscriptions } from '../../stores'; -export const toggleFavoriteRoom = (roomId: string, favorite: boolean, userId: string | null) => { +export const toggleFavoriteRoom = (roomId: string, favorite: boolean, userId: string | undefined) => { if (!userId) { return; } diff --git a/apps/meteor/client/lib/utils/getUidDirectMessage.ts b/apps/meteor/client/lib/utils/getUidDirectMessage.ts index b4a8386c7259f..0712173381dcc 100644 --- a/apps/meteor/client/lib/utils/getUidDirectMessage.ts +++ b/apps/meteor/client/lib/utils/getUidDirectMessage.ts @@ -3,7 +3,10 @@ import { Meteor } from 'meteor/meteor'; import { Rooms } from '../../stores'; -export const getUidDirectMessage = (rid: IRoom['_id'], uid: IUser['_id'] | null = Meteor.userId()): string | undefined => { +export const getUidDirectMessage = ( + rid: IRoom['_id'], + uid: IUser['_id'] | undefined = Meteor.userId() ?? undefined, +): string | undefined => { const room = Rooms.state.get(rid); if (!room || room.t !== 'd' || !room.uids || room.uids.length > 2) { diff --git a/apps/meteor/client/lib/utils/timeAgo.ts b/apps/meteor/client/lib/utils/timeAgo.ts index 49a2629dde9b8..9dedfca9c1ba7 100644 --- a/apps/meteor/client/lib/utils/timeAgo.ts +++ b/apps/meteor/client/lib/utils/timeAgo.ts @@ -10,7 +10,7 @@ import { settings } from '../settings'; const dayFormat = ['h:mm A', 'H:mm']; export const timeAgo = (date: MomentInput) => { - const clockMode = Tracker.nonreactive(() => getUserPreference(Meteor.userId(), 'clockMode', false) as number | boolean); + const clockMode = Tracker.nonreactive(() => getUserPreference(Meteor.userId() ?? undefined, 'clockMode', false) as number | boolean); const messageTimeFormat = settings.peek('Message_TimeFormat'); const sameDay = (typeof clockMode === 'number' ? dayFormat[clockMode - 1] : undefined) || messageTimeFormat; diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index ba2819ae18d3a..fd340f3bd473d 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -30,7 +30,7 @@ import { useSamlInviteToken } from '../../views/invite/hooks/useSamlInviteToken' const getUser = (): IUser | null => Meteor.user() as IUser | null; -const getUserId = (): string | null => Meteor.userId(); +const getUserId = () => Meteor.userId() ?? undefined; type UserProviderProps = { children: ReactNode; diff --git a/apps/meteor/client/providers/UserProvider/hooks/useClearRemovedRoomsHistory.ts b/apps/meteor/client/providers/UserProvider/hooks/useClearRemovedRoomsHistory.ts index f48b55986756a..2251610dc9a56 100644 --- a/apps/meteor/client/providers/UserProvider/hooks/useClearRemovedRoomsHistory.ts +++ b/apps/meteor/client/providers/UserProvider/hooks/useClearRemovedRoomsHistory.ts @@ -1,9 +1,10 @@ +import type { IUser } from '@rocket.chat/core-typings'; import { useStream } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { RoomHistoryManager } from '../../../../app/ui-utils/client'; -export const useClearRemovedRoomsHistory = (userId: string | null) => { +export const useClearRemovedRoomsHistory = (userId: IUser['_id'] | undefined) => { const subscribeToNotifyUser = useStream('notify-user'); useEffect(() => { if (!userId) { diff --git a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/useHistoryMessageList.ts b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/useHistoryMessageList.ts index 4b1a9fbbd13d8..7d098a58e63eb 100644 --- a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/useHistoryMessageList.ts +++ b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/useHistoryMessageList.ts @@ -15,7 +15,7 @@ type HistoryMessageListOptions = { export const useHistoryMessageList = ( options: HistoryMessageListOptions, - uid: IUser['_id'] | null, + uid: IUser['_id'] | undefined, ): { itemsList: MessageList; initialItemCount: number; diff --git a/apps/meteor/client/views/room/MemberListRouter.tsx b/apps/meteor/client/views/room/MemberListRouter.tsx index ac646a6df4021..77695eb089321 100644 --- a/apps/meteor/client/views/room/MemberListRouter.tsx +++ b/apps/meteor/client/views/room/MemberListRouter.tsx @@ -6,7 +6,7 @@ import { useRoomToolbox } from './contexts/RoomToolboxContext'; import RoomMembers from './contextualBar/RoomMembers'; import UserInfo from './contextualBar/UserInfo'; -const getUid = (room: IRoom, ownUserId: string | null) => { +const getUid = (room: IRoom, ownUserId: string | undefined) => { if (room.uids?.length === 1) { return room.uids[0]; } diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/useDiscussionsList.ts b/apps/meteor/client/views/room/contextualBar/Discussions/useDiscussionsList.ts index d747c97c34289..ef86598558ca0 100644 --- a/apps/meteor/client/views/room/contextualBar/Discussions/useDiscussionsList.ts +++ b/apps/meteor/client/views/room/contextualBar/Discussions/useDiscussionsList.ts @@ -10,7 +10,7 @@ import { getConfig } from '../../../../lib/utils/getConfig'; export const useDiscussionsList = ( options: DiscussionsListOptions, - uid: IUser['_id'] | null, + uid: IUser['_id'] | undefined, ): { discussionsList: DiscussionsList; initialItemCount: number; diff --git a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/hooks/useMessageSearchQuery.ts b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/hooks/useMessageSearchQuery.ts index 7402c5ab75761..ffa22b4ae69a6 100644 --- a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/hooks/useMessageSearchQuery.ts +++ b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/hooks/useMessageSearchQuery.ts @@ -12,7 +12,7 @@ export const useMessageSearchQuery = ({ limit: number; globalSearch: boolean; }) => { - const uid = useUserId() ?? undefined; + const uid = useUserId(); const room = useRoom(); const t = useTranslation(); diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useMessageDeletionIsAllowed.ts b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useMessageDeletionIsAllowed.ts index 9928aefd504cc..5c45c8cc585fb 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useMessageDeletionIsAllowed.ts +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useMessageDeletionIsAllowed.ts @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import { getDifference, MINUTES } from '../lib/getDifference'; -export const useMessageDeletionIsAllowed = (rid: IRoom['_id'], file: IUpload, uid: IUser['_id'] | null) => { +export const useMessageDeletionIsAllowed = (rid: IRoom['_id'], file: IUpload, uid: IUser['_id'] | undefined) => { const canForceDelete = usePermission('force-delete-message', rid); const deletionIsEnabled = useSetting('Message_AllowDeleting'); const userHasPermissionToDeleteAny = usePermission('delete-message', rid); diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadListItem.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadListItem.tsx index 520aed714a6d2..c15993707aa25 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadListItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadListItem.tsx @@ -18,7 +18,7 @@ type ThreadListItemProps = { }; const ThreadListItem = ({ thread, unread, unreadUser, unreadGroup, onClick }: ThreadListItemProps): ReactElement => { - const uid = useUserId() ?? undefined; + const uid = useUserId(); const decryptedMsg = useDecryptedMessage(thread); const msg = normalizeThreadMessage({ ...thread, msg: decryptedMsg }); diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadsList.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadsList.ts index cf9c32980f01f..2fee0b037a756 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadsList.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadsList.ts @@ -10,7 +10,7 @@ import { getConfig } from '../../../../../lib/utils/getConfig'; export const useThreadsList = ( options: ThreadsListOptions, - uid: IUser['_id'] | null, + uid: IUser['_id'] | undefined, ): { threadsList: ThreadsList; initialItemCount: number; diff --git a/packages/mock-providers/src/MockedAppRootBuilder.tsx b/packages/mock-providers/src/MockedAppRootBuilder.tsx index e36b639c5596a..68120afadc06d 100644 --- a/packages/mock-providers/src/MockedAppRootBuilder.tsx +++ b/packages/mock-providers/src/MockedAppRootBuilder.tsx @@ -152,7 +152,7 @@ export class MockedAppRootBuilder { querySubscription: () => [() => () => undefined, () => this.subscriptions as unknown as ISubscription], querySubscriptions: () => [() => () => undefined, () => this.subscriptions], // apply query and option user: null, - userId: null, + userId: undefined, }; private userPresence: ContextType = { @@ -410,7 +410,7 @@ export class MockedAppRootBuilder { } withAnonymous(): this { - this.user.userId = null; + this.user.userId = undefined; this.user.user = null; return this; diff --git a/packages/ui-contexts/src/UserContext.ts b/packages/ui-contexts/src/UserContext.ts index a5c697c7d0df0..2d4872434db41 100644 --- a/packages/ui-contexts/src/UserContext.ts +++ b/packages/ui-contexts/src/UserContext.ts @@ -29,7 +29,7 @@ export type FindOptions = { }; export type UserContextValue = { - userId: string | null; + userId: string | undefined; user: IUser | null; queryPreference: ( key: string | ObjectId, @@ -52,7 +52,7 @@ export type UserContextValue = { }; export const UserContext = createContext({ - userId: null, + userId: undefined, user: null, queryPreference: () => [() => (): void => undefined, (): undefined => undefined], querySubscription: () => [() => (): void => undefined, (): undefined => undefined], diff --git a/packages/ui-contexts/src/hooks/useUserId.ts b/packages/ui-contexts/src/hooks/useUserId.ts index 393db4295104f..73a3bcaa4cea9 100644 --- a/packages/ui-contexts/src/hooks/useUserId.ts +++ b/packages/ui-contexts/src/hooks/useUserId.ts @@ -2,4 +2,4 @@ import { useContext } from 'react'; import { UserContext } from '../UserContext'; -export const useUserId = (): string | null => useContext(UserContext).userId; +export const useUserId = () => useContext(UserContext).userId; From 8d653b0f24ca7997033a4b1efd90addfc5f9d12f Mon Sep 17 00:00:00 2001 From: Tasso Date: Tue, 2 Sep 2025 16:02:05 -0300 Subject: [PATCH 2/8] Override `Meteor.userId()` --- apps/meteor/client/lib/userId.ts | 7 +++ .../client/meteor/overrides/userAndUsers.ts | 43 ++++++------------- apps/meteor/client/meteor/watchUserId.ts | 4 ++ .../providers/UserProvider/UserProvider.tsx | 20 ++++----- .../externals/meteor/accounts-base.d.ts | 4 ++ 5 files changed, 38 insertions(+), 40 deletions(-) create mode 100644 apps/meteor/client/lib/userId.ts create mode 100644 apps/meteor/client/meteor/watchUserId.ts diff --git a/apps/meteor/client/lib/userId.ts b/apps/meteor/client/lib/userId.ts new file mode 100644 index 0000000000000..9f4ad79319c85 --- /dev/null +++ b/apps/meteor/client/lib/userId.ts @@ -0,0 +1,7 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { create } from 'zustand'; + +/** + * @private do not consume this store directly + */ +export const userIdStore = create(() => undefined); diff --git a/apps/meteor/client/meteor/overrides/userAndUsers.ts b/apps/meteor/client/meteor/overrides/userAndUsers.ts index e8969df34648c..f4fc097f35a40 100644 --- a/apps/meteor/client/meteor/overrides/userAndUsers.ts +++ b/apps/meteor/client/meteor/overrides/userAndUsers.ts @@ -1,40 +1,25 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import { Accounts } from 'meteor/accounts-base'; +import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; +import { userIdStore } from '../../lib/userId'; import { Users } from '../../stores/Users'; +import { watch } from '../watch'; +import { watchUserId } from '../watchUserId'; -// assertion is needed because global Mongo.Collection differs from the `meteor/mongo` package's Mongo.Collection -Meteor.users = Users.collection as typeof Meteor.users; - -const dep = new Tracker.Dependency(); -let currentUser: IUser | undefined; - -// Watch Meteor.userId() changes Tracker.autorun(() => { - const uid = Meteor.userId(); - - // This will only run when the current user has changed; there is no need to validate by referential equality - currentUser = uid ? Users.state.get(uid) : undefined; - dep.changed(); + const userId = Accounts.connection.userId() ?? undefined; + userIdStore.setState(userId); }); -// Watch user store changes -Users.use.subscribe((state) => { - // Tracker.nonreactive is used here just to highlight that this is not a reactive computation. - // At the module level, there is almost zero chance of Tracker.active being set. - const uid = Tracker.nonreactive(() => Meteor.userId()); +Meteor.userId = () => watchUserId() ?? null; - // This lookup is fast enough to be called whenever the user store changes - const user = uid ? state.get(uid) : undefined; - - if (user !== currentUser) { - currentUser = user; - dep.changed(); - } -}); +// assertion is needed because IUser has more obligatory fields than Meteor.User +Meteor.users = Users.collection as unknown as typeof Meteor.users; // overwrite Meteor.users collection so records on it don't get erased whenever the client reconnects to websocket -Meteor.user = function user(): Meteor.User | null { - dep.depend(); - return (currentUser ?? null) as Meteor.User | null; +Meteor.user = () => { + const userId = watchUserId(); + if (!userId) return null; + return watch(Users.use, (state) => state.get(userId) as Meteor.User | undefined) ?? null; }; diff --git a/apps/meteor/client/meteor/watchUserId.ts b/apps/meteor/client/meteor/watchUserId.ts new file mode 100644 index 0000000000000..1c2dcb9a31ec9 --- /dev/null +++ b/apps/meteor/client/meteor/watchUserId.ts @@ -0,0 +1,4 @@ +import { watch } from './watch'; +import { userIdStore } from '../lib/userId'; + +export const watchUserId = () => watch(userIdStore, (state) => state); diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index fd340f3bd473d..6b23eb2428237 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -1,4 +1,4 @@ -import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IRoom } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter'; @@ -21,17 +21,13 @@ import { getUserPreference } from '../../../app/utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { afterLogoutCleanUpCallback } from '../../../lib/callbacks/afterLogoutCleanUpCallback'; import { useIdleConnection } from '../../hooks/useIdleConnection'; -import { useReactiveValue } from '../../hooks/useReactiveValue'; import { applyQueryOptions } from '../../lib/cachedStores'; import type { IDocumentMapStore } from '../../lib/cachedStores/DocumentMapStore'; import { createReactiveSubscriptionFactory } from '../../lib/createReactiveSubscriptionFactory'; +import { userIdStore } from '../../lib/userId'; import { Users, Rooms, Subscriptions } from '../../stores'; import { useSamlInviteToken } from '../../views/invite/hooks/useSamlInviteToken'; -const getUser = (): IUser | null => Meteor.user() as IUser | null; - -const getUserId = () => Meteor.userId() ?? undefined; - type UserProviderProps = { children: ReactNode; }; @@ -40,11 +36,11 @@ const ee = new Emitter(); Accounts.onLogout(() => ee.emit('logout')); ee.on('logout', async () => { - const user = getUser(); + const userId = userIdStore.getState(); + if (!userId) return; + const user = Users.state.get(userId); + if (!user) return; - if (!user) { - return; - } await afterLogoutCleanUpCallback.run(user); await sdk.call('logoutCleanUp', user); }); @@ -69,11 +65,13 @@ const queryRoom = ( }; const UserProvider = ({ children }: UserProviderProps): ReactElement => { - const userId = useReactiveValue(getUserId); + const userId = userIdStore(); + const user = Users.use((state) => { if (!userId) return null; return state.get(userId) ?? null; }); + const previousUserId = useRef(userId); const [userLanguage, setUserLanguage] = useLocalStorage('userLanguage', ''); const [preferedLanguage, setPreferedLanguage] = useLocalStorage('preferedLanguage', ''); diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index cffaa88ad8675..875b3cb5291e6 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -76,5 +76,9 @@ declare module 'meteor/accounts-base' { function serviceNames(): string[]; } + + const connection: { + userId(): string | null; + }; } } From de4d233642f8febdef3982b3beecff0a0c9db5fb Mon Sep 17 00:00:00 2001 From: Tasso Date: Tue, 2 Sep 2025 16:48:49 -0300 Subject: [PATCH 3/8] Move minimongo modules --- apps/meteor/client/cachedStores/PermissionsCachedStore.ts | 2 +- .../client/cachedStores/PrivateSettingsCachedStore.ts | 2 +- .../client/cachedStores/PublicSettingsCachedStore.ts | 2 +- apps/meteor/client/cachedStores/RoomsCachedStore.ts | 2 +- .../meteor/client/cachedStores/SubscriptionsCachedStore.ts | 2 +- .../lib/cachedStores/{utils.ts => applyQueryOptions.ts} | 0 apps/meteor/client/lib/cachedStores/index.ts | 7 ------- .../{lib/cachedStores => meteor/minimongo}/Cursor.ts | 2 +- .../{lib/cachedStores => meteor/minimongo}/DiffSequence.ts | 2 +- .../client/{lib/cachedStores => meteor/minimongo}/IdMap.ts | 0 .../minimongo}/LocalCollection.spec.ts | 0 .../cachedStores => meteor/minimongo}/LocalCollection.ts | 2 +- .../minimongo}/MinimongoCollection.ts | 4 ++-- .../cachedStores => meteor/minimongo}/MinimongoError.ts | 0 .../cachedStores => meteor/minimongo}/ObserveHandle.ts | 2 +- .../{lib/cachedStores => meteor/minimongo}/OrderedDict.ts | 0 .../cachedStores => meteor/minimongo}/SynchronousQueue.ts | 0 .../{lib/cachedStores => meteor/minimongo}/common.ts | 2 +- .../{lib/cachedStores => meteor/minimongo}/observers.ts | 0 .../cachedStores/Query.ts => meteor/minimongo/queries.ts} | 0 apps/meteor/client/meteor/overrides/unstoreLoginToken.ts | 2 +- apps/meteor/client/providers/SettingsProvider.tsx | 2 +- apps/meteor/client/providers/UserProvider/UserProvider.tsx | 2 +- apps/meteor/client/stores/Messages.ts | 3 ++- apps/meteor/client/stores/Permissions.ts | 3 ++- apps/meteor/client/stores/Roles.ts | 3 ++- apps/meteor/client/stores/Rooms.ts | 3 ++- apps/meteor/client/stores/Settings.ts | 3 ++- apps/meteor/client/stores/Subscriptions.ts | 3 ++- apps/meteor/client/stores/Users.ts | 3 ++- .../admin/permissions/hooks/usePermissionsAndRoles.ts | 2 +- .../navigation/sidepanel/hooks/useChannelsChildrenList.ts | 2 +- .../client/views/room/providers/ComposerPopupProvider.tsx | 2 +- 33 files changed, 32 insertions(+), 32 deletions(-) rename apps/meteor/client/lib/cachedStores/{utils.ts => applyQueryOptions.ts} (100%) delete mode 100644 apps/meteor/client/lib/cachedStores/index.ts rename apps/meteor/client/{lib/cachedStores => meteor/minimongo}/Cursor.ts (99%) rename apps/meteor/client/{lib/cachedStores => meteor/minimongo}/DiffSequence.ts (99%) rename apps/meteor/client/{lib/cachedStores => meteor/minimongo}/IdMap.ts (100%) rename apps/meteor/client/{lib/cachedStores => meteor/minimongo}/LocalCollection.spec.ts (100%) rename apps/meteor/client/{lib/cachedStores => meteor/minimongo}/LocalCollection.ts (99%) rename apps/meteor/client/{lib/cachedStores => meteor/minimongo}/MinimongoCollection.ts (95%) rename apps/meteor/client/{lib/cachedStores => meteor/minimongo}/MinimongoError.ts (100%) rename apps/meteor/client/{lib/cachedStores => meteor/minimongo}/ObserveHandle.ts (94%) rename apps/meteor/client/{lib/cachedStores => meteor/minimongo}/OrderedDict.ts (100%) rename apps/meteor/client/{lib/cachedStores => meteor/minimongo}/SynchronousQueue.ts (100%) rename apps/meteor/client/{lib/cachedStores => meteor/minimongo}/common.ts (98%) rename apps/meteor/client/{lib/cachedStores => meteor/minimongo}/observers.ts (100%) rename apps/meteor/client/{lib/cachedStores/Query.ts => meteor/minimongo/queries.ts} (100%) diff --git a/apps/meteor/client/cachedStores/PermissionsCachedStore.ts b/apps/meteor/client/cachedStores/PermissionsCachedStore.ts index f52f94ab3985d..e3f5ddd7f5de8 100644 --- a/apps/meteor/client/cachedStores/PermissionsCachedStore.ts +++ b/apps/meteor/client/cachedStores/PermissionsCachedStore.ts @@ -1,6 +1,6 @@ import type { IPermission } from '@rocket.chat/core-typings'; -import { PrivateCachedStore } from '../lib/cachedStores'; +import { PrivateCachedStore } from '../lib/cachedStores/CachedStore'; import { Permissions } from '../stores'; export const PermissionsCachedStore = new PrivateCachedStore({ diff --git a/apps/meteor/client/cachedStores/PrivateSettingsCachedStore.ts b/apps/meteor/client/cachedStores/PrivateSettingsCachedStore.ts index 4469b3f1f2bcb..c3054458ad25c 100644 --- a/apps/meteor/client/cachedStores/PrivateSettingsCachedStore.ts +++ b/apps/meteor/client/cachedStores/PrivateSettingsCachedStore.ts @@ -1,7 +1,7 @@ import type { ISetting } from '@rocket.chat/core-typings'; import { sdk } from '../../app/utils/client/lib/SDKClient'; -import { PrivateCachedStore } from '../lib/cachedStores'; +import { PrivateCachedStore } from '../lib/cachedStores/CachedStore'; import { PrivateSettings } from '../stores'; class PrivateSettingsCachedStore extends PrivateCachedStore { diff --git a/apps/meteor/client/cachedStores/PublicSettingsCachedStore.ts b/apps/meteor/client/cachedStores/PublicSettingsCachedStore.ts index 095b15c2fa056..ff450accd717a 100644 --- a/apps/meteor/client/cachedStores/PublicSettingsCachedStore.ts +++ b/apps/meteor/client/cachedStores/PublicSettingsCachedStore.ts @@ -1,6 +1,6 @@ import type { ISetting } from '@rocket.chat/core-typings'; -import { PublicCachedStore } from '../lib/cachedStores'; +import { PublicCachedStore } from '../lib/cachedStores/CachedStore'; import { PublicSettings } from '../stores'; class PublicSettingsCachedStore extends PublicCachedStore { diff --git a/apps/meteor/client/cachedStores/RoomsCachedStore.ts b/apps/meteor/client/cachedStores/RoomsCachedStore.ts index 1204525b6c850..6ab8c929e73b0 100644 --- a/apps/meteor/client/cachedStores/RoomsCachedStore.ts +++ b/apps/meteor/client/cachedStores/RoomsCachedStore.ts @@ -2,7 +2,7 @@ import type { IOmnichannelRoom, IRoom, IRoomWithRetentionPolicy } from '@rocket. import { DEFAULT_SLA_CONFIG, isRoomNativeFederated, LivechatPriorityWeight } from '@rocket.chat/core-typings'; import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; -import { PrivateCachedStore } from '../lib/cachedStores'; +import { PrivateCachedStore } from '../lib/cachedStores/CachedStore'; import { Rooms, Subscriptions } from '../stores'; class RoomsCachedStore extends PrivateCachedStore { diff --git a/apps/meteor/client/cachedStores/SubscriptionsCachedStore.ts b/apps/meteor/client/cachedStores/SubscriptionsCachedStore.ts index 01f1865373415..6e5fea6c2a488 100644 --- a/apps/meteor/client/cachedStores/SubscriptionsCachedStore.ts +++ b/apps/meteor/client/cachedStores/SubscriptionsCachedStore.ts @@ -2,7 +2,7 @@ import type { IOmnichannelRoom, IRoomWithRetentionPolicy, ISubscription } from ' import { DEFAULT_SLA_CONFIG, isRoomNativeFederated, LivechatPriorityWeight } from '@rocket.chat/core-typings'; import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; -import { PrivateCachedStore } from '../lib/cachedStores'; +import { PrivateCachedStore } from '../lib/cachedStores/CachedStore'; import { Rooms, Subscriptions } from '../stores'; class SubscriptionsCachedStore extends PrivateCachedStore { diff --git a/apps/meteor/client/lib/cachedStores/utils.ts b/apps/meteor/client/lib/cachedStores/applyQueryOptions.ts similarity index 100% rename from apps/meteor/client/lib/cachedStores/utils.ts rename to apps/meteor/client/lib/cachedStores/applyQueryOptions.ts diff --git a/apps/meteor/client/lib/cachedStores/index.ts b/apps/meteor/client/lib/cachedStores/index.ts deleted file mode 100644 index b54a0f462bed2..0000000000000 --- a/apps/meteor/client/lib/cachedStores/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { CachedStoresManager } from './CachedStoresManager'; -export { pipe } from './pipe'; -export { applyQueryOptions } from './utils'; -export { createDocumentMapStore, type IDocumentMapStore } from './DocumentMapStore'; -export { MinimongoCollection } from './MinimongoCollection'; -export { PublicCachedStore, PrivateCachedStore } from './CachedStore'; -export { createGlobalStore } from './createGlobalStore'; diff --git a/apps/meteor/client/lib/cachedStores/Cursor.ts b/apps/meteor/client/meteor/minimongo/Cursor.ts similarity index 99% rename from apps/meteor/client/lib/cachedStores/Cursor.ts rename to apps/meteor/client/meteor/minimongo/Cursor.ts index 96070f28f384d..f77180bde2a45 100644 --- a/apps/meteor/client/lib/cachedStores/Cursor.ts +++ b/apps/meteor/client/meteor/minimongo/Cursor.ts @@ -8,9 +8,9 @@ import type { LocalCollection } from './LocalCollection'; import { MinimongoError } from './MinimongoError'; import { ObserveHandle, ReactiveObserveHandle } from './ObserveHandle'; import { OrderedDict } from './OrderedDict'; -import type { Query, OrderedQuery, UnorderedQuery } from './Query'; import { isPlainObject, clone, hasOwn } from './common'; import type { OrderedObserver, UnorderedObserver } from './observers'; +import type { Query, OrderedQuery, UnorderedQuery } from './queries'; type Transform = ((doc: T) => any) | null | undefined; diff --git a/apps/meteor/client/lib/cachedStores/DiffSequence.ts b/apps/meteor/client/meteor/minimongo/DiffSequence.ts similarity index 99% rename from apps/meteor/client/lib/cachedStores/DiffSequence.ts rename to apps/meteor/client/meteor/minimongo/DiffSequence.ts index 7f20a683d9296..6b66285111dfe 100644 --- a/apps/meteor/client/lib/cachedStores/DiffSequence.ts +++ b/apps/meteor/client/meteor/minimongo/DiffSequence.ts @@ -1,7 +1,7 @@ -import { entriesOf } from '../objectUtils'; import type { IdMap } from './IdMap'; import { clone, hasOwn, equals } from './common'; import type { Observer, OrderedObserver, UnorderedObserver } from './observers'; +import { entriesOf } from '../../lib/objectUtils'; function isObjEmpty(obj: Record): boolean { for (const key in Object(obj)) { diff --git a/apps/meteor/client/lib/cachedStores/IdMap.ts b/apps/meteor/client/meteor/minimongo/IdMap.ts similarity index 100% rename from apps/meteor/client/lib/cachedStores/IdMap.ts rename to apps/meteor/client/meteor/minimongo/IdMap.ts diff --git a/apps/meteor/client/lib/cachedStores/LocalCollection.spec.ts b/apps/meteor/client/meteor/minimongo/LocalCollection.spec.ts similarity index 100% rename from apps/meteor/client/lib/cachedStores/LocalCollection.spec.ts rename to apps/meteor/client/meteor/minimongo/LocalCollection.spec.ts diff --git a/apps/meteor/client/lib/cachedStores/LocalCollection.ts b/apps/meteor/client/meteor/minimongo/LocalCollection.ts similarity index 99% rename from apps/meteor/client/lib/cachedStores/LocalCollection.ts rename to apps/meteor/client/meteor/minimongo/LocalCollection.ts index ebcf913fd717a..4ee6c13257e14 100644 --- a/apps/meteor/client/lib/cachedStores/LocalCollection.ts +++ b/apps/meteor/client/meteor/minimongo/LocalCollection.ts @@ -14,9 +14,9 @@ import type { Options } from './Cursor'; import { DiffSequence } from './DiffSequence'; import type { IdMap } from './IdMap'; import { MinimongoError } from './MinimongoError'; -import type { Query } from './Query'; import { SynchronousQueue } from './SynchronousQueue'; import { clone, assertHasValidFieldNames } from './common'; +import type { Query } from './queries'; /** * Forked from Meteor's Mongo.Collection, this class implements a local collection over a Zustand store. diff --git a/apps/meteor/client/lib/cachedStores/MinimongoCollection.ts b/apps/meteor/client/meteor/minimongo/MinimongoCollection.ts similarity index 95% rename from apps/meteor/client/lib/cachedStores/MinimongoCollection.ts rename to apps/meteor/client/meteor/minimongo/MinimongoCollection.ts index 34d44a6ec9613..b4c4346722b41 100644 --- a/apps/meteor/client/lib/cachedStores/MinimongoCollection.ts +++ b/apps/meteor/client/meteor/minimongo/MinimongoCollection.ts @@ -1,8 +1,8 @@ import { Mongo } from 'meteor/mongo'; -import { createDocumentMapStore } from './DocumentMapStore'; import { LocalCollection } from './LocalCollection'; -import type { Query } from './Query'; +import type { Query } from './queries'; +import { createDocumentMapStore } from '../../lib/cachedStores/DocumentMapStore'; /** * Implements a minimal version of a MongoDB collection using Zustand for state management. diff --git a/apps/meteor/client/lib/cachedStores/MinimongoError.ts b/apps/meteor/client/meteor/minimongo/MinimongoError.ts similarity index 100% rename from apps/meteor/client/lib/cachedStores/MinimongoError.ts rename to apps/meteor/client/meteor/minimongo/MinimongoError.ts diff --git a/apps/meteor/client/lib/cachedStores/ObserveHandle.ts b/apps/meteor/client/meteor/minimongo/ObserveHandle.ts similarity index 94% rename from apps/meteor/client/lib/cachedStores/ObserveHandle.ts rename to apps/meteor/client/meteor/minimongo/ObserveHandle.ts index 91e8c01590acc..6ec754e2eec7c 100644 --- a/apps/meteor/client/lib/cachedStores/ObserveHandle.ts +++ b/apps/meteor/client/meteor/minimongo/ObserveHandle.ts @@ -1,7 +1,7 @@ import { Tracker } from 'meteor/tracker'; import type { LocalCollection } from './LocalCollection'; -import type { Query } from './Query'; +import type { Query } from './queries'; export class ObserveHandle { isReady: boolean; diff --git a/apps/meteor/client/lib/cachedStores/OrderedDict.ts b/apps/meteor/client/meteor/minimongo/OrderedDict.ts similarity index 100% rename from apps/meteor/client/lib/cachedStores/OrderedDict.ts rename to apps/meteor/client/meteor/minimongo/OrderedDict.ts diff --git a/apps/meteor/client/lib/cachedStores/SynchronousQueue.ts b/apps/meteor/client/meteor/minimongo/SynchronousQueue.ts similarity index 100% rename from apps/meteor/client/lib/cachedStores/SynchronousQueue.ts rename to apps/meteor/client/meteor/minimongo/SynchronousQueue.ts diff --git a/apps/meteor/client/lib/cachedStores/common.ts b/apps/meteor/client/meteor/minimongo/common.ts similarity index 98% rename from apps/meteor/client/lib/cachedStores/common.ts rename to apps/meteor/client/meteor/minimongo/common.ts index ba9d634a178f3..01d5c96252ad2 100644 --- a/apps/meteor/client/lib/cachedStores/common.ts +++ b/apps/meteor/client/meteor/minimongo/common.ts @@ -1,6 +1,6 @@ import { getBSONType } from '@rocket.chat/mongo-adapter'; -import { entriesOf } from '../objectUtils'; +import { entriesOf } from '../../lib/objectUtils'; export const hasOwn = Object.prototype.hasOwnProperty; diff --git a/apps/meteor/client/lib/cachedStores/observers.ts b/apps/meteor/client/meteor/minimongo/observers.ts similarity index 100% rename from apps/meteor/client/lib/cachedStores/observers.ts rename to apps/meteor/client/meteor/minimongo/observers.ts diff --git a/apps/meteor/client/lib/cachedStores/Query.ts b/apps/meteor/client/meteor/minimongo/queries.ts similarity index 100% rename from apps/meteor/client/lib/cachedStores/Query.ts rename to apps/meteor/client/meteor/minimongo/queries.ts diff --git a/apps/meteor/client/meteor/overrides/unstoreLoginToken.ts b/apps/meteor/client/meteor/overrides/unstoreLoginToken.ts index 8630d5b6ebcae..8adbf1d912e9a 100644 --- a/apps/meteor/client/meteor/overrides/unstoreLoginToken.ts +++ b/apps/meteor/client/meteor/overrides/unstoreLoginToken.ts @@ -1,6 +1,6 @@ import { Accounts } from 'meteor/accounts-base'; -import { CachedStoresManager } from '../../lib/cachedStores'; +import { CachedStoresManager } from '../../lib/cachedStores/CachedStoresManager'; const { _unstoreLoginToken } = Accounts; Accounts._unstoreLoginToken = (...args) => { diff --git a/apps/meteor/client/providers/SettingsProvider.tsx b/apps/meteor/client/providers/SettingsProvider.tsx index df0ba3a8e2a3d..8010ee0e46a30 100644 --- a/apps/meteor/client/providers/SettingsProvider.tsx +++ b/apps/meteor/client/providers/SettingsProvider.tsx @@ -7,7 +7,7 @@ import type { ReactNode } from 'react'; import { useCallback, useMemo } from 'react'; import { PublicSettingsCachedStore, PrivateSettingsCachedStore } from '../cachedStores'; -import { applyQueryOptions } from '../lib/cachedStores'; +import { applyQueryOptions } from '../lib/cachedStores/applyQueryOptions'; const settingsManagementPermissions = ['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings']; diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index 6b23eb2428237..651e929919191 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -21,8 +21,8 @@ import { getUserPreference } from '../../../app/utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { afterLogoutCleanUpCallback } from '../../../lib/callbacks/afterLogoutCleanUpCallback'; import { useIdleConnection } from '../../hooks/useIdleConnection'; -import { applyQueryOptions } from '../../lib/cachedStores'; import type { IDocumentMapStore } from '../../lib/cachedStores/DocumentMapStore'; +import { applyQueryOptions } from '../../lib/cachedStores/applyQueryOptions'; import { createReactiveSubscriptionFactory } from '../../lib/createReactiveSubscriptionFactory'; import { userIdStore } from '../../lib/userId'; import { Users, Rooms, Subscriptions } from '../../stores'; diff --git a/apps/meteor/client/stores/Messages.ts b/apps/meteor/client/stores/Messages.ts index 187d25a771a8b..ff9fcdeee9f38 100644 --- a/apps/meteor/client/stores/Messages.ts +++ b/apps/meteor/client/stores/Messages.ts @@ -1,6 +1,7 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { createDocumentMapStore, createGlobalStore } from '../lib/cachedStores'; +import { createDocumentMapStore } from '../lib/cachedStores/DocumentMapStore'; +import { createGlobalStore } from '../lib/cachedStores/createGlobalStore'; /** @deprecated prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ export const Messages = diff --git a/apps/meteor/client/stores/Permissions.ts b/apps/meteor/client/stores/Permissions.ts index 0dcce02542d43..ca8899107062e 100644 --- a/apps/meteor/client/stores/Permissions.ts +++ b/apps/meteor/client/stores/Permissions.ts @@ -1,6 +1,7 @@ import type { IPermission } from '@rocket.chat/core-typings'; -import { createDocumentMapStore, createGlobalStore } from '../lib/cachedStores'; +import { createDocumentMapStore } from '../lib/cachedStores/DocumentMapStore'; +import { createGlobalStore } from '../lib/cachedStores/createGlobalStore'; /** @deprecated prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ export const Permissions = createGlobalStore(createDocumentMapStore()); diff --git a/apps/meteor/client/stores/Roles.ts b/apps/meteor/client/stores/Roles.ts index 1ddc4e71f3b2f..69b373bd13457 100644 --- a/apps/meteor/client/stores/Roles.ts +++ b/apps/meteor/client/stores/Roles.ts @@ -1,6 +1,7 @@ import type { IRole } from '@rocket.chat/core-typings'; -import { createDocumentMapStore, createGlobalStore } from '../lib/cachedStores'; +import { createDocumentMapStore } from '../lib/cachedStores/DocumentMapStore'; +import { createGlobalStore } from '../lib/cachedStores/createGlobalStore'; /** @deprecated prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ export const Roles = createGlobalStore(createDocumentMapStore()); diff --git a/apps/meteor/client/stores/Rooms.ts b/apps/meteor/client/stores/Rooms.ts index 3f2ddcbccae3d..c227d43e3e158 100644 --- a/apps/meteor/client/stores/Rooms.ts +++ b/apps/meteor/client/stores/Rooms.ts @@ -1,6 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { createDocumentMapStore, createGlobalStore } from '../lib/cachedStores'; +import { createDocumentMapStore } from '../lib/cachedStores/DocumentMapStore'; +import { createGlobalStore } from '../lib/cachedStores/createGlobalStore'; /** @deprecated prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ export const Rooms = createGlobalStore(createDocumentMapStore()); diff --git a/apps/meteor/client/stores/Settings.ts b/apps/meteor/client/stores/Settings.ts index dfe68c881cfa2..b78089728403c 100644 --- a/apps/meteor/client/stores/Settings.ts +++ b/apps/meteor/client/stores/Settings.ts @@ -1,6 +1,7 @@ import type { ISetting } from '@rocket.chat/core-typings'; -import { createDocumentMapStore, createGlobalStore } from '../lib/cachedStores'; +import { createDocumentMapStore } from '../lib/cachedStores/DocumentMapStore'; +import { createGlobalStore } from '../lib/cachedStores/createGlobalStore'; export const PublicSettings = createGlobalStore(createDocumentMapStore()); diff --git a/apps/meteor/client/stores/Subscriptions.ts b/apps/meteor/client/stores/Subscriptions.ts index ca02ed9025a12..95c6628cc2005 100644 --- a/apps/meteor/client/stores/Subscriptions.ts +++ b/apps/meteor/client/stores/Subscriptions.ts @@ -1,6 +1,7 @@ import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; -import { createDocumentMapStore, createGlobalStore } from '../lib/cachedStores'; +import { createDocumentMapStore } from '../lib/cachedStores/DocumentMapStore'; +import { createGlobalStore } from '../lib/cachedStores/createGlobalStore'; /** @deprecated prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ export const Subscriptions = createGlobalStore(createDocumentMapStore()); diff --git a/apps/meteor/client/stores/Users.ts b/apps/meteor/client/stores/Users.ts index 63125486bbc4f..96fb690b61110 100644 --- a/apps/meteor/client/stores/Users.ts +++ b/apps/meteor/client/stores/Users.ts @@ -1,6 +1,7 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { createGlobalStore, MinimongoCollection } from '../lib/cachedStores'; +import { createGlobalStore } from '../lib/cachedStores/createGlobalStore'; +import { MinimongoCollection } from '../meteor/minimongo/MinimongoCollection'; class UsersCollection extends MinimongoCollection {} diff --git a/apps/meteor/client/views/admin/permissions/hooks/usePermissionsAndRoles.ts b/apps/meteor/client/views/admin/permissions/hooks/usePermissionsAndRoles.ts index a7c1c33b48513..5799c1eb580fd 100644 --- a/apps/meteor/client/views/admin/permissions/hooks/usePermissionsAndRoles.ts +++ b/apps/meteor/client/views/admin/permissions/hooks/usePermissionsAndRoles.ts @@ -4,7 +4,7 @@ import { useShallow } from 'zustand/shallow'; import { useFilteredPermissions } from './useFilteredPermissions'; import { CONSTANTS } from '../../../../../app/authorization/lib'; -import { pipe } from '../../../../lib/cachedStores'; +import { pipe } from '../../../../lib/cachedStores/pipe'; import { Permissions, Roles } from '../../../../stores'; export const usePermissionsAndRoles = ( diff --git a/apps/meteor/client/views/navigation/sidepanel/hooks/useChannelsChildrenList.ts b/apps/meteor/client/views/navigation/sidepanel/hooks/useChannelsChildrenList.ts index d833713a87c69..13bff7cb441d1 100644 --- a/apps/meteor/client/views/navigation/sidepanel/hooks/useChannelsChildrenList.ts +++ b/apps/meteor/client/views/navigation/sidepanel/hooks/useChannelsChildrenList.ts @@ -1,7 +1,7 @@ import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; import { useShallow } from 'zustand/shallow'; -import { pipe } from '../../../../lib/cachedStores'; +import { pipe } from '../../../../lib/cachedStores/pipe'; import { Subscriptions } from '../../../../stores'; import { isUnreadSubscription } from '../../contexts/RoomsNavigationContext'; diff --git a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx index d656c607f3011..71ed4df201811 100644 --- a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx +++ b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx @@ -26,7 +26,7 @@ import type { ComposerBoxPopupUserProps } from '../composer/ComposerBoxPopupUser import type { ComposerPopupContextValue } from '../contexts/ComposerPopupContext'; import { ComposerPopupContext, createMessageBoxPopupConfig } from '../contexts/ComposerPopupContext'; import useCannedResponsesQuery from './hooks/useCannedResponsesQuery'; -import { pipe } from '../../../lib/cachedStores'; +import { pipe } from '../../../lib/cachedStores/pipe'; export type CannedResponse = { _id: string; shortcut: string; text: string }; From 9128d5ffdf24b72c0b123885b5f929b3e7452b6b Mon Sep 17 00:00:00 2001 From: Tasso Date: Tue, 2 Sep 2025 18:27:07 -0300 Subject: [PATCH 4/8] Replace `Meteor.userId()` and `Meteor.user()` invocations --- .../app/authorization/client/hasPermission.ts | 4 +- .../autotranslate/client/lib/autotranslate.ts | 22 +++---- .../app/lib/client/methods/sendMessage.ts | 7 ++- apps/meteor/app/otr/client/OTRRoom.ts | 8 +-- .../reactions/client/methods/setReaction.ts | 5 +- .../ui-utils/client/lib/RoomHistoryManager.ts | 3 +- apps/meteor/app/ui/client/lib/UserAction.ts | 6 +- apps/meteor/app/webrtc/client/WebRTCClass.ts | 24 +++----- .../client/apps/RealAppsEngineUIHost.ts | 4 +- .../meteor/client/lib/2fa/process2faReturn.ts | 5 +- .../client/lib/cachedStores/CachedStore.ts | 3 +- apps/meteor/client/lib/chats/data.ts | 11 ++-- .../lib/chats/flows/processTooLongMessage.ts | 3 +- .../client/lib/chats/readStateManager.ts | 4 +- apps/meteor/client/lib/e2ee/rocketchat.e2e.ts | 8 +-- apps/meteor/client/lib/getPermaLink.ts | 5 +- apps/meteor/client/lib/loggedIn.ts | 8 +-- .../lib/mutationEffects/starredMessage.ts | 4 +- .../meteor/client/lib/normalizeThreadTitle.ts | 44 ------------- apps/meteor/client/lib/{userId.ts => user.ts} | 10 +++ .../client/lib/utils/getUidDirectMessage.ts | 7 +-- apps/meteor/client/lib/utils/timeAgo.ts | 4 +- .../client/meteor/overrides/ddpOverREST.ts | 4 +- .../client/meteor/overrides/userAndUsers.ts | 15 ++--- apps/meteor/client/meteor/user.ts | 13 ++++ apps/meteor/client/meteor/watchUserId.ts | 4 -- .../providers/AuthorizationProvider.tsx | 28 +++++---- .../providers/UserProvider/UserProvider.tsx | 2 +- apps/meteor/client/startup/accounts.ts | 61 ++++++++++++------- apps/meteor/client/startup/iframeCommands.ts | 8 +-- .../meteor/client/startup/incomingMessages.ts | 5 +- apps/meteor/client/startup/roles.ts | 5 +- apps/meteor/client/startup/startup.ts | 3 +- .../Threads/components/ThreadTitle.tsx | 47 +++++++++++++- 34 files changed, 212 insertions(+), 182 deletions(-) delete mode 100644 apps/meteor/client/lib/normalizeThreadTitle.ts rename apps/meteor/client/lib/{userId.ts => user.ts} (50%) create mode 100644 apps/meteor/client/meteor/user.ts delete mode 100644 apps/meteor/client/meteor/watchUserId.ts diff --git a/apps/meteor/app/authorization/client/hasPermission.ts b/apps/meteor/app/authorization/client/hasPermission.ts index d007a00f8168a..7dd5e03413475 100644 --- a/apps/meteor/app/authorization/client/hasPermission.ts +++ b/apps/meteor/app/authorization/client/hasPermission.ts @@ -1,8 +1,8 @@ import type { IUser, IPermission } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; import { hasRole } from './hasRole'; import { PermissionsCachedStore } from '../../../client/cachedStores'; +import { watchUserId } from '../../../client/meteor/user'; import { watch } from '../../../client/meteor/watch'; import { Permissions, Users } from '../../../client/stores'; import { AuthorizationUtils } from '../lib/AuthorizationUtils'; @@ -50,7 +50,7 @@ const validatePermissions = ( userId?: IUser['_id'], scopedRoles?: IPermission['_id'][], ): boolean => { - userId = userId ?? Meteor.userId() ?? undefined; + userId = userId ?? watchUserId() ?? undefined; if (!userId) { return false; diff --git a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts index 830553eef587a..3cea977c8b4e4 100644 --- a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts +++ b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts @@ -1,17 +1,12 @@ -import type { - IRoom, - ISubscription, - ISupportedLanguage, - ITranslatedMessage, - IUser, - MessageAttachmentDefault, -} from '@rocket.chat/core-typings'; +import type { IRoom, ISubscription, ISupportedLanguage, ITranslatedMessage, MessageAttachmentDefault } from '@rocket.chat/core-typings'; import { isTranslatedMessageAttachment } from '@rocket.chat/core-typings'; import mem from 'mem'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { settings } from '../../../../client/lib/settings'; +import { getUserId } from '../../../../client/lib/user'; +import { watchUser, watchUserId } from '../../../../client/meteor/user'; import { Messages, Subscriptions } from '../../../../client/stores'; import { hasTranslationLanguageInAttachments, @@ -25,10 +20,9 @@ let username = ''; Meteor.startup(() => { Tracker.autorun(() => { - const user: Pick | null = Meteor.user(); - if (!user) { - return; - } + const user = watchUser(); + if (!user) return; + userLanguage = user.language || 'en'; username = user.username || ''; }); @@ -102,7 +96,7 @@ export const AutoTranslate = { } Tracker.autorun(async (c) => { - const uid = Meteor.userId(); + const uid = watchUserId(); if (!settings.watch('AutoTranslate_Enabled') || !uid || !hasPermission('auto-translate')) { return; } @@ -132,7 +126,7 @@ export const createAutoTranslateMessageStreamHandler = (): ((message: ITranslate AutoTranslate.init(); return (message: ITranslatedMessage): void => { - if (message.u && message.u._id !== Meteor.userId()) { + if (message.u && message.u._id !== getUserId()) { const subscription = AutoTranslate.findSubscriptionByRid(message.rid); const language = AutoTranslate.getLanguage(message.rid); if ( diff --git a/apps/meteor/app/lib/client/methods/sendMessage.ts b/apps/meteor/app/lib/client/methods/sendMessage.ts index 59a8f4e03616c..85117da3db345 100644 --- a/apps/meteor/app/lib/client/methods/sendMessage.ts +++ b/apps/meteor/app/lib/client/methods/sendMessage.ts @@ -1,10 +1,11 @@ -import type { IMessage, IUser } from '@rocket.chat/core-typings'; +import type { IMessage } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; import { onClientMessageReceived } from '../../../../client/lib/onClientMessageReceived'; import { settings } from '../../../../client/lib/settings'; import { dispatchToastMessage } from '../../../../client/lib/toast'; +import { getUser, getUserId } from '../../../../client/lib/user'; import { Messages, Rooms } from '../../../../client/stores'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; @@ -12,7 +13,7 @@ import { t } from '../../../utils/lib/i18n'; Meteor.methods({ async sendMessage(message) { - const uid = Meteor.userId(); + const uid = getUserId(); if (!uid || trim(message.msg) === '') { return false; } @@ -20,7 +21,7 @@ Meteor.methods({ if (messageAlreadyExists) { return dispatchToastMessage({ type: 'error', message: t('Message_Already_Sent') }); } - const user = Meteor.user() as IUser | null; + const user = getUser(); if (!user?.username) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendMessage' }); } diff --git a/apps/meteor/app/otr/client/OTRRoom.ts b/apps/meteor/app/otr/client/OTRRoom.ts index 0e861c6f4d316..b4a6b43dca13d 100644 --- a/apps/meteor/app/otr/client/OTRRoom.ts +++ b/apps/meteor/app/otr/client/OTRRoom.ts @@ -9,6 +9,7 @@ import { Tracker } from 'meteor/tracker'; import { Presence } from '../../../client/lib/presence'; import { dispatchToastMessage } from '../../../client/lib/toast'; +import { getUser } from '../../../client/lib/user'; import { getUidDirectMessage } from '../../../client/lib/utils/getUidDirectMessage'; import { goToRoomById } from '../../../client/lib/utils/goToRoomById'; import { Messages } from '../../../client/stores'; @@ -102,10 +103,9 @@ export class OTRRoom implements IOTRRoom { ]); if (refresh) { - const user = Meteor.user(); - if (!user) { - return; - } + const user = getUser(); + if (!user) return; + await sdk.rest.post('/v1/chat.otr', { roomId: this._roomId, type: otrSystemMessages.USER_REQUESTED_OTR_KEY_REFRESH, diff --git a/apps/meteor/app/reactions/client/methods/setReaction.ts b/apps/meteor/app/reactions/client/methods/setReaction.ts index 1da1a4669ddd1..96cd4bcb95910 100644 --- a/apps/meteor/app/reactions/client/methods/setReaction.ts +++ b/apps/meteor/app/reactions/client/methods/setReaction.ts @@ -3,16 +3,17 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; +import { getUser, getUserId } from '../../../../client/lib/user'; import { Rooms, Subscriptions, Messages } from '../../../../client/stores'; import { emoji } from '../../../emoji/client'; Meteor.methods({ async setReaction(reaction, messageId) { - if (!Meteor.userId()) { + if (!getUserId()) { throw new Meteor.Error(203, 'User_logged_out'); } - const user = Meteor.user(); + const user = getUser(); if (!user?.username) { return false; diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index 214427d7b7bee..f0f207b55cffe 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -7,6 +7,7 @@ import type { MutableRefObject } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { onClientMessageReceived } from '../../../../client/lib/onClientMessageReceived'; +import { getUserId } from '../../../../client/lib/user'; import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; import { getConfig } from '../../../../client/lib/utils/getConfig'; import { waitForElement } from '../../../../client/lib/utils/waitForElement'; @@ -138,7 +139,7 @@ class RoomHistoryManagerClass extends Emitter { ({ ls } = subscription); } - const showThreadsInMainChannel = getUserPreference(Meteor.userId() ?? undefined, 'showThreadsInMainChannel', false); + const showThreadsInMainChannel = getUserPreference(getUserId(), 'showThreadsInMainChannel', false); const result = await callWithErrorHandling( 'loadHistory', rid, diff --git a/apps/meteor/app/ui/client/lib/UserAction.ts b/apps/meteor/app/ui/client/lib/UserAction.ts index 8292e193908e7..061aacd739738 100644 --- a/apps/meteor/app/ui/client/lib/UserAction.ts +++ b/apps/meteor/app/ui/client/lib/UserAction.ts @@ -1,9 +1,9 @@ import type { IExtras, IRoomActivity, IUser } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import { debounce } from 'lodash'; -import { Meteor } from 'meteor/meteor'; import { settings } from '../../../../client/lib/settings'; +import { getUser, getUserId } from '../../../../client/lib/user'; import { Users } from '../../../../client/stores'; import { sdk } from '../../../utils/client/lib/SDKClient'; @@ -40,7 +40,7 @@ const shownName = function (user: IUser | null | undefined): string | undefined const emitActivities = debounce(async (rid: string, extras: IExtras): Promise => { const activities = roomActivities.get(extras?.tmid || rid) || new Set(); - sdk.publish('notify-room', [`${rid}/${USER_ACTIVITY}`, shownName(Meteor.user() as unknown as IUser), [...activities], extras]); + sdk.publish('notify-room', [`${rid}/${USER_ACTIVITY}`, shownName(getUser()), [...activities], extras]); }, 500); function handleStreamAction(rid: string, username: string, activityTypes: string[], extras?: IExtras): void { @@ -74,7 +74,7 @@ export const UserAction = new (class { } const handler = function (username: string, activityType: string[], extras?: object): void { - const uid = Meteor.userId(); + const uid = getUserId(); const user = uid ? Users.state.get(uid) : undefined; if (username === shownName(user)) { diff --git a/apps/meteor/app/webrtc/client/WebRTCClass.ts b/apps/meteor/app/webrtc/client/WebRTCClass.ts index a82ac6353f7f3..264745b00a38e 100644 --- a/apps/meteor/app/webrtc/client/WebRTCClass.ts +++ b/apps/meteor/app/webrtc/client/WebRTCClass.ts @@ -2,11 +2,11 @@ import type { IRoom } from '@rocket.chat/core-typings'; import type { StreamKeys, StreamNames, StreamerCallbackArgs } from '@rocket.chat/ddp-client'; import { Emitter } from '@rocket.chat/emitter'; import { GenericModal, imperativeModal } from '@rocket.chat/ui-client'; -import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { ChromeScreenShare } from './screenShare'; import { settings } from '../../../client/lib/settings'; +import { getUserId } from '../../../client/lib/user'; import { goToRoomById } from '../../../client/lib/utils/goToRoomById'; import { Subscriptions, Users } from '../../../client/stores'; import { sdk } from '../../utils/client/lib/SDKClient'; @@ -1035,32 +1035,28 @@ const WebRTC = new (class { } switch (subscription.t) { case 'd': - enabled = settings.watch('WebRTC_Enable_Direct') ?? false; + enabled = settings.peek('WebRTC_Enable_Direct') ?? false; break; case 'p': - enabled = settings.watch('WebRTC_Enable_Private') ?? false; + enabled = settings.peek('WebRTC_Enable_Private') ?? false; break; case 'c': - enabled = settings.watch('WebRTC_Enable_Channel') ?? false; + enabled = settings.peek('WebRTC_Enable_Channel') ?? false; break; case 'l': - enabled = settings.watch('Omnichannel_call_provider') === 'WebRTC'; + enabled = settings.peek('Omnichannel_call_provider') === 'WebRTC'; } } else { - enabled = settings.watch('Omnichannel_call_provider') === 'WebRTC'; + enabled = settings.peek('Omnichannel_call_provider') === 'WebRTC'; } - enabled = enabled && (settings.watch('WebRTC_Enabled') ?? false); + enabled = enabled && (settings.peek('WebRTC_Enabled') ?? false); if (enabled === false) { return; } if (this.instancesByRoomId[rid] == null) { - let uid = Meteor.userId()!; - let autoAccept = false; - if (visitorId) { - uid = visitorId; - autoAccept = true; - } - this.instancesByRoomId[rid] = new WebRTCClass(uid, rid, autoAccept); + const uid = visitorId ?? getUserId(); + const autoAccept = !!visitorId; + this.instancesByRoomId[rid] = new WebRTCClass(uid!, rid, autoAccept); } return this.instancesByRoomId[rid]; } diff --git a/apps/meteor/client/apps/RealAppsEngineUIHost.ts b/apps/meteor/client/apps/RealAppsEngineUIHost.ts index 54bd4c8fa84da..c6a746f6bb7a8 100644 --- a/apps/meteor/client/apps/RealAppsEngineUIHost.ts +++ b/apps/meteor/client/apps/RealAppsEngineUIHost.ts @@ -1,11 +1,11 @@ import { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHost'; import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '@rocket.chat/apps-engine/client/definition'; -import { Meteor } from 'meteor/meteor'; import { getUserAvatarURL } from '../../app/utils/client/getUserAvatarURL'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { RoomManager } from '../lib/RoomManager'; import { baseURI } from '../lib/baseURI'; +import { getUser } from '../lib/user'; import { Rooms } from '../stores'; // FIXME: replace non-null assertions with proper error handling @@ -59,7 +59,7 @@ export class RealAppsEngineUIHost extends AppsEngineUIHost { } async getClientUserInfo(): Promise { - const { username, _id } = Meteor.user()!; + const { username, _id } = getUser()!; return { id: _id, diff --git a/apps/meteor/client/lib/2fa/process2faReturn.ts b/apps/meteor/client/lib/2fa/process2faReturn.ts index 4f81b49c1f07c..4042370caae39 100644 --- a/apps/meteor/client/lib/2fa/process2faReturn.ts +++ b/apps/meteor/client/lib/2fa/process2faReturn.ts @@ -5,6 +5,7 @@ import { lazy } from 'react'; import type { LoginCallback } from './overrideLoginMethod'; import { isTotpInvalidError, isTotpRequiredError } from './utils'; +import { getUser } from '../user'; const TwoFactorModal = lazy(() => import('../../components/TwoFactorModal')); @@ -46,7 +47,7 @@ const getProps = ( case 'email': return { method, - emailOrUsername: typeof emailOrUsername === 'string' ? emailOrUsername : Meteor.user()?.username, + emailOrUsername: typeof emailOrUsername === 'string' ? emailOrUsername : getUser()?.username, }; case 'password': return { method }; @@ -109,7 +110,7 @@ export async function process2faAsyncReturn({ const props = { method: error.details.method, - emailOrUsername: emailOrUsername || error.details.emailOrUsername || Meteor.user()?.username, + emailOrUsername: emailOrUsername || error.details.emailOrUsername || getUser()?.username, // eslint-disable-next-line no-nested-ternary invalidAttempt: isTotpInvalidError(error), }; diff --git a/apps/meteor/client/lib/cachedStores/CachedStore.ts b/apps/meteor/client/lib/cachedStores/CachedStore.ts index b28bdd057679b..63ee34a4d5ddc 100644 --- a/apps/meteor/client/lib/cachedStores/CachedStore.ts +++ b/apps/meteor/client/lib/cachedStores/CachedStore.ts @@ -14,6 +14,7 @@ import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { isTruthy } from '../../../lib/isTruthy'; import { withDebouncing } from '../../../lib/utils/highOrderFunctions'; import { watch } from '../../meteor/watch'; +import { getUserId } from '../user'; import { getConfig } from '../utils/getConfig'; type Name = 'rooms' | 'subscriptions' | 'permissions' | 'public-settings' | 'private-settings'; @@ -74,7 +75,7 @@ export abstract class CachedStore implements protected get eventName(): `${Name}-changed` | `${string}/${Name}-changed` { if (this.eventType === 'notify-user') { - return `${Meteor.userId()}/${this.name}-changed`; + return `${getUserId()}/${this.name}-changed`; } return `${this.name}-changed`; } diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts index eb5c5698750f4..df508b28325b4 100644 --- a/apps/meteor/client/lib/chats/data.ts +++ b/apps/meteor/client/lib/chats/data.ts @@ -15,6 +15,7 @@ import { hasAtLeastOnePermission, hasPermission } from '../../../app/authorizati import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { Messages, Rooms, Subscriptions } from '../../stores'; import { settings } from '../settings'; +import { getUserId } from '../user'; import { prependReplies } from '../utils/prependReplies'; export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage['_id'] | undefined }): DataAPI => { @@ -78,7 +79,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage const canEditMessage = hasAtLeastOnePermission('edit-message', message.rid); const editAllowed = (settings.peek('Message_AllowEditing') as boolean | undefined) ?? false; - const editOwn = message?.u && message.u._id === Meteor.userId(); + const editOwn = message?.u && message.u._id === getUserId(); if (!canEditMessage && (!editAllowed || !editOwn)) { return false; @@ -96,7 +97,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage }; const findPreviousOwnMessage = async (message?: IMessage): Promise => { - const uid = Meteor.userId(); + const uid = getUserId(); if (!uid) { return undefined; @@ -134,7 +135,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage }; const findNextOwnMessage = async (message: IMessage): Promise => { - const uid = Meteor.userId(); + const uid = getUserId(); if (!uid) { return undefined; @@ -179,7 +180,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage sdk.call('updateMessage', message, previewUrls); const canDeleteMessage = async (message: IMessage): Promise => { - const uid = Meteor.userId(); + const uid = getUserId(); if (!uid) { return false; @@ -201,7 +202,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage const deleteAnyAllowed = hasPermission('delete-message', rid); const deleteOwnAllowed = hasPermission('delete-own-message'); - const deleteAllowed = deleteAnyAllowed || (deleteOwnAllowed && message?.u && message.u._id === Meteor.userId()); + const deleteAllowed = deleteAnyAllowed || (deleteOwnAllowed && message?.u && message.u._id === getUserId()); if (!deleteAllowed) { return false; diff --git a/apps/meteor/client/lib/chats/flows/processTooLongMessage.ts b/apps/meteor/client/lib/chats/flows/processTooLongMessage.ts index 3b05a016212cb..972984dd6ffd8 100644 --- a/apps/meteor/client/lib/chats/flows/processTooLongMessage.ts +++ b/apps/meteor/client/lib/chats/flows/processTooLongMessage.ts @@ -4,6 +4,7 @@ import { GenericModal, imperativeModal } from '@rocket.chat/ui-client'; import { t } from '../../../../app/utils/lib/i18n'; import { settings } from '../../settings'; import { dispatchToastMessage } from '../../toast'; +import { getUser } from '../../user'; import type { ChatAPI } from '../ChatAPI'; export const processTooLongMessage = async (chat: ChatAPI, { msg }: Pick): Promise => { @@ -25,7 +26,7 @@ export const processTooLongMessage = async (chat: ChatAPI, { msg }: Pick => { const contentType = 'text/plain'; const messageBlob = new Blob([msg], { type: contentType }); - const fileName = `${Meteor.user()?.username ?? 'anonymous'} - ${new Date()}.txt`; // TODO: proper naming and formatting + const fileName = `${getUser()?.username ?? 'anonymous'} - ${new Date()}.txt`; // TODO: proper naming and formatting const file = new File([messageBlob], fileName, { type: contentType, lastModified: Date.now(), diff --git a/apps/meteor/client/lib/chats/readStateManager.ts b/apps/meteor/client/lib/chats/readStateManager.ts index 80449dcac246f..4827ecdc97ba7 100644 --- a/apps/meteor/client/lib/chats/readStateManager.ts +++ b/apps/meteor/client/lib/chats/readStateManager.ts @@ -1,12 +1,12 @@ import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; -import { Meteor } from 'meteor/meteor'; import { LegacyRoomManager } from '../../../app/ui-utils/client/lib/LegacyRoomManager'; import { RoomHistoryManager } from '../../../app/ui-utils/client/lib/RoomHistoryManager'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { withDebouncing } from '../../../lib/utils/highOrderFunctions'; import { Messages } from '../../stores'; +import { getUserId } from '../user'; export class ReadStateManager extends Emitter { private rid: IRoom['_id']; @@ -76,7 +76,7 @@ export class ReadStateManager extends Emitter { const firstUnreadRecord = Messages.state.findFirst( (record) => - record.rid === this.subscription?.rid && record.ts.getTime() > this.subscription.ls.getTime() && record.u._id !== Meteor.userId(), + record.rid === this.subscription?.rid && record.ts.getTime() > this.subscription.ls.getTime() && record.u._id !== getUserId(), (a, b) => a.ts.getTime() - b.ts.getTime(), ); diff --git a/apps/meteor/client/lib/e2ee/rocketchat.e2e.ts b/apps/meteor/client/lib/e2ee/rocketchat.e2e.ts index cd42765297d49..e1a42f6c792ab 100644 --- a/apps/meteor/client/lib/e2ee/rocketchat.e2e.ts +++ b/apps/meteor/client/lib/e2ee/rocketchat.e2e.ts @@ -8,7 +8,6 @@ import { imperativeModal } from '@rocket.chat/ui-client'; import EJSON from 'ejson'; import _ from 'lodash'; import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; import { E2EEState } from './E2EEState'; import { @@ -41,6 +40,7 @@ import * as banners from '../banners'; import type { LegacyBannerPayload } from '../banners'; import { settings } from '../settings'; import { dispatchToastMessage } from '../toast'; +import { getUserId } from '../user'; import { mapMessageFromApi } from '../utils/mapMessageFromApi'; let failedToDecodeKey = false; @@ -269,7 +269,7 @@ class E2E extends Emitter { return null; } - const userId = Meteor.userId(); + const userId = getUserId(); if (!this.instancesByRoomId[rid] && userId) { this.instancesByRoomId[rid] = new E2ERoom(userId, room); } @@ -553,7 +553,7 @@ class E2E extends Emitter { // Derive a key from the password try { - return await deriveKey(toArrayBuffer(Meteor.userId()), baseKey); + return await deriveKey(toArrayBuffer(getUserId()), baseKey); } catch (error) { this.setState(E2EEState.ERROR); return this.error('Error deriving baseKey: ', error); @@ -853,7 +853,7 @@ class E2E extends Emitter { } const predicate = (record: IRoom) => - Boolean('usersWaitingForE2EKeys' in record && record.usersWaitingForE2EKeys?.every((user) => user.userId !== Meteor.userId())); + Boolean('usersWaitingForE2EKeys' in record && record.usersWaitingForE2EKeys?.every((user) => user.userId !== getUserId())); const keyDistribution = async () => { const roomIds = Rooms.state.filter(predicate).map((room) => room._id); diff --git a/apps/meteor/client/lib/getPermaLink.ts b/apps/meteor/client/lib/getPermaLink.ts index 9710d70cb8ba7..72a1b268c13f4 100644 --- a/apps/meteor/client/lib/getPermaLink.ts +++ b/apps/meteor/client/lib/getPermaLink.ts @@ -1,5 +1,6 @@ import type { IMessage, Serialized } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; + +import { getUserId } from './user'; const getMessage = async (msgId: string): Promise | null> => { try { @@ -28,7 +29,7 @@ export const getPermaLink = async (msgId: string): Promise => { throw new Error('room-not-found'); } - const subData = Subscriptions.state.find((record) => record.rid === roomData._id && record.u._id === Meteor.userId()); + const subData = Subscriptions.state.find((record) => record.rid === roomData._id && record.u._id === getUserId()); const { roomCoordinator } = await import('./rooms/roomCoordinator'); diff --git a/apps/meteor/client/lib/loggedIn.ts b/apps/meteor/client/lib/loggedIn.ts index 033727ed98365..859ff2db7a4de 100644 --- a/apps/meteor/client/lib/loggedIn.ts +++ b/apps/meteor/client/lib/loggedIn.ts @@ -1,10 +1,10 @@ import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; + +import { getUserId } from './user'; const isLoggedIn = () => { - const uid = Tracker.nonreactive(() => Meteor.userId()); - return uid !== null; + const uid = getUserId(); + return !!uid; }; export const whenLoggedIn = () => { diff --git a/apps/meteor/client/lib/mutationEffects/starredMessage.ts b/apps/meteor/client/lib/mutationEffects/starredMessage.ts index 95cc58a7b8916..99fe06bee3436 100644 --- a/apps/meteor/client/lib/mutationEffects/starredMessage.ts +++ b/apps/meteor/client/lib/mutationEffects/starredMessage.ts @@ -1,10 +1,10 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; import { Messages } from '../../stores'; +import { getUserId } from '../user'; export const toggleStarredMessage = (message: IMessage, starred: boolean) => { - const uid = Meteor.userId()!; + const uid = getUserId()!; if (starred) { Messages.state.update( diff --git a/apps/meteor/client/lib/normalizeThreadTitle.ts b/apps/meteor/client/lib/normalizeThreadTitle.ts deleted file mode 100644 index 52195e34ff5af..0000000000000 --- a/apps/meteor/client/lib/normalizeThreadTitle.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { IMessage } from '@rocket.chat/core-typings'; -import { escapeHTML } from '@rocket.chat/string-helpers'; -import { Meteor } from 'meteor/meteor'; - -import { settings } from './settings'; -import { emojiParser } from '../../app/emoji/client/emojiParser'; -import { filterMarkdown } from '../../app/markdown/lib/markdown'; -import { MentionsParser } from '../../app/mentions/lib/MentionsParser'; -import { Users } from '../stores'; - -export function normalizeThreadTitle({ ...message }: Readonly) { - if (message.msg) { - const filteredMessage = filterMarkdown(escapeHTML(message.msg)); - if (!message.channels && !message.mentions) { - return filteredMessage; - } - const uid = Meteor.userId(); - const me = (uid && Users.state.get(uid)?.username) || ''; - const pattern = settings.peek('UTF8_User_Names_Validation'); - const useRealName = settings.peek('UI_Use_Real_Name'); - - const instance = new MentionsParser({ - pattern: () => pattern, - useRealName: () => useRealName, - me: () => me, - userTemplate: ({ label }) => ` ${label} `, - roomTemplate: ({ prefix, mention }) => `${prefix} ${mention} `, - }); - const html = emojiParser(filteredMessage); - return instance.parse({ ...message, msg: filteredMessage, html }).html; - } - - if (message.attachments) { - const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); - - if (attachment?.description) { - return escapeHTML(attachment.description); - } - - if (attachment?.title) { - return escapeHTML(attachment.title); - } - } -} diff --git a/apps/meteor/client/lib/userId.ts b/apps/meteor/client/lib/user.ts similarity index 50% rename from apps/meteor/client/lib/userId.ts rename to apps/meteor/client/lib/user.ts index 9f4ad79319c85..eba7bd9e753dd 100644 --- a/apps/meteor/client/lib/userId.ts +++ b/apps/meteor/client/lib/user.ts @@ -1,7 +1,17 @@ import type { IUser } from '@rocket.chat/core-typings'; import { create } from 'zustand'; +import { Users } from '../stores'; + /** * @private do not consume this store directly */ export const userIdStore = create(() => undefined); + +export const getUserId = () => userIdStore.getState(); + +export const getUser = () => { + const userId = getUserId(); + if (!userId) return undefined; + return Users.state.get(userId); +}; diff --git a/apps/meteor/client/lib/utils/getUidDirectMessage.ts b/apps/meteor/client/lib/utils/getUidDirectMessage.ts index 0712173381dcc..6f56110629855 100644 --- a/apps/meteor/client/lib/utils/getUidDirectMessage.ts +++ b/apps/meteor/client/lib/utils/getUidDirectMessage.ts @@ -1,12 +1,9 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; import { Rooms } from '../../stores'; +import { getUserId } from '../user'; -export const getUidDirectMessage = ( - rid: IRoom['_id'], - uid: IUser['_id'] | undefined = Meteor.userId() ?? undefined, -): string | undefined => { +export const getUidDirectMessage = (rid: IRoom['_id'], uid: IUser['_id'] | undefined = getUserId() ?? undefined): string | undefined => { const room = Rooms.state.get(rid); if (!room || room.t !== 'd' || !room.uids || room.uids.length > 2) { diff --git a/apps/meteor/client/lib/utils/timeAgo.ts b/apps/meteor/client/lib/utils/timeAgo.ts index 9dedfca9c1ba7..98e5d936c2d6b 100644 --- a/apps/meteor/client/lib/utils/timeAgo.ts +++ b/apps/meteor/client/lib/utils/timeAgo.ts @@ -1,4 +1,3 @@ -import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import type { Moment, MomentInput } from 'moment'; import moment from 'moment'; @@ -6,11 +5,12 @@ import moment from 'moment'; import { getUserPreference } from '../../../app/utils/client'; import { t } from '../../../app/utils/lib/i18n'; import { settings } from '../settings'; +import { getUserId } from '../user'; const dayFormat = ['h:mm A', 'H:mm']; export const timeAgo = (date: MomentInput) => { - const clockMode = Tracker.nonreactive(() => getUserPreference(Meteor.userId() ?? undefined, 'clockMode', false) as number | boolean); + const clockMode = Tracker.nonreactive(() => getUserPreference(getUserId(), 'clockMode', false) as number | boolean); const messageTimeFormat = settings.peek('Message_TimeFormat'); const sameDay = (typeof clockMode === 'number' ? dayFormat[clockMode - 1] : undefined) || messageTimeFormat; diff --git a/apps/meteor/client/meteor/overrides/ddpOverREST.ts b/apps/meteor/client/meteor/overrides/ddpOverREST.ts index d3781e87a6af2..305b9d6b002a6 100644 --- a/apps/meteor/client/meteor/overrides/ddpOverREST.ts +++ b/apps/meteor/client/meteor/overrides/ddpOverREST.ts @@ -1,8 +1,8 @@ import { DDPCommon } from 'meteor/ddp-common'; import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { getUserId } from '../../lib/user'; const bypassMethods: string[] = ['setUserStatus', 'logout']; @@ -32,7 +32,7 @@ const withDDPOverREST = (_send: (this: Meteor.IMeteorConnection, message: Meteor return _send.call(this, message, ...args); } - const endpoint = Tracker.nonreactive(() => (!Meteor.userId() ? 'method.callAnon' : 'method.call')); + const endpoint = !getUserId() ? 'method.callAnon' : 'method.call'; const restParams = { message: DDPCommon.stringifyDDP({ ...message }), diff --git a/apps/meteor/client/meteor/overrides/userAndUsers.ts b/apps/meteor/client/meteor/overrides/userAndUsers.ts index f4fc097f35a40..f603a9aaaf8a8 100644 --- a/apps/meteor/client/meteor/overrides/userAndUsers.ts +++ b/apps/meteor/client/meteor/overrides/userAndUsers.ts @@ -2,10 +2,9 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { userIdStore } from '../../lib/userId'; +import { userIdStore } from '../../lib/user'; import { Users } from '../../stores/Users'; -import { watch } from '../watch'; -import { watchUserId } from '../watchUserId'; +import { watchUser, watchUserId } from '../user'; Tracker.autorun(() => { const userId = Accounts.connection.userId() ?? undefined; @@ -14,12 +13,8 @@ Tracker.autorun(() => { Meteor.userId = () => watchUserId() ?? null; +// overwrite Meteor.users collection so records on it don't get erased whenever the client reconnects to websocket +Meteor.user = () => (watchUser() as Meteor.User | undefined) ?? null; + // assertion is needed because IUser has more obligatory fields than Meteor.User Meteor.users = Users.collection as unknown as typeof Meteor.users; - -// overwrite Meteor.users collection so records on it don't get erased whenever the client reconnects to websocket -Meteor.user = () => { - const userId = watchUserId(); - if (!userId) return null; - return watch(Users.use, (state) => state.get(userId) as Meteor.User | undefined) ?? null; -}; diff --git a/apps/meteor/client/meteor/user.ts b/apps/meteor/client/meteor/user.ts new file mode 100644 index 0000000000000..c7e3e7b90bf5c --- /dev/null +++ b/apps/meteor/client/meteor/user.ts @@ -0,0 +1,13 @@ +import type { IUser } from '@rocket.chat/core-typings'; + +import { watch } from './watch'; +import { userIdStore } from '../lib/user'; +import { Users } from '../stores'; + +export const watchUserId = (): IUser['_id'] | undefined => watch(userIdStore, (state) => state); + +export const watchUser = (): IUser | undefined => { + const userId = watchUserId(); + if (!userId) return undefined; + return watch(Users.use, (state) => state.get(userId)); +}; diff --git a/apps/meteor/client/meteor/watchUserId.ts b/apps/meteor/client/meteor/watchUserId.ts deleted file mode 100644 index 1c2dcb9a31ec9..0000000000000 --- a/apps/meteor/client/meteor/watchUserId.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { watch } from './watch'; -import { userIdStore } from '../lib/userId'; - -export const watchUserId = () => watch(userIdStore, (state) => state); diff --git a/apps/meteor/client/providers/AuthorizationProvider.tsx b/apps/meteor/client/providers/AuthorizationProvider.tsx index d03b4bf8ff63f..542c90f3ca366 100644 --- a/apps/meteor/client/providers/AuthorizationProvider.tsx +++ b/apps/meteor/client/providers/AuthorizationProvider.tsx @@ -1,22 +1,12 @@ -import { AuthorizationContext } from '@rocket.chat/ui-contexts'; -import { Meteor } from 'meteor/meteor'; +import { AuthorizationContext, useUserId } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { hasPermission, hasAtLeastOnePermission, hasAllPermission, hasRole } from '../../app/authorization/client'; import { PermissionsCachedStore } from '../cachedStores'; import { createReactiveSubscriptionFactory } from '../lib/createReactiveSubscriptionFactory'; import { Roles } from '../stores'; -const contextValue = { - queryPermission: createReactiveSubscriptionFactory((permission, scope, scopeRoles) => hasPermission(permission, scope, scopeRoles)), - queryAtLeastOnePermission: createReactiveSubscriptionFactory((permissions, scope) => hasAtLeastOnePermission(permissions, scope)), - queryAllPermissions: createReactiveSubscriptionFactory((permissions, scope) => hasAllPermission(permissions, scope)), - queryRole: createReactiveSubscriptionFactory((role, scope?) => !!Meteor.userId() && hasRole(Meteor.userId() as string, role, scope)), - getRoles: () => Roles.state.records, - subscribeToRoles: (callback: () => void) => Roles.use.subscribe(callback), -}; - type AuthorizationProviderProps = { children?: ReactNode; }; @@ -34,6 +24,20 @@ const AuthorizationProvider = ({ children }: AuthorizationProviderProps) => { })(); } + const userId = useUserId(); + + const contextValue = useMemo( + () => ({ + queryPermission: createReactiveSubscriptionFactory((permission, scope, scopeRoles) => hasPermission(permission, scope, scopeRoles)), + queryAtLeastOnePermission: createReactiveSubscriptionFactory((permissions, scope) => hasAtLeastOnePermission(permissions, scope)), + queryAllPermissions: createReactiveSubscriptionFactory((permissions, scope) => hasAllPermission(permissions, scope)), + queryRole: createReactiveSubscriptionFactory((role, scope?) => !!userId && hasRole(userId, role, scope)), + getRoles: () => Roles.state.records, + subscribeToRoles: (callback: () => void) => Roles.use.subscribe(callback), + }), + [userId], + ); + return ; }; diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index 651e929919191..5b641d50da0c8 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -24,7 +24,7 @@ import { useIdleConnection } from '../../hooks/useIdleConnection'; import type { IDocumentMapStore } from '../../lib/cachedStores/DocumentMapStore'; import { applyQueryOptions } from '../../lib/cachedStores/applyQueryOptions'; import { createReactiveSubscriptionFactory } from '../../lib/createReactiveSubscriptionFactory'; -import { userIdStore } from '../../lib/userId'; +import { userIdStore } from '../../lib/user'; import { Users, Rooms, Subscriptions } from '../../stores'; import { useSamlInviteToken } from '../../views/invite/hooks/useSamlInviteToken'; diff --git a/apps/meteor/client/startup/accounts.ts b/apps/meteor/client/startup/accounts.ts index c6a38360ebbea..d61734864823d 100644 --- a/apps/meteor/client/startup/accounts.ts +++ b/apps/meteor/client/startup/accounts.ts @@ -1,37 +1,54 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { t } from '../../app/utils/lib/i18n'; import { PublicSettingsCachedStore, SubscriptionsCachedStore } from '../cachedStores'; import { dispatchToastMessage } from '../lib/toast'; +import { userIdStore } from '../lib/user'; import { useUserDataSyncReady } from '../lib/userData'; -import { watch } from '../meteor/watch'; -const watchMainReady = () => { - const uid = Meteor.userId(); - const subscriptionsReady = watch(SubscriptionsCachedStore.useReady, (state) => state); - const settingsReady = watch(PublicSettingsCachedStore.useReady, (state) => state); - const userDataReady = watch(useUserDataSyncReady, (state) => state); +const whenMainReady = (): Promise => { + const isMainReady = (): boolean => { + const uid = userIdStore.getState(); + if (!uid) return true; - return !uid || (userDataReady && subscriptionsReady && settingsReady); + const subscriptionsReady = SubscriptionsCachedStore.useReady.getState(); + const settingsReady = PublicSettingsCachedStore.useReady.getState(); + const userDataReady = useUserDataSyncReady.getState(); + + return userDataReady && subscriptionsReady && settingsReady; + }; + + if (isMainReady()) return Promise.resolve(); + + return new Promise((resolve) => { + const checkAndResolve = () => { + if (!isMainReady()) return; + unsubscribeUserId(); + unsubscribeSubscriptions(); + unsubscribeSettings(); + unsubscribeUserData(); + resolve(); + }; + + const unsubscribeUserId = userIdStore.subscribe(checkAndResolve); + const unsubscribeSubscriptions = SubscriptionsCachedStore.useReady.subscribe(checkAndResolve); + const unsubscribeSettings = PublicSettingsCachedStore.useReady.subscribe(checkAndResolve); + const unsubscribeUserData = useUserDataSyncReady.subscribe(checkAndResolve); + }); }; Accounts.onEmailVerificationLink((token: string) => { - Accounts.verifyEmail(token, (error) => { - Tracker.autorun(() => { - if (!watchMainReady()) return; - - if (error) { - dispatchToastMessage({ type: 'error', message: error }); - throw new Meteor.Error('verify-email', 'E-mail not verified'); - } else { - Tracker.nonreactive(() => { - void sdk.call('afterVerifyEmail'); - }); - dispatchToastMessage({ type: 'success', message: t('Email_verified') }); - } - }); + Accounts.verifyEmail(token, async (error) => { + await whenMainReady(); + + if (error) { + dispatchToastMessage({ type: 'error', message: error }); + throw new Meteor.Error('verify-email', 'E-mail not verified'); + } else { + void sdk.call('afterVerifyEmail'); + dispatchToastMessage({ type: 'success', message: t('Email_verified') }); + } }); }); diff --git a/apps/meteor/client/startup/iframeCommands.ts b/apps/meteor/client/startup/iframeCommands.ts index e85285ea1fa32..2c5582e2d9d0b 100644 --- a/apps/meteor/client/startup/iframeCommands.ts +++ b/apps/meteor/client/startup/iframeCommands.ts @@ -10,6 +10,7 @@ import { capitalize, ltrim, rtrim } from '../../lib/utils/stringUtils'; import { baseURI } from '../lib/baseURI'; import { loginServices } from '../lib/loginServices'; import { settings } from '../lib/settings'; +import { getUser } from '../lib/user'; import { router } from '../providers/RouterProvider'; const commands = { @@ -77,11 +78,10 @@ const commands = { }, async 'logout'() { - const user = Meteor.user(); + const user = getUser(); Meteor.logout(() => { - if (!user) { - return; - } + if (!user) return; + void afterLogoutCleanUpCallback.run(user); sdk.call('logoutCleanUp', user as unknown as IUser); return router.navigate('/home'); diff --git a/apps/meteor/client/startup/incomingMessages.ts b/apps/meteor/client/startup/incomingMessages.ts index 6aaf2f6af1931..ac840725e9bf4 100644 --- a/apps/meteor/client/startup/incomingMessages.ts +++ b/apps/meteor/client/startup/incomingMessages.ts @@ -3,13 +3,14 @@ import { Meteor } from 'meteor/meteor'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { onLoggedIn } from '../lib/loggedIn'; +import { getUserId } from '../lib/user'; import { Messages } from '../stores'; Meteor.startup(() => { onLoggedIn(() => { // Only event I found triggers this is from ephemeral messages // Other types of messages come from another stream - return sdk.stream('notify-user', [`${Meteor.userId()}/message`], (msg: IMessage) => { + return sdk.stream('notify-user', [`${getUserId()}/message`], (msg: IMessage) => { msg.u = msg.u || { username: 'rocket.cat' }; msg.private = true; @@ -18,7 +19,7 @@ Meteor.startup(() => { }); onLoggedIn(() => { - return sdk.stream('notify-user', [`${Meteor.userId()}/subscriptions-changed`], (_action, sub) => { + return sdk.stream('notify-user', [`${getUserId()}/subscriptions-changed`], (_action, sub) => { Messages.state.update( (record) => record.rid === sub.rid && ('ignored' in sub && sub.ignored ? !sub.ignored.includes(record.u._id) : 'ignored' in record), ({ ignored: _, ...record }) => record, diff --git a/apps/meteor/client/startup/roles.ts b/apps/meteor/client/startup/roles.ts index 7491462f77b80..de1121d25d69d 100644 --- a/apps/meteor/client/startup/roles.ts +++ b/apps/meteor/client/startup/roles.ts @@ -4,6 +4,7 @@ import { Tracker } from 'meteor/tracker'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { onLoggedIn } from '../lib/loggedIn'; +import { watchUserId } from '../meteor/user'; import { Roles } from '../stores'; Meteor.startup(() => { @@ -26,9 +27,7 @@ Meteor.startup(() => { }; Tracker.autorun((c) => { - if (!Meteor.userId()) { - return; - } + if (!watchUserId()) return; Tracker.afterFlush(() => { sdk.stream('roles', ['roles'], (role) => { diff --git a/apps/meteor/client/startup/startup.ts b/apps/meteor/client/startup/startup.ts index 21747fc94e691..652b85045f19a 100644 --- a/apps/meteor/client/startup/startup.ts +++ b/apps/meteor/client/startup/startup.ts @@ -7,13 +7,14 @@ import 'highlight.js/styles/github.css'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { synchronizeUserData, removeLocalUserData } from '../lib/userData'; import { fireGlobalEvent } from '../lib/utils/fireGlobalEvent'; +import { watchUserId } from '../meteor/user'; Meteor.startup(() => { fireGlobalEvent('startup', true); let status: UserStatus | undefined = undefined; Tracker.autorun(async () => { - const uid = Meteor.userId(); + const uid = watchUserId(); if (!uid) { removeLocalUserData(); return; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx index 7c8bd0cbc6b10..56ac6b2960eb0 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx @@ -1,15 +1,58 @@ import type { IThreadMainMessage } from '@rocket.chat/core-typings'; +import { escapeHTML } from '@rocket.chat/string-helpers'; +import { useSetting, useUser } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; +import { emojiParser } from '../../../../../../app/emoji/client/emojiParser'; +import { filterMarkdown } from '../../../../../../app/markdown/lib/markdown'; +import { MentionsParser } from '../../../../../../app/mentions/lib/MentionsParser'; import { ContextualbarTitle } from '../../../../../components/Contextualbar'; -import { normalizeThreadTitle } from '../../../../../lib/normalizeThreadTitle'; type ThreadTitleProps = { mainMessage: IThreadMainMessage; }; const ThreadTitle = ({ mainMessage }: ThreadTitleProps) => { - const innerHTML = useMemo(() => ({ __html: normalizeThreadTitle(mainMessage) ?? '' }), [mainMessage]); + const me = useUser()?.username || ''; + const pattern = useSetting('UTF8_User_Names_Validation', '[0-9a-zA-Z-_.]+'); + const useRealName = useSetting('UI_Use_Real_Name', false); + + const html = useMemo((): string => { + const message = { ...mainMessage }; + + if (message.msg) { + const filteredMessage = filterMarkdown(escapeHTML(message.msg)); + if (!message.channels && !message.mentions) { + return filteredMessage; + } + + const instance = new MentionsParser({ + pattern: () => pattern, + useRealName: () => useRealName, + me: () => me, + userTemplate: ({ label }) => ` ${label} `, + roomTemplate: ({ prefix, mention }) => `${prefix} ${mention} `, + }); + const html = emojiParser(filteredMessage); + return instance.parse({ ...message, msg: filteredMessage, html }).html ?? ''; + } + + if (message.attachments) { + const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); + + if (attachment?.description) { + return escapeHTML(attachment.description); + } + + if (attachment?.title) { + return escapeHTML(attachment.title); + } + } + + return ''; + }, [mainMessage, me, pattern, useRealName]); + + const innerHTML = useMemo(() => ({ __html: html }), [html]); return ; }; From 7a7307e38a842a9005be7d21d897b8cfd61e472c Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 29 Sep 2025 15:35:11 -0300 Subject: [PATCH 5/8] Add early-return --- apps/meteor/app/webrtc/client/WebRTCClass.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/webrtc/client/WebRTCClass.ts b/apps/meteor/app/webrtc/client/WebRTCClass.ts index 264745b00a38e..28877346bacd6 100644 --- a/apps/meteor/app/webrtc/client/WebRTCClass.ts +++ b/apps/meteor/app/webrtc/client/WebRTCClass.ts @@ -1055,8 +1055,9 @@ const WebRTC = new (class { } if (this.instancesByRoomId[rid] == null) { const uid = visitorId ?? getUserId(); + if (!uid) return undefined; const autoAccept = !!visitorId; - this.instancesByRoomId[rid] = new WebRTCClass(uid!, rid, autoAccept); + this.instancesByRoomId[rid] = new WebRTCClass(uid, rid, autoAccept); } return this.instancesByRoomId[rid]; } From 61eaee7f8b56b5f9327159bb157d15f821bbcf81 Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 29 Sep 2025 16:09:15 -0300 Subject: [PATCH 6/8] Expand comment --- apps/meteor/client/lib/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/user.ts b/apps/meteor/client/lib/user.ts index eba7bd9e753dd..c41ccbcb419d2 100644 --- a/apps/meteor/client/lib/user.ts +++ b/apps/meteor/client/lib/user.ts @@ -4,7 +4,7 @@ import { create } from 'zustand'; import { Users } from '../stores'; /** - * @private do not consume this store directly + * @private do not consume this store directly -- consume it via UserContext */ export const userIdStore = create(() => undefined); From 3b40ed65acdd6aae0413db2f200031b5b321f734 Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 29 Sep 2025 16:33:10 -0300 Subject: [PATCH 7/8] Split hook --- .../Threads/components/ThreadTitle.tsx | 45 +---------------- .../hooks/useNormalizedThreadTitleHtml.ts | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 43 deletions(-) create mode 100644 apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx index 56ac6b2960eb0..e1a6d03798a58 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx @@ -1,56 +1,15 @@ import type { IThreadMainMessage } from '@rocket.chat/core-typings'; -import { escapeHTML } from '@rocket.chat/string-helpers'; -import { useSetting, useUser } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; -import { emojiParser } from '../../../../../../app/emoji/client/emojiParser'; -import { filterMarkdown } from '../../../../../../app/markdown/lib/markdown'; -import { MentionsParser } from '../../../../../../app/mentions/lib/MentionsParser'; import { ContextualbarTitle } from '../../../../../components/Contextualbar'; +import { useNormalizedThreadTitleHtml } from '../hooks/useNormalizedThreadTitleHtml'; type ThreadTitleProps = { mainMessage: IThreadMainMessage; }; const ThreadTitle = ({ mainMessage }: ThreadTitleProps) => { - const me = useUser()?.username || ''; - const pattern = useSetting('UTF8_User_Names_Validation', '[0-9a-zA-Z-_.]+'); - const useRealName = useSetting('UI_Use_Real_Name', false); - - const html = useMemo((): string => { - const message = { ...mainMessage }; - - if (message.msg) { - const filteredMessage = filterMarkdown(escapeHTML(message.msg)); - if (!message.channels && !message.mentions) { - return filteredMessage; - } - - const instance = new MentionsParser({ - pattern: () => pattern, - useRealName: () => useRealName, - me: () => me, - userTemplate: ({ label }) => ` ${label} `, - roomTemplate: ({ prefix, mention }) => `${prefix} ${mention} `, - }); - const html = emojiParser(filteredMessage); - return instance.parse({ ...message, msg: filteredMessage, html }).html ?? ''; - } - - if (message.attachments) { - const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); - - if (attachment?.description) { - return escapeHTML(attachment.description); - } - - if (attachment?.title) { - return escapeHTML(attachment.title); - } - } - - return ''; - }, [mainMessage, me, pattern, useRealName]); + const html = useNormalizedThreadTitleHtml(mainMessage); const innerHTML = useMemo(() => ({ __html: html }), [html]); return ; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts new file mode 100644 index 0000000000000..f391e6ad7bc75 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts @@ -0,0 +1,49 @@ +import type { IThreadMainMessage } from '@rocket.chat/core-typings'; +import { escapeHTML } from '@rocket.chat/string-helpers'; +import { useUser, useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { emojiParser } from '../../../../../../app/emoji/client/emojiParser'; +import { filterMarkdown } from '../../../../../../app/markdown/lib/markdown'; +import { MentionsParser } from '../../../../../../app/mentions/lib/MentionsParser'; + +export const useNormalizedThreadTitleHtml = (mainMessage: IThreadMainMessage) => { + const me = useUser()?.username || ''; + const pattern = useSetting('UTF8_User_Names_Validation', '[0-9a-zA-Z-_.]+'); + const useRealName = useSetting('UI_Use_Real_Name', false); + + return useMemo((): string => { + const message = { ...mainMessage }; + + if (message.msg) { + const filteredMessage = filterMarkdown(escapeHTML(message.msg)); + if (!message.channels && !message.mentions) { + return filteredMessage; + } + + const instance = new MentionsParser({ + pattern: () => pattern, + useRealName: () => useRealName, + me: () => me, + userTemplate: ({ label }) => ` ${label} `, + roomTemplate: ({ prefix, mention }) => `${prefix} ${mention} `, + }); + const html = emojiParser(filteredMessage); + return instance.parse({ ...message, msg: filteredMessage, html }).html ?? ''; + } + + if (message.attachments) { + const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); + + if (attachment?.description) { + return escapeHTML(attachment.description); + } + + if (attachment?.title) { + return escapeHTML(attachment.title); + } + } + + return ''; + }, [mainMessage, me, pattern, useRealName]); +}; From 6571ff6110d22d38b3d45b774ae93d9b65d70fef Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 29 Sep 2025 17:02:52 -0300 Subject: [PATCH 8/8] Replace type --- apps/meteor/client/views/room/MemberListRouter.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/MemberListRouter.tsx b/apps/meteor/client/views/room/MemberListRouter.tsx index 77695eb089321..74b45c0c00529 100644 --- a/apps/meteor/client/views/room/MemberListRouter.tsx +++ b/apps/meteor/client/views/room/MemberListRouter.tsx @@ -1,4 +1,4 @@ -import type { IRoom } from '@rocket.chat/core-typings'; +import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { useUserId } from '@rocket.chat/ui-contexts'; import { useRoom } from './contexts/RoomContext'; @@ -6,7 +6,7 @@ import { useRoomToolbox } from './contexts/RoomToolboxContext'; import RoomMembers from './contextualBar/RoomMembers'; import UserInfo from './contextualBar/UserInfo'; -const getUid = (room: IRoom, ownUserId: string | undefined) => { +const getUid = (room: IRoom, ownUserId: IUser['_id'] | undefined) => { if (room.uids?.length === 1) { return room.uids[0]; }