diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index 6f2408414..a15aca920 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -27,7 +27,7 @@ export default class EmbeddedChatApi { constructor( host: string, rid: string, - { getToken, saveToken, deleteToken }: IRocketChatAuthOptions + { getToken, saveToken, deleteToken }: IRocketChatAuthOptions, ) { this.host = host; this.rid = rid; @@ -73,7 +73,7 @@ export default class EmbeddedChatApi { private async _restRequest( endpoint: string, method: "GET" | "POST" | "PUT" | "DELETE" = "GET", - body?: any + body?: any, ) { const options: RequestInit = {}; if (body !== undefined) { @@ -151,7 +151,7 @@ export default class EmbeddedChatApi { if (!response.data.me.username) { await this.updateUserUsername( response.data.userId, - response.data.me.name + response.data.me.name, ); } return { status: response.status, me: response.data.me }; @@ -220,7 +220,7 @@ export default class EmbeddedChatApi { } catch (error) { console.error( "Auto-login failed:", - error instanceof Error ? error.message : error + error instanceof Error ? error.message : error, ); } } @@ -294,7 +294,7 @@ export default class EmbeddedChatApi { if (message) { this.onMessageCallbacks.forEach((callback) => callback(message)); } - } + }, ); this._activeSubscriptions.push(roomMsgSub); @@ -312,7 +312,7 @@ export default class EmbeddedChatApi { // Legacy "typing" event: args[1] is a boolean this.handleTypingEvent({ typingUser, isTyping: !!activities }); } - } + }, ); this._activeSubscriptions.push(notifyRoomSub); @@ -324,10 +324,10 @@ export default class EmbeddedChatApi { const messageId = args[0]?._id; if (messageId) { this.onMessageDeleteCallbacks.forEach((callback) => - callback(messageId) + callback(messageId), ); } - } + }, ); this._activeSubscriptions.push(deleteRoomSub); @@ -346,7 +346,7 @@ export default class EmbeddedChatApi { message.renderType = "blocks"; this.onMessageCallbacks.forEach((callback) => callback(message)); } - } + }, ); this._activeSubscriptions.push(notifyUserMsgSub); @@ -355,7 +355,7 @@ export default class EmbeddedChatApi { [`${userId}/uiInteraction`, false], (data: any) => { this.onUiInteractionCallbacks.forEach((callback) => callback(data)); - } + }, ); this._activeSubscriptions.push(notifyUserUiSub); } @@ -375,7 +375,7 @@ export default class EmbeddedChatApi { async removeMessageListener(callback: (message: any) => void) { this.onMessageCallbacks = this.onMessageCallbacks.filter( - (c) => c !== callback + (c) => c !== callback, ); } @@ -390,7 +390,7 @@ export default class EmbeddedChatApi { async removeMessageDeleteListener(callback: (messageId: string) => void) { this.onMessageDeleteCallbacks = this.onMessageDeleteCallbacks.filter( - (c) => c !== callback + (c) => c !== callback, ); } @@ -405,13 +405,13 @@ export default class EmbeddedChatApi { async removeTypingStatusListener(callback: (users: string[]) => void) { this.onTypingStatusCallbacks = this.onTypingStatusCallbacks.filter( - (c) => c !== callback + (c) => c !== callback, ); } async addActionTriggeredListener(callback: (data: any) => void) { const idx = this.onActionTriggeredCallbacks.findIndex( - (c) => c === callback + (c) => c === callback, ); if (idx !== -1) { this.onActionTriggeredCallbacks[idx] = callback; @@ -422,7 +422,7 @@ export default class EmbeddedChatApi { async removeActionTriggeredListener(callback: (data: any) => void) { this.onActionTriggeredCallbacks = this.onActionTriggeredCallbacks.filter( - (c) => c !== callback + (c) => c !== callback, ); } @@ -437,7 +437,7 @@ export default class EmbeddedChatApi { async removeUiInteractionListener(callback: (data: any) => void) { this.onUiInteractionCallbacks = this.onUiInteractionCallbacks.filter( - (c) => c !== callback + (c) => c !== callback, ); } @@ -466,14 +466,14 @@ export default class EmbeddedChatApi { typingHandlerLock = 0; const newTypingStatus = cloneArray(this.typingUsers); this.onTypingStatusCallbacks.forEach((callback) => - callback(newTypingStatus) + callback(newTypingStatus), ); } async getRCAppInfo() { try { const response = await fetch( - `${this.host}/api/apps/public/${ROCKETCHAT_APP_ID}/info` + `${this.host}/api/apps/public/${ROCKETCHAT_APP_ID}/info`, ); if (!response.ok) { @@ -489,7 +489,7 @@ export default class EmbeddedChatApi { async updateUserNameThroughSuggestion(userid: string) { try { const suggestedUsername = await this._restRequest( - "/v1/users.getUsernameSuggestion" + "/v1/users.getUsernameSuggestion", ); if (suggestedUsername.success) { return await this._restRequest("/v1/users.update", "POST", { @@ -564,7 +564,7 @@ export default class EmbeddedChatApi { latest?: string; count?: number; } = {}, - isChannelPrivate = false + isChannelPrivate = false, ) { const roomType = isChannelPrivate ? "groups" : "channels"; const endp = anonymousMode ? "anonymousread" : "messages"; @@ -573,7 +573,7 @@ export default class EmbeddedChatApi { const count = options?.count != null ? `&count=${options.count}` : ""; try { return await this._restRequest( - `/v1/${roomType}.${endp}?roomId=${this.rid}${oldest}${latest}${count}` + `/v1/${roomType}.${endp}?roomId=${this.rid}${oldest}${latest}${count}`, ); } catch (err: any) { console.error(err instanceof Error ? err.message : err); @@ -589,7 +589,7 @@ export default class EmbeddedChatApi { count?: number; offset?: number; } = {}, - isChannelPrivate = false + isChannelPrivate = false, ) { const roomType = isChannelPrivate ? "groups" : "channels"; const endp = anonymousMode ? "anonymousread" : "messages"; @@ -599,7 +599,7 @@ export default class EmbeddedChatApi { const offset = options?.offset ? `&offset=${options.offset}` : "&offset=0"; try { return await this._restRequest( - `/v1/${roomType}.${endp}?roomId=${this.rid}${oldest}${latest}${count}${offset}` + `/v1/${roomType}.${endp}?roomId=${this.rid}${oldest}${latest}${count}${offset}`, ); } catch (err: any) { console.error(err instanceof Error ? err.message : String(err)); @@ -609,7 +609,7 @@ export default class EmbeddedChatApi { async getThreadMessages( tmid: string, - isChannelPrivate = false + isChannelPrivate = false, ): Promise { try { return await this._restRequest(`/v1/chat.getThreadMessages?tmid=${tmid}`); @@ -623,7 +623,7 @@ export default class EmbeddedChatApi { const roomType = isChannelPrivate ? "groups" : "channels"; try { return await this._restRequest( - `/v1/${roomType}.roles?roomId=${this.rid}` + `/v1/${roomType}.roles?roomId=${this.rid}`, ); } catch (err: any) { console.error(err instanceof Error ? err.message : String(err)); @@ -640,16 +640,35 @@ export default class EmbeddedChatApi { } } - async getUserRoles() { - try { - const response = await this.getUsersInRole("admin"); - if (response && response.success) { - return { result: response.users }; + async getUserRoles(): Promise<{ + admins: string[]; + moderators: string[]; + owners: string[]; + }> { + const extractUsernames = (res: any): string[] => { + if (res && res.success && Array.isArray(res.users)) { + return res.users + .filter((u: any) => u?.username) + .map((u: any) => String(u.username)); } - return { result: [] }; + return []; + }; + + try { + const [adminRes, moderatorRes, ownerRes] = await Promise.all([ + this.getUsersInRole("admin"), + this.getUsersInRole("moderator"), + this.getUsersInRole("owner"), + ]); + + return { + admins: extractUsernames(adminRes), + moderators: extractUsernames(moderatorRes), + owners: extractUsernames(ownerRes), + }; } catch (err) { console.error(err instanceof Error ? err.message : err); - return { result: [] }; + return { admins: [], moderators: [], owners: [] }; } } @@ -659,7 +678,7 @@ export default class EmbeddedChatApi { "stream-notify-room", `${this.rid}/user-activity`, username, - typing ? ["user-typing"] : [] + typing ? ["user-typing"] : [], ); } catch (err) { // DDP typing indicator fails when connection is temporarily down — expected, safe to ignore @@ -761,7 +780,7 @@ export default class EmbeddedChatApi { async getStarredMessages(): Promise { try { return await this._restRequest( - `/v1/chat.getStarredMessages?roomId=${this.rid}` + `/v1/chat.getStarredMessages?roomId=${this.rid}`, ); } catch (err: any) { console.error(err instanceof Error ? err.message : err); @@ -772,7 +791,7 @@ export default class EmbeddedChatApi { async getPinnedMessages(): Promise { try { return await this._restRequest( - `/v1/chat.getPinnedMessages?roomId=${this.rid}` + `/v1/chat.getPinnedMessages?roomId=${this.rid}`, ); } catch (err: any) { console.error(err instanceof Error ? err.message : err); @@ -783,7 +802,7 @@ export default class EmbeddedChatApi { async getMentionedMessages(): Promise { try { return await this._restRequest( - `/v1/chat.getMentionedMessages?roomId=${this.rid}` + `/v1/chat.getMentionedMessages?roomId=${this.rid}`, ); } catch (err: any) { console.error(err instanceof Error ? err.message : err); @@ -815,7 +834,7 @@ export default class EmbeddedChatApi { async reactToMessage( emoji: string, messageId: string, - shouldReact: boolean | string + shouldReact: boolean | string, ): Promise { try { return await this._restRequest("/v1/chat.react", "POST", { @@ -861,7 +880,7 @@ export default class EmbeddedChatApi { file: File, fileName: string, fileDescription = "", - threadId = undefined + threadId = undefined, ) { try { const currentUser = await this.auth.getCurrentUser(); @@ -875,7 +894,7 @@ export default class EmbeddedChatApi { const uploadResult = await this._restUpload( `/v1/rooms.media/${this.rid}`, - form + form, ); if (!uploadResult.success || !uploadResult.file?._id) { @@ -888,7 +907,7 @@ export default class EmbeddedChatApi { "POST", threadId ? { msg: "", description: fileDescription || "", tmid: threadId } - : { msg: "", description: fileDescription || "" } + : { msg: "", description: fileDescription || "" }, ); } catch (err) { console.error("sendAttachment error:", err); @@ -908,7 +927,7 @@ export default class EmbeddedChatApi { const roomType = isChannelPrivate ? "groups" : "channels"; try { return await this._restRequest( - `/v1/${roomType}.members?roomId=${this.rid}` + `/v1/${roomType}.members?roomId=${this.rid}`, ); } catch (err: any) { console.error(err instanceof Error ? err.message : err); @@ -920,8 +939,8 @@ export default class EmbeddedChatApi { try { return await this._restRequest( `/v1/chat.search?roomId=${this.rid}&searchText=${encodeURIComponent( - text - )}` + text, + )}`, ); } catch (err: any) { console.error(err instanceof Error ? err.message : err); @@ -944,7 +963,7 @@ export default class EmbeddedChatApi { const interaction = await this._restRequest( `/apps/ui.interaction/${appId}`, "POST", - { triggerId, ...userInteraction } + { triggerId, ...userInteraction }, ); this.onActionTriggeredCallbacks.forEach((cb) => cb(interaction)); return interaction; @@ -985,7 +1004,7 @@ export default class EmbeddedChatApi { async userData(username: string): Promise { return await this._restRequest( - `/v1/users.info?username=${encodeURIComponent(username)}` + `/v1/users.info?username=${encodeURIComponent(username)}`, ); } } diff --git a/packages/react/src/hooks/useFetchChatData.js b/packages/react/src/hooks/useFetchChatData.js index 57a564a18..f8f7f465e 100644 --- a/packages/react/src/hooks/useFetchChatData.js +++ b/packages/react/src/hooks/useFetchChatData.js @@ -14,34 +14,36 @@ const useFetchChatData = (showRoles) => { const setMessages = useMessageStore((state) => state.setMessages); const setMessagesOffset = useMessageStore((state) => state.setMessagesOffset); const setAdmins = useMemberStore((state) => state.setAdmins); + const setModerators = useMemberStore((state) => state.setModerators); + const setOwners = useMemberStore((state) => state.setOwners); const setMemberRoles = useMemberStore((state) => state.setMemberRoles); const permissionsRef = useRef(null); const setStarredMessages = useStarredMessageStore( - (state) => state.setStarredMessages + (state) => state.setStarredMessages, ); const setPinnedMessages = usePinnedMessageStore( - (state) => state.setPinnedMessages + (state) => state.setPinnedMessages, ); const isUserAuthenticated = useUserStore( - (state) => state.isUserAuthenticated + (state) => state.isUserAuthenticated, ); const setViewUserInfoPermissions = useUserStore( - (state) => state.setViewUserInfoPermissions + (state) => state.setViewUserInfoPermissions, ); const setDeleteMessageRoles = useMessageStore( - (state) => state.setDeleteMessageRoles + (state) => state.setDeleteMessageRoles, ); const setDeleteOwnMessageRoles = useMessageStore( - (state) => state.setDeleteOwnMessageRoles + (state) => state.setDeleteOwnMessageRoles, ); const setForceDeleteMessageRoles = useMessageStore( - (state) => state.setForceDeleteMessageRoles + (state) => state.setForceDeleteMessageRoles, ); const setUserPinPermissions = useUserStore( - (state) => state.setUserPinPermissions + (state) => state.setUserPinPermissions, ); const setEditMessagePermissions = useMessageStore( - (state) => state.setEditMessagePermissions + (state) => state.setEditMessagePermissions, ); const setters = useMemo( @@ -61,7 +63,7 @@ const useFetchChatData = (showRoles) => { setForceDeleteMessageRoles, setUserPinPermissions, setEditMessagePermissions, - ] + ], ); const permissionKeys = useMemo( @@ -74,7 +76,7 @@ const useFetchChatData = (showRoles) => { ['userPin', 'pin-message'], ['editMessage', 'edit-message'], ]), - [] + [], ); const applyPermissions = useCallback( @@ -86,13 +88,13 @@ const useFetchChatData = (showRoles) => { } }); }, - [permissionKeys, setters] + [permissionKeys, setters], ); const createPermissionsMap = useCallback( (permissions) => new Map(permissions.update.map((item) => [item._id, item])), - [] + [], ); const fetchAndSetPermissions = useCallback(async () => { try { @@ -145,7 +147,7 @@ const useFetchChatData = (showRoles) => { const { messages, count } = await RCInstance.getMessages( anonymousMode, undefined, - channelIsPrivate + channelIsPrivate, ); if (messages) { @@ -159,17 +161,18 @@ const useFetchChatData = (showRoles) => { if (showRoles) { const { roles } = await RCInstance.getChannelRoles(channelIsPrivate); - const fetchedRoles = await RCInstance.getUserRoles(); - const fetchedAdmins = fetchedRoles?.result; + const { admins, moderators, owners } = + await RCInstance.getUserRoles(); - const adminUsernames = fetchedAdmins?.map((user) => user.username); - setAdmins(adminUsernames); + setAdmins(admins); + setModerators(moderators); + setOwners(owners); const rolesObj = roles?.length > 0 ? roles.reduce( (obj, item) => ({ ...obj, [item.u.username]: item }), - {} + {}, ) : {}; @@ -185,8 +188,10 @@ const useFetchChatData = (showRoles) => { showRoles, setMessages, setAdmins, + setModerators, + setOwners, setMemberRoles, - ] + ], ); const getStarredMessages = useCallback( @@ -203,7 +208,7 @@ const useFetchChatData = (showRoles) => { } } }, - [isUserAuthenticated, RCInstance, setStarredMessages] + [isUserAuthenticated, RCInstance, setStarredMessages], ); const getPinnedMessages = useCallback( @@ -220,7 +225,7 @@ const useFetchChatData = (showRoles) => { } } }, - [isUserAuthenticated, RCInstance, setPinnedMessages] + [isUserAuthenticated, RCInstance, setPinnedMessages], ); return { diff --git a/packages/react/src/store/memberStore.js b/packages/react/src/store/memberStore.js index 574601726..4a77935e5 100644 --- a/packages/react/src/store/memberStore.js +++ b/packages/react/src/store/memberStore.js @@ -6,8 +6,12 @@ const useMemberStore = create((set) => ({ setShowMembers: (showMembers) => set(() => ({ showMembers })), memberRoles: {}, admins: [], + moderators: [], + owners: [], setMemberRoles: (memberRoles) => set((state) => ({ ...state, memberRoles })), setAdmins: (admins) => set(() => ({ admins })), + setModerators: (moderators) => set(() => ({ moderators })), + setOwners: (owners) => set(() => ({ owners })), setMembersHandler: (memberList) => set(() => ({ members: memberList })), })); diff --git a/packages/react/src/views/Message/MessageHeader.js b/packages/react/src/views/Message/MessageHeader.js index 1e135c530..05ea60058 100644 --- a/packages/react/src/views/Message/MessageHeader.js +++ b/packages/react/src/views/Message/MessageHeader.js @@ -37,6 +37,8 @@ const MessageHeader = ({ const showName = ECOptions?.showName; const channelLevelRoles = useMemberStore((state) => state.memberRoles); const admins = useMemberStore((state) => state.admins); + const moderators = useMemberStore((state) => state.moderators); + const owners = useMemberStore((state) => state.owners); const isPinned = message.pinned; const isStarred = @@ -170,6 +172,26 @@ const MessageHeader = ({ )} + {moderators?.includes(message?.u?.username) && ( + + Moderator + + )} + + {owners?.includes(message?.u?.username) && ( + + Owner + + )} + {channelLevelRoles[message.u.username]?.roles?.map((role, index) => (