diff --git a/apps/meteor/app/authorization/server/constant/permissions.ts b/apps/meteor/app/authorization/server/constant/permissions.ts index c54f84c03fac2..675b55d025713 100644 --- a/apps/meteor/app/authorization/server/constant/permissions.ts +++ b/apps/meteor/app/authorization/server/constant/permissions.ts @@ -67,6 +67,7 @@ export const permissions = [ { _id: 'set-owner', roles: ['admin', 'owner'] }, { _id: 'send-many-messages', roles: ['admin', 'bot', 'app'] }, { _id: 'set-leader', roles: ['admin', 'owner'] }, + { _id: 'set-important-message-marker', roles: ['admin', 'owner'] }, { _id: 'start-discussion', roles: ['admin', 'user', 'federated-external', 'guest', 'app'] }, { _id: 'start-discussion-other-user', roles: ['admin', 'user', 'federated-external', 'owner', 'app'] }, { _id: 'unarchive-room', roles: ['admin'] }, @@ -239,4 +240,5 @@ export const permissions = [ { _id: 'manage-moderation-actions', roles: ['admin'] }, { _id: 'bypass-time-limit-edit-and-delete', roles: ['bot', 'app'] }, { _id: 'export-messages-as-pdf', roles: ['admin', 'user'] }, + { _id: 'mark-message-as-important', roles: ['admin', 'owner', 'important-message-marker'] }, ]; diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index a1208543d6841..5fa4200af496c 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -149,6 +149,7 @@ Meteor.methods({ federation: Match.Maybe(Object), groupable: Match.Maybe(Boolean), sentByEmail: Match.Maybe(Boolean), + isImportant: Match.Maybe(Boolean), }); const user = (await Meteor.userAsync()) as IUser; @@ -158,13 +159,28 @@ Meteor.methods({ }); } + if (message.isImportant) { + console.log('[sendMessage] Received important message:', { + messageId: message._id, + userId: user._id, + roomId: message.rid + }); + } + if (MessageTypes.isSystemMessage(message)) { throw new Error("Cannot send system messages using 'sendMessage'"); } try { - return await applyAirGappedRestrictionsValidation(() => executeSendMessage(user, message, { previewUrls })); + const result = await applyAirGappedRestrictionsValidation(() => executeSendMessage(user, message, { previewUrls })); + if (message.isImportant) { + console.log('[sendMessage] Important message saved successfully:', { messageId: result._id }); + } + return result; } catch (error: any) { + if (message.isImportant) { + console.error('[sendMessage] Error saving important message:', error); + } if (['error-not-allowed', 'restricted-workspace'].includes(error.error || error.message)) { throw new Meteor.Error(error.error || error.message, error.reason, { method: 'sendMessage', diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index 7486bd9aacb07..f5e2749eee866 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -84,6 +84,13 @@ &.highlight { animation: highlight 6s; } + + &.rcx-message--important { + background-color: rgba(245, 69, 92, 0.03) !important; + border-left: 3px solid rgba(245, 69, 92, 0.4); + padding-left: 8px; + margin: 2px 0; + } } .page-loading { diff --git a/apps/meteor/client/components/message/variants/ImportantMessageReadButton.tsx b/apps/meteor/client/components/message/variants/ImportantMessageReadButton.tsx new file mode 100644 index 0000000000000..70c7a9632c904 --- /dev/null +++ b/apps/meteor/client/components/message/variants/ImportantMessageReadButton.tsx @@ -0,0 +1,71 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { Button, Box } from '@rocket.chat/fuselage'; +import { useUserId, useMethod, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; +import type { ReactElement } from 'react'; +import { memo, useState, useEffect } from 'react'; + +import ImportantMessageReadInfo from './ImportantMessageReadInfo'; + +type ImportantMessageReadButtonProps = { + message: IMessage; +}; + +const ImportantMessageReadButton = ({ message }: ImportantMessageReadButtonProps): ReactElement | null => { + const userId = useUserId(); + const toggleImportantMessageRead = useMethod('toggleImportantMessageRead'); + const dispatchToastMessage = useToastMessageDispatch(); + const queryClient = useQueryClient(); + + const serverIsRead = message.importantReadBy?.includes(userId || '') ?? false; + const [isRead, setIsRead] = useState(serverIsRead); + + useEffect(() => { + setIsRead(serverIsRead); + }, [serverIsRead]); + + if (!message.isImportant || !userId) { + return null; + } + + const handleToggle = async () => { + const newState = !isRead; + setIsRead(newState); + + console.log('[ImportantMessageReadButton] Toggling read status:', { messageId: message._id, newState }); + + try { + await toggleImportantMessageRead(message._id); + queryClient.invalidateQueries({ + queryKey: ['important-message-readers', message._id] + }); + console.log('[ImportantMessageReadButton] Read status toggled successfully'); + } catch (error) { + setIsRead(!newState); + console.error('[ImportantMessageReadButton] Error toggling read status:', error); + dispatchToastMessage({ + type: 'error', + message: error instanceof Error ? error.message : String(error), + }); + } + }; + + return ( + + + + + ); +}; + +export default memo(ImportantMessageReadButton); diff --git a/apps/meteor/client/components/message/variants/ImportantMessageReadInfo.tsx b/apps/meteor/client/components/message/variants/ImportantMessageReadInfo.tsx new file mode 100644 index 0000000000000..b19b5e19bbbd5 --- /dev/null +++ b/apps/meteor/client/components/message/variants/ImportantMessageReadInfo.tsx @@ -0,0 +1,174 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { Box, Icon, TextInput } from '@rocket.chat/fuselage'; +import { useMethod, useUserId, usePermission, useUserSubscription, useStream } from '@rocket.chat/ui-contexts'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ReactElement, ChangeEvent } from 'react'; +import { memo, useState, useMemo, useEffect } from 'react'; + +type ImportantMessageReadInfoProps = { + message: IMessage; +}; + +type User = { + _id: string; + username: string; + name?: string; +}; + +const ImportantMessageReadInfo = ({ message }: ImportantMessageReadInfoProps): ReactElement | null => { + const [showList, setShowList] = useState(false); + const [searchText, setSearchText] = useState(''); + const getUsersWhoRead = useMethod('getUsersWhoReadImportantMessage'); + const getUserRoomRole = useMethod('getUserRoomRole'); + const userId = useUserId(); + const subscription = useUserSubscription(message.rid); + const queryClient = useQueryClient(); + const subscribeToRoomMessages = useStream('room-messages'); + + useEffect(() => { + const unsubscribe = subscribeToRoomMessages(message.rid, (msg) => { + if (msg._id === message._id && msg.importantReadBy) { + queryClient.invalidateQueries({ + queryKey: ['important-message-readers', message._id] + }); + } + }); + + return unsubscribe; + }, [subscribeToRoomMessages, message.rid, message._id, queryClient]); + + const { data: hasRoleFromQuery = false } = useQuery({ + queryKey: ['user-room-role-info', userId, message.rid, 'important-message-marker'], + queryFn: async () => { + if (!userId) return false; + try { + const result = await getUserRoomRole(message.rid, userId, 'important-message-marker'); + return result ?? false; + } catch (error) { + return false; + } + }, + staleTime: 0, + enabled: !!userId, + }); + + const hasPermission = usePermission('mark-message-as-important', message.rid); + const hasRole = subscription?.roles?.includes('important-message-marker') ?? hasRoleFromQuery; + const canMarkMessagesAsImportant = hasPermission || hasRole; + + const { data: users = [], isLoading } = useQuery({ + queryKey: ['important-message-readers', message._id], + queryFn: async () => { + try { + const result = await getUsersWhoRead(message._id); + return result || []; + } catch (error) { + console.error('Error fetching users who read:', error); + return []; + } + }, + enabled: showList, + refetchInterval: showList ? 5000 : false, + }); + + const filteredUsers = useMemo(() => { + if (!searchText.trim()) { + return users; + } + const search = searchText.toLowerCase(); + return users.filter( + (user) => + user.username.toLowerCase().includes(search) || + (user.name && user.name.toLowerCase().includes(search)) + ); + }, [users, searchText]); + + const readCount = message.importantReadBy?.length || 0; + + if (!message.isImportant || !canMarkMessagesAsImportant) { + return null; + } + + const handleClick = () => { + console.log('[ImportantMessageReadInfo] Toggling list visibility:', { messageId: message._id, currentState: showList }); + setShowList(!showList); + if (!showList) { + setSearchText(''); + } + }; + + const handleSearchChange = (e: ChangeEvent) => { + const value = e.target.value; + console.log('[ImportantMessageReadInfo] Search text changed:', { messageId: message._id, searchText: value }); + setSearchText(value); + }; + + return ( + + + + + + {showList && ( + + + Read by ({readCount}): + + + {users.length > 3 && ( + + + + )} + + {isLoading ? ( + Loading... + ) : filteredUsers.length > 0 ? ( + + {filteredUsers.map((user) => ( + + @{user.username} {user.name && `(${user.name})`} + + ))} + + ) : searchText ? ( + No users found matching "{searchText}" + ) : ( + No one has read this message yet + )} + + )} + + ); +}; + +export default memo(ImportantMessageReadInfo); diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index 14895b05d0929..a9845c0109740 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx @@ -20,6 +20,7 @@ import IgnoredContent from '../IgnoredContent'; import MessageHeader from '../MessageHeader'; import MessageToolbarHolder from '../MessageToolbarHolder'; import StatusIndicators from '../StatusIndicators'; +import ImportantMessageReadButton from './ImportantMessageReadButton'; import RoomMessageContent from './room/RoomMessageContent'; import { useMessageListReadReceipts } from '../list/MessageListContext'; @@ -106,7 +107,10 @@ const RoomMessage = ({ data-unread={unread} data-sequential={sequential} data-own={message.u._id === uid} + data-qa-type='message' + data-important={message.isImportant} aria-busy={message.temp} + className={message.isImportant ? 'rcx-message--important' : undefined} {...props} > @@ -130,7 +134,10 @@ const RoomMessage = ({ {ignored ? ( ) : ( - + <> + + {message.isImportant && } + )} {!message.private && message?.e2e !== 'pending' && !selecting && } diff --git a/apps/meteor/client/hooks/useRoomRolesQuery.ts b/apps/meteor/client/hooks/useRoomRolesQuery.ts index cdc012b1b7832..48903199dcc22 100644 --- a/apps/meteor/client/hooks/useRoomRolesQuery.ts +++ b/apps/meteor/client/hooks/useRoomRolesQuery.ts @@ -52,7 +52,7 @@ export const useRoomRolesQuery = (rid: IRoom['_id'], option case 'removed': { const { _id: roleId, scope, u } = role; - if (!!scope || !u) return; + if (!u) return; queryClient.setQueryData(roomsQueryKeys.roles(rid), (data: RoomRoles[] | undefined = []) => { const index = data?.findIndex((record) => record.rid === rid && record.u._id === u._id) ?? -1; @@ -65,6 +65,8 @@ export const useRoomRolesQuery = (rid: IRoom['_id'], option return [...data]; }); + + queryClient.invalidateQueries({ queryKey: roomsQueryKeys.roles(rid) }); break; } } diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index db2e61ad640af..13cea9e22ef73 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -170,6 +170,7 @@ export type ChatAPI = { tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean; + isImportant?: boolean; tmid?: IMessage['tmid']; }) => Promise; readonly processSlashCommand: (message: IMessage, userId: string | null) => Promise; diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts index e2ad4962a851d..cb2e108303f28 100644 --- a/apps/meteor/client/lib/chats/flows/sendMessage.ts +++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts @@ -60,7 +60,8 @@ export const sendMessage = async ( tshow, previewUrls, isSlashCommandAllowed, - }: { text: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean; tmid?: IMessage['tmid'] }, + isImportant, + }: { text: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean; isImportant?: boolean; tmid?: IMessage['tmid'] }, ): Promise => { if (!(await chat.data.isSubscribedToRoom())) { try { @@ -92,8 +93,11 @@ export const sendMessage = async ( originalMessage: mid ? await chat.data.findMessageByID(mid) : null, }); - // When editing an encrypted message with files, preserve the original attachments/files - // This ensures they're included in the re-encryption process + if (isImportant) { + message.isImportant = true; + console.log('[sendMessage] Marking message as important'); + } + if (mid) { const originalMessage = await chat.data.findMessageByID(mid); diff --git a/apps/meteor/client/views/room/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index 2daa10309ef45..e8c62d4af4b5b 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -44,11 +44,13 @@ const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): Reac tshow, previewUrls, isSlashCommandAllowed, + isImportant, }: { value: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean; + isImportant?: boolean; }): Promise => { try { await chat?.action.stop('typing'); @@ -57,6 +59,7 @@ const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): Reac tshow, previewUrls, isSlashCommandAllowed, + isImportant, tmid, }); if (newMessageSent) onSend?.(); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index c653b0a132e55..795a9d36c33c0 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -14,7 +14,7 @@ import { import { useTranslation, useUserPreference, useLayout, useSetting } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; import type { ReactElement, FormEvent, MouseEvent, ClipboardEvent } from 'react'; -import { memo, useRef, useReducer, useCallback, useSyncExternalStore } from 'react'; +import { memo, useRef, useReducer, useCallback, useSyncExternalStore, useState } from 'react'; import MessageBoxActionsToolbar from './MessageBoxActionsToolbar'; import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; @@ -50,7 +50,6 @@ import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; const reducer = (_: unknown, event: FormEvent): boolean => { const target = event.target as HTMLInputElement; - return Boolean(target.value.trim()); }; @@ -63,7 +62,6 @@ const handleFormattingShortcut = (event: KeyboardEvent, formattingButtons: Forma } const key = event.key.toLowerCase(); - const formatter = formattingButtons.find((formatter) => 'command' in formatter && formatter.command === key); if (!formatter || !('pattern' in formatter)) { @@ -81,7 +79,7 @@ const getEmptyArray = () => a; type MessageBoxProps = { tmid?: IMessage['_id']; - onSend?: (params: { value: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean }) => Promise; + onSend?: (params: { value: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean; isImportant?: boolean }) => Promise; onJoin?: () => Promise; onResize?: () => void; onTyping?: () => void; @@ -115,6 +113,7 @@ const MessageBox = ({ const composerPlaceholder = useMessageBoxPlaceholder(t('Message'), room); const quoteChainLimit = useSetting('Message_QuoteChainLimit', 2); const [typing, setTyping] = useReducer(reducer, false); + const [isImportantActive, setIsImportantActive] = useState(false); const { isMobile } = useLayout(); const sendOnEnterBehavior = useUserPreference<'normal' | 'alternative' | 'desktop'>('sendOnEnter') || isMobile; @@ -124,7 +123,7 @@ const MessageBox = ({ throw new Error('Chat context not found'); } - const textareaRef = useRef(null); + const textareaRef = useRef(null); const messageComposerRef = useRef(null); const storageID = `messagebox_${room._id}${tmid ? `-${tmid}` : ''}`; @@ -175,7 +174,16 @@ const MessageBox = ({ tshow, previewUrls, isSlashCommandAllowed, + isImportant: isImportantActive, }); + + setIsImportantActive(false); + }); + + const handleImportantToggle = useEffectEvent((active: boolean) => { + console.log('[MessageBox] Important toggle clicked:', { active }); + setIsImportantActive(active); + textareaRef.current?.focus(); }); const closeEditing = (event: KeyboardEvent | MouseEvent) => { @@ -346,7 +354,6 @@ const MessageBox = ({ } const imageExtension = fileItem ? getImageExtensionFromMime(fileItem.type) : undefined; - const extension = imageExtension ? `.${imageExtension}` : ''; Object.defineProperty(fileItem, 'name', { @@ -484,6 +491,8 @@ const MessageBox = ({ tmid={tmid} isRecording={isRecording} variant={sizes.inlineSize < 480 ? 'small' : 'large'} + isImportantActive={isImportantActive} + onImportantToggle={handleImportantToggle} isEditing={isEditing} /> diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index 5ea38edf5f43d..1d673c4e2cb20 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -4,7 +4,8 @@ import { isTruthy } from '@rocket.chat/tools'; import { GenericMenu, type GenericMenuItemProps } from '@rocket.chat/ui-client'; import { MessageComposerAction, MessageComposerActionsDivider } from '@rocket.chat/ui-composer'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useTranslation, useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; +import { useTranslation, useLayoutHiddenActions, useUserId, usePermission, useUserSubscription, useMethod } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import type { ComponentProps, MouseEvent } from 'react'; import { memo } from 'react'; @@ -28,6 +29,8 @@ type MessageBoxActionsToolbarProps = { isRecording: boolean; rid: IRoom['_id']; tmid?: IMessage['_id']; + isImportantActive?: boolean; + onImportantToggle?: (active: boolean) => void; isEditing: boolean; }; @@ -46,6 +49,8 @@ const MessageBoxActionsToolbar = ({ tmid, variant = 'large', isMicrophoneDenied, + isImportantActive = false, + onImportantToggle, isEditing = false, }: MessageBoxActionsToolbarProps) => { const t = useTranslation(); @@ -56,6 +61,27 @@ const MessageBoxActionsToolbar = ({ } const room = useRoom(); + const userId = useUserId(); + const subscription = useUserSubscription(rid); + const getUserRoomRole = useMethod('getUserRoomRole'); + + const { data: hasRoleFromQuery = false } = useQuery({ + queryKey: ['user-room-role-toolbar', userId, rid, 'important-message-marker'], + queryFn: async () => { + if (!userId) return false; + try { + return await getUserRoomRole(rid, userId, 'important-message-marker'); + } catch (error) { + return false; + } + }, + staleTime: 0, + enabled: !!userId, + }); + + const hasPermission = usePermission('mark-message-as-important', rid); + const hasRole = subscription?.roles?.includes('important-message-marker') ?? hasRoleFromQuery; + const canMarkMessagesAsImportant = hasPermission || hasRole; const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); @@ -93,7 +119,11 @@ const MessageBoxActionsToolbar = ({ featured.push(allActions.audioMessageAction, allActions.fileUploadAction); createNew.push(allActions.videoMessageAction); } else { - featured.push(allActions.audioMessageAction, allActions.videoMessageAction, allActions.fileUploadAction); + featured.push( + allActions.audioMessageAction, + allActions.videoMessageAction, + allActions.fileUploadAction + ); } if (allActions.webdavActions) { @@ -115,7 +145,7 @@ const MessageBoxActionsToolbar = ({ .map((item) => ({ id: item.id, icon: item.icon as ComponentProps['name'], - content: t(item.label), + content: t(item.label as TranslationKey), onClick: (event?: MouseEvent) => item.action({ rid, @@ -148,6 +178,16 @@ const MessageBoxActionsToolbar = ({ <> {featured.map((action) => action && renderAction(action))} + {canMarkMessagesAsImportant && ( + onImportantToggle?.(!isImportantActive)} + pressed={isImportantActive} + /> + )} , + rid: IRoom['_id'], +): UserInfoAction | undefined => { + const room = useUserRoom(rid); + const { _id: uid, username } = user; + const currentUserId = useUserId(); + const currentUser = useUser(); + + const currentUserRoles = currentUser?.roles || []; + + const userSubscription = useUserSubscription(rid); + + const userCanSetImportantMessageMarker = usePermission('set-important-message-marker', rid); + const dispatchToastMessage = useToastMessageDispatch(); + const getUserRoomRole = useMethod('getUserRoomRole'); + const queryClient = useQueryClient(); + const subscribeToNotifyLogged = useStream('notify-logged'); + + if (!room) { + throw Error('Room not provided'); + } + + const { roomCanSetImportantMessageMarker } = getRoomDirectives({ room, showingUserId: uid, userSubscription }); + + const isCurrentUserPrivileged = currentUserRoles.some((role: string) => + ['admin', 'owner'].includes(role) + ); + + useEffect(() => { + const unsubscribe = subscribeToNotifyLogged('roles-change', (role) => { + if (role.u?._id === uid && (role.scope === rid || !role.scope)) { + queryClient.invalidateQueries({ + queryKey: ['user-room-role', uid, rid] + }); + } + }); + + return unsubscribe; + }, [subscribeToNotifyLogged, uid, rid, queryClient]); + + const { data: isTargetOwner = false } = useQuery({ + queryKey: ['user-room-role', uid, rid, 'owner'], + queryFn: async () => { + try { + return await getUserRoomRole(rid, uid, 'owner'); + } catch (error) { + return false; + } + }, + staleTime: 0, + }); + + const isTargetPrivileged = isTargetOwner; + + const { data: hasRole = false, refetch } = useQuery({ + queryKey: ['user-room-role', uid, rid, 'important-message-marker'], + queryFn: async () => { + try { + return await getUserRoomRole(rid, uid, 'important-message-marker'); + } catch (error) { + return false; + } + }, + staleTime: 0, + }); + + const changeRoleAction = useEffectEvent(async () => { + try { + const newRoleState = !hasRole; + + console.log('[useChangeImportantMessageMarkerAction] Changing role:', { + rid, + userId: uid, + username, + newRoleState + }); + + await Meteor.callAsync( + newRoleState + ? 'addRoomImportantMessageMarker' + : 'removeRoomImportantMessageMarker', + rid, + uid + ); + + await refetch(); + await queryClient.invalidateQueries({ + queryKey: ['user-room-role', uid, rid] + }); + + console.log('[useChangeImportantMessageMarkerAction] Role changed successfully'); + + dispatchToastMessage({ + type: 'success', + message: newRoleState + ? `Granted ability to mark important messages to @${username} in this room` + : `Removed ability to mark important messages from @${username} in this room`, + }); + } catch (error) { + console.error('[useChangeImportantMessageMarkerAction] Error changing role:', error); + + const message = + error && typeof error === 'object' && 'message' in error + ? (error as any).message + : String(error); + + dispatchToastMessage({ + type: 'error', + message: `Failed to change role: ${message}`, + }); + } + }); + + const changeRoleOption = useMemo(() => { + if (uid === currentUserId) { + return undefined; + } + + if (!roomCanSetImportantMessageMarker) { + return undefined; + } + + if (!userCanSetImportantMessageMarker) { + return undefined; + } + + if (!isCurrentUserPrivileged) { + return undefined; + } + + if (isTargetPrivileged) { + return undefined; + } + + return { + content: hasRole + ? 'Remove ability to mark important messages' + : 'Grant ability to mark important messages', + icon: 'flag' as const, + onClick: changeRoleAction, + type: 'privileges' as UserInfoActionType, + }; + }, [ + hasRole, + roomCanSetImportantMessageMarker, + userCanSetImportantMessageMarker, + isCurrentUserPrivileged, + isTargetPrivileged, + changeRoleAction, + uid, + currentUserId + ]); + + return changeRoleOption; +}; diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts index afd1b9f57596b..a94ec9698c79c 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts @@ -12,6 +12,7 @@ import { useBlockUserAction } from './actions/useBlockUserAction'; import { useChangeLeaderAction } from './actions/useChangeLeaderAction'; import { useChangeModeratorAction } from './actions/useChangeModeratorAction'; import { useChangeOwnerAction } from './actions/useChangeOwnerAction'; +import { useChangeImportantMessageMarkerAction } from './actions/useChangeImportantMessageMarkerAction'; import { useDirectMessageAction } from './actions/useDirectMessageAction'; import { useIgnoreUserAction } from './actions/useIgnoreUserAction'; import { useMuteUserAction } from './actions/useMuteUserAction'; @@ -52,7 +53,7 @@ export type UserMenuAction = { }[]; type UserInfoActionsParams = { - user: Pick; + user: Pick; rid: IRoom['_id']; reload?: () => void; size?: number; @@ -77,6 +78,7 @@ export const useUserInfoActions = ({ const blockUser = useBlockUserAction(user, rid); const changeLeader = useChangeLeaderAction(user, rid); const changeModerator = useChangeModeratorAction(user, rid); + const changeImportantMessageMarker = useChangeImportantMessageMarkerAction(user, rid); const openModerationConsole = useRedirectModerationConsole(user._id); const changeOwner = useChangeOwnerAction(user, rid); const openDirectMessage = useDirectMessageAction(user, rid); @@ -99,6 +101,7 @@ export const useUserInfoActions = ({ ...(isMember && changeOwner && { changeOwner }), ...(isMember && changeLeader && { changeLeader }), ...(isMember && changeModerator && { changeModerator }), + ...(isMember && changeImportantMessageMarker && { changeImportantMessageMarker }), ...(isMember && openModerationConsole && { openModerationConsole }), ...(isMember && ignoreUser && { ignoreUser }), ...(isMember && muteUser && { muteUser }), @@ -115,6 +118,7 @@ export const useUserInfoActions = ({ changeOwner, changeLeader, changeModerator, + changeImportantMessageMarker, ignoreUser, muteUser, blockUser, @@ -160,3 +164,4 @@ export const useUserInfoActions = ({ return actionSpread; }; + diff --git a/apps/meteor/client/views/room/lib/getRoomDirectives.ts b/apps/meteor/client/views/room/lib/getRoomDirectives.ts index ead6eb5856889..21dd0e3072a03 100644 --- a/apps/meteor/client/views/room/lib/getRoomDirectives.ts +++ b/apps/meteor/client/views/room/lib/getRoomDirectives.ts @@ -7,6 +7,7 @@ type getRoomDirectiesType = { roomCanSetOwner: boolean; roomCanSetLeader: boolean; roomCanSetModerator: boolean; + roomCanSetImportantMessageMarker: boolean; roomCanIgnore: boolean; roomCanBlock: boolean; roomCanMute: boolean; @@ -30,6 +31,7 @@ export const getRoomDirectives = ({ roomCanSetOwner, roomCanSetLeader, roomCanSetModerator, + roomCanSetImportantMessageMarker, roomCanIgnore, roomCanBlock, roomCanMute, @@ -41,6 +43,7 @@ export const getRoomDirectives = ({ roomDirectives.allowMemberAction(room, RoomMemberActions.SET_AS_OWNER, showingUserId, userSubscription), roomDirectives.allowMemberAction(room, RoomMemberActions.SET_AS_LEADER, showingUserId, userSubscription), roomDirectives.allowMemberAction(room, RoomMemberActions.SET_AS_MODERATOR, showingUserId, userSubscription), + roomDirectives.allowMemberAction(room, RoomMemberActions.SET_AS_IMPORTANT_MESSAGE_MARKER, showingUserId, userSubscription), roomDirectives.allowMemberAction(room, RoomMemberActions.IGNORE, showingUserId, userSubscription), roomDirectives.allowMemberAction(room, RoomMemberActions.BLOCK, showingUserId, userSubscription), roomDirectives.allowMemberAction(room, RoomMemberActions.MUTE, showingUserId, userSubscription), @@ -55,6 +58,7 @@ export const getRoomDirectives = ({ roomCanSetOwner, roomCanSetLeader, roomCanSetModerator, + roomCanSetImportantMessageMarker, roomCanIgnore, roomCanBlock, roomCanMute, diff --git a/apps/meteor/definition/IRoomTypeConfig.ts b/apps/meteor/definition/IRoomTypeConfig.ts index 7fafc301275e7..1c7f3d4226ea5 100644 --- a/apps/meteor/definition/IRoomTypeConfig.ts +++ b/apps/meteor/definition/IRoomTypeConfig.ts @@ -44,6 +44,7 @@ export const RoomMemberActions = { SET_AS_OWNER: 'setAsOwner', SET_AS_LEADER: 'setAsLeader', SET_AS_MODERATOR: 'setAsModerator', + SET_AS_IMPORTANT_MESSAGE_MARKER: 'setAsImportantMessageMarker', LEAVE: 'leave', REMOVE_USER: 'removeUser', JOIN: 'join', diff --git a/apps/meteor/server/methods/addRoomImportantMessageMarker.ts b/apps/meteor/server/methods/addRoomImportantMessageMarker.ts new file mode 100644 index 0000000000000..ef72263d1cf23 --- /dev/null +++ b/apps/meteor/server/methods/addRoomImportantMessageMarker.ts @@ -0,0 +1,126 @@ +import { api, Message, Team } from '@rocket.chat/core-services'; +import { Subscriptions, Rooms, Users } from '@rocket.chat/models'; +import { check } from 'meteor/check'; +import { Meteor } from 'meteor/meteor'; + +import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; +import { settings } from '../../app/settings/server'; +import { beforeChangeRoomRole } from '../../lib/callbacks/beforeChangeRoomRole'; +import { syncRoomRolePriorityForUserAndRoom } from '../lib/roles/syncRoomRolePriority'; + +declare module '@rocket.chat/ddp-client' { + interface ServerMethods { + addRoomImportantMessageMarker(rid: string, userId: string): boolean; + removeRoomImportantMessageMarker(rid: string, userId: string): boolean; + } +} + +export const addRoomImportantMessageMarker = async ( + fromUserId: string, + rid: string, + userId: string, +): Promise => { + check(rid, String); + check(userId, String); + + console.log('[addRoomImportantMessageMarker] Starting:', { fromUserId, rid, userId }); + + const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); + if (!room) { + console.error('[addRoomImportantMessageMarker] Room not found:', rid); + throw new Meteor.Error('error-invalid-room', 'Invalid room', { + method: 'addRoomImportantMessageMarker', + }); + } + + if (!(await hasPermissionAsync(fromUserId, 'set-important-message-marker', rid))) { + console.error('[addRoomImportantMessageMarker] Permission denied:', { fromUserId, rid }); + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'addRoomImportantMessageMarker', + }); + } + + const user = await Users.findOneById(userId); + if (!user?.username) { + console.error('[addRoomImportantMessageMarker] User not found:', userId); + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'addRoomImportantMessageMarker', + }); + } + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, user._id); + if (!subscription) { + console.error('[addRoomImportantMessageMarker] Subscription not found:', { rid, userId }); + throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', { + method: 'addRoomImportantMessageMarker', + }); + } + + if (subscription.roles?.includes('important-message-marker')) { + console.log('[addRoomImportantMessageMarker] User already has role:', { rid, userId }); + return true; + } + + await beforeChangeRoomRole.run({ fromUserId, userId, room, role: 'user' }); + + await Subscriptions.addRoleById(subscription._id, 'important-message-marker'); + + await syncRoomRolePriorityForUserAndRoom( + userId, + rid, + subscription.roles?.concat(['important-message-marker']) || ['important-message-marker'], + ); + + if (subscription._id) { + void notifyOnSubscriptionChangedById(subscription._id); + } + + const fromUser = await Users.findOneById(fromUserId); + + await Message.saveSystemMessage( + 'subscription-role-added', + rid, + user.username, + fromUser!, + { role: 'important-message-marker' }, + ); + + const team = await Team.getOneByMainRoomId(rid); + if (team) { + await Team.addRolesToMember(team._id, userId, ['important-message-marker']); + } + + const event = { + type: 'added', + _id: 'important-message-marker', + u: { + _id: user._id, + username: user.username, + name: user.name, + }, + scope: rid, + } as const; + + if (settings.get('UI_DisplayRoles')) { + void api.broadcast('user.roleUpdate', event); + } + void api.broadcast('federation.userRoleChanged', { ...event, givenByUserId: fromUserId }); + + console.log('[addRoomImportantMessageMarker] Role added successfully:', { rid, userId }); + return true; +}; + +// Register Meteor method +Meteor.methods({ + async addRoomImportantMessageMarker(rid: string, userId: string) { + const uid = Meteor.userId(); + if (!uid) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'addRoomImportantMessageMarker', + }); + } + + return await addRoomImportantMessageMarker(uid, rid, userId); + }, +}); diff --git a/apps/meteor/server/methods/getUserRoomRole.ts b/apps/meteor/server/methods/getUserRoomRole.ts new file mode 100644 index 0000000000000..e125db4857e98 --- /dev/null +++ b/apps/meteor/server/methods/getUserRoomRole.ts @@ -0,0 +1,35 @@ +import { Subscriptions } from '@rocket.chat/models'; +import { check } from 'meteor/check'; +import { Meteor } from 'meteor/meteor'; + +declare module '@rocket.chat/ddp-client' { + interface ServerMethods { + getUserRoomRole(rid: string, userId: string, role: string): boolean; + } +} + +Meteor.methods({ + async getUserRoomRole(rid: string, userId: string, role: string): Promise { + check(rid, String); + check(userId, String); + check(role, String); + + const currentUserId = Meteor.userId(); + if (!currentUserId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'getUserRoomRole', + }); + } + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); + + if (!subscription) { + console.log('[getUserRoomRole] Subscription not found:', { rid, userId, role }); + return false; + } + + const hasRole = subscription.roles?.includes(role) ?? false; + console.log('[getUserRoomRole] Checked role:', { rid, userId, role, hasRole }); + return hasRole; + }, +}); diff --git a/apps/meteor/server/methods/getUsersWhoReadImportantMessage.ts b/apps/meteor/server/methods/getUsersWhoReadImportantMessage.ts new file mode 100644 index 0000000000000..fd3453af9cc20 --- /dev/null +++ b/apps/meteor/server/methods/getUsersWhoReadImportantMessage.ts @@ -0,0 +1,58 @@ +import { Messages, Users } from '@rocket.chat/models'; +import { check } from 'meteor/check'; +import { Meteor } from 'meteor/meteor'; + +declare module '@rocket.chat/ddp-client' { + interface ServerMethods { + getUsersWhoReadImportantMessage(messageId: string): Array<{ _id: string; username: string; name?: string }>; + } +} + +Meteor.methods({ + async getUsersWhoReadImportantMessage(messageId: string): Promise> { + check(messageId, String); + + const userId = Meteor.userId(); + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'getUsersWhoReadImportantMessage', + }); + } + + console.log('[getUsersWhoReadImportantMessage] Fetching readers for message:', messageId); + + const message = await Messages.findOneById(messageId); + if (!message) { + console.error('[getUsersWhoReadImportantMessage] Message not found:', messageId); + throw new Meteor.Error('error-invalid-message', 'Invalid message', { + method: 'getUsersWhoReadImportantMessage', + }); + } + + if (!message.isImportant) { + console.error('[getUsersWhoReadImportantMessage] Message not marked as important:', messageId); + throw new Meteor.Error('error-not-important-message', 'Message is not marked as important', { + method: 'getUsersWhoReadImportantMessage', + }); + } + + const userIds = message.importantReadBy || []; + if (userIds.length === 0) { + console.log('[getUsersWhoReadImportantMessage] No readers yet:', messageId); + return []; + } + + const users = await Users.find( + { _id: { $in: userIds } }, + { projection: { _id: 1, username: 1, name: 1 } } + ).toArray(); + + console.log('[getUsersWhoReadImportantMessage] Found readers:', { messageId, count: users.length }); + + return users.map(user => ({ + _id: user._id, + username: user.username || '', + name: user.name, + })); + }, +}); diff --git a/apps/meteor/server/methods/index.ts b/apps/meteor/server/methods/index.ts index 544370864f5e2..c125ffe45e933 100644 --- a/apps/meteor/server/methods/index.ts +++ b/apps/meteor/server/methods/index.ts @@ -2,6 +2,7 @@ import '../../imports/personal-access-tokens/server/api/methods'; import './OEmbedCacheCleanup'; import './addAllUserToRoom'; +import './addRoomImportantMessageMarker'; import './addRoomLeader'; import './addRoomModerator'; import './addRoomOwner'; @@ -14,6 +15,8 @@ import './deleteUser'; import './getRoomById'; import './getRoomIdByNameOrId'; import './getRoomNameById'; +import './getUserRoomRole'; +import './getUsersWhoReadImportantMessage'; import './getSetupWizardParameters'; import './getTotalChannels'; import './getUsersOfRoom'; @@ -34,6 +37,7 @@ import './registerUser'; import './removeRoomLeader'; import './removeRoomModerator'; import './removeRoomOwner'; +import './removeRoomImportantMessageMarker'; import './removeUserFromRoom'; import './requestDataDownload'; import './resetAvatar'; @@ -44,6 +48,7 @@ import './sendForgotPasswordEmail'; import './setAvatarFromService'; import './setUserActiveStatus'; import './toggleFavorite'; +import './toggleImportantMessageRead'; import './unmuteUserInRoom'; import './userPresence'; import './userSetUtcOffset'; diff --git a/apps/meteor/server/methods/removeRoomImportantMessageMarker.ts b/apps/meteor/server/methods/removeRoomImportantMessageMarker.ts new file mode 100644 index 0000000000000..5c6cf38c60bbb --- /dev/null +++ b/apps/meteor/server/methods/removeRoomImportantMessageMarker.ts @@ -0,0 +1,128 @@ +import { api, Message, Team } from '@rocket.chat/core-services'; +import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import type { ServerMethods } from '@rocket.chat/ddp-client'; +import { Subscriptions, Rooms, Users } from '@rocket.chat/models'; +import { check } from 'meteor/check'; +import { Meteor } from 'meteor/meteor'; + +import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; +import { settings } from '../../app/settings/server'; +import { beforeChangeRoomRole } from '../../lib/callbacks/beforeChangeRoomRole'; +import { syncRoomRolePriorityForUserAndRoom } from '../lib/roles/syncRoomRolePriority'; + +declare module '@rocket.chat/ddp-client' { + interface ServerMethods { + removeRoomImportantMessageMarker(rid: IRoom['_id'], userId: IUser['_id']): boolean; + } +} + +export const removeRoomImportantMessageMarker = async ( + fromUserId: IUser['_id'], + rid: IRoom['_id'], + userId: IUser['_id'], +): Promise => { + check(rid, String); + check(userId, String); + + console.log('[removeRoomImportantMessageMarker] Starting:', { fromUserId, rid, userId }); + + const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); + if (!room) { + console.error('[removeRoomImportantMessageMarker] Room not found:', rid); + throw new Meteor.Error('error-invalid-room', 'Invalid room', { + method: 'removeRoomImportantMessageMarker', + }); + } + + if (!(await hasPermissionAsync(fromUserId, 'set-important-message-marker', rid))) { + console.error('[removeRoomImportantMessageMarker] Permission denied:', { fromUserId, rid }); + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'removeRoomImportantMessageMarker', + }); + } + + const user = await Users.findOneById(userId); + if (!user?.username) { + console.error('[removeRoomImportantMessageMarker] User not found:', userId); + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'removeRoomImportantMessageMarker', + }); + } + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, user._id); + if (!subscription) { + console.error('[removeRoomImportantMessageMarker] Subscription not found:', { rid, userId }); + throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', { + method: 'removeRoomImportantMessageMarker', + }); + } + + if (!Array.isArray(subscription.roles) || !subscription.roles.includes('important-message-marker')) { + console.error('[removeRoomImportantMessageMarker] User does not have role:', { rid, userId }); + throw new Meteor.Error('error-user-does-not-have-role', 'User does not have the role', { + method: 'removeRoomImportantMessageMarker', + }); + } + + await beforeChangeRoomRole.run({ fromUserId, userId, room, role: 'user' }); + + const removeRoleResponse = await Subscriptions.removeRoleById(subscription._id, 'important-message-marker'); + await syncRoomRolePriorityForUserAndRoom( + userId, + rid, + subscription.roles?.filter((r) => r !== 'important-message-marker') || [], + ); + + if (removeRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } + + const fromUser = await Users.findOneById(fromUserId); + if (!fromUser) { + console.error('[removeRoomImportantMessageMarker] FromUser not found:', fromUserId); + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'removeRoomImportantMessageMarker', + }); + } + + await Message.saveSystemMessage('subscription-role-removed', rid, user.username, fromUser, { role: 'important-message-marker' }); + + const team = await Team.getOneByMainRoomId(rid); + if (team) { + await Team.removeRolesFromMember(team._id, userId, ['important-message-marker']); + } + + const event = { + type: 'removed', + _id: 'important-message-marker', + u: { + _id: user._id, + username: user.username, + name: user.name, + }, + scope: rid, + } as const; + + if (settings.get('UI_DisplayRoles')) { + void api.broadcast('user.roleUpdate', event); + } + + void api.broadcast('federation.userRoleChanged', { ...event, givenByUserId: fromUserId }); + + console.log('[removeRoomImportantMessageMarker] Role removed successfully:', { rid, userId }); + return true; +}; + +Meteor.methods({ + async removeRoomImportantMessageMarker(rid, userId) { + const uid = Meteor.userId(); + if (!uid) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'removeRoomImportantMessageMarker', + }); + } + + return removeRoomImportantMessageMarker(uid, rid, userId); + }, +}); \ No newline at end of file diff --git a/apps/meteor/server/methods/toggleImportantMessageRead.ts b/apps/meteor/server/methods/toggleImportantMessageRead.ts new file mode 100644 index 0000000000000..6a8ebd0691db7 --- /dev/null +++ b/apps/meteor/server/methods/toggleImportantMessageRead.ts @@ -0,0 +1,58 @@ +import { Messages } from '@rocket.chat/models'; +import { check } from 'meteor/check'; +import { Meteor } from 'meteor/meteor'; + +declare module '@rocket.chat/ddp-client' { + interface ServerMethods { + toggleImportantMessageRead(messageId: string): boolean; + } +} + +Meteor.methods({ + async toggleImportantMessageRead(messageId: string): Promise { + check(messageId, String); + + const userId = Meteor.userId(); + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'toggleImportantMessageRead', + }); + } + + console.log('[toggleImportantMessageRead] Starting:', { messageId, userId }); + + const message = await Messages.findOneById(messageId); + if (!message) { + console.error('[toggleImportantMessageRead] Message not found:', messageId); + throw new Meteor.Error('error-invalid-message', 'Invalid message', { + method: 'toggleImportantMessageRead', + }); + } + + if (!message.isImportant) { + console.error('[toggleImportantMessageRead] Message not marked as important:', messageId); + throw new Meteor.Error('error-not-important-message', 'Message is not marked as important', { + method: 'toggleImportantMessageRead', + }); + } + + const importantReadBy = message.importantReadBy || []; + const isRead = importantReadBy.includes(userId); + + if (isRead) { + await Messages.updateOne( + { _id: messageId }, + { $pull: { importantReadBy: userId } } + ); + console.log('[toggleImportantMessageRead] Marked as unread:', { messageId, userId }); + } else { + await Messages.updateOne( + { _id: messageId }, + { $addToSet: { importantReadBy: userId } } + ); + console.log('[toggleImportantMessageRead] Marked as read:', { messageId, userId }); + } + + return !isRead; + }, +}); diff --git a/apps/meteor/server/startup/importantMessageMarkerRole.ts b/apps/meteor/server/startup/importantMessageMarkerRole.ts new file mode 100644 index 0000000000000..6c09b8a3fff66 --- /dev/null +++ b/apps/meteor/server/startup/importantMessageMarkerRole.ts @@ -0,0 +1,29 @@ +import { Meteor } from 'meteor/meteor'; +import { Roles } from '@rocket.chat/models'; +import type { IRole } from '@rocket.chat/core-typings'; + +Meteor.startup(async () => { + const roleName = 'important-message-marker'; + + // Проверяем, есть ли уже роль + const existingRole: IRole | null = await Roles.findOne({ name: roleName }); + + if (!existingRole) { + // Создаём новую роль с нужными полями для комнат (scope: 'Subscriptions') + await Roles.insertOne({ + name: roleName, + description: 'Role to allow marking messages as important in rooms', + scope: 'Subscriptions', // Ключевой момент для комнатной роли + protected: false, // необязательная, можно удалить + mandatory2fa: false, // необязательная, можно удалить + }); + console.log(`Role ${roleName} created`); + } else { + // Если роль уже есть, можно обновить описание + await Roles.updateOne( + { _id: existingRole._id }, + { $set: { description: 'Role to allow marking messages as important in rooms', scope: 'Subscriptions' } } + ); + console.log(`Role ${roleName} already exists and updated`); + } +}); \ No newline at end of file diff --git a/apps/meteor/server/startup/index.ts b/apps/meteor/server/startup/index.ts index 60a57895286b7..c1c3d4e401c64 100644 --- a/apps/meteor/server/startup/index.ts +++ b/apps/meteor/server/startup/index.ts @@ -2,6 +2,7 @@ import './appcache'; import './callbacks'; import { startCronJobs } from './cron'; import './initialData'; +import './importantMessageMarkerRole'; import './serverRunning'; import './coreApps'; import { generateFederationKeys } from './generateKeys'; diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index 94dc60aa1488b..626c7a6ef8ee1 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -169,6 +169,8 @@ export interface IMessage extends IRocketChatRecord { pinnedAt?: Date; pinnedBy?: Pick; unread?: boolean; + isImportant?: boolean; + importantReadBy?: string[]; temp?: boolean; drid?: IRoom['_id']; tlm?: Date; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index eb71374580bd1..bbe2795533f1c 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2141,6 +2141,7 @@ "Export_Messages": "Export messages", "Export_My_Data": "Export My Data (JSON)", "export-messages-as-pdf": "Export messages as PDF", + "export-messages-as-pdf_description": "Permission to export messages as PDF", "Export_most_recent_logs": "Export most recent logs", "Export_as_PDF": "Export as PDF", "Export_as_file": "Export as file", @@ -2148,6 +2149,10 @@ "Export_enabled_at_the_end_of_the_conversation": "Export enabled at the end of the conversation", "Extended": "Extended", "Extension": "Extension", + "mark-message-as-important": "Mark Message as Important", + "mark-message-as-important_description": "Permission to mark messages as important requiring acknowledgment from recipients", + "Extension_Number": "Extension Number", + "Extension_Status": "Extension Status", "Extension_removed": "Extension removed", "Extensions": "Extensions", "External": "External", @@ -6574,6 +6579,13 @@ "manage-the-app": "Manage the App", "manage-user-status": "Manage User Status", "manage-user-status_description": "Permission to manage the server custom user statuses", + "manage-voip-call-settings": "Manage Voip Call Settings", + "manage-voip-call-settings_description": "Permission to manage voip call settings", + "manage-voip-contact-center-settings": "Manage Voip Contact Center Settings", + "manage-voip-contact-center-settings_description": "Permission to manage voip contact center settings", + "manage-voip-extensions": "Manage Voice Calls", + "manage-voip-extensions_description": "Permission to manage voice calls and assign extensions to users", + "Mark_as_important": "Mark as important", "marketplace_featured_section_community_featured": "Featured Community Apps", "marketplace_featured_section_community_supported": "Community Supported Apps", "marketplace_featured_section_enterprise": "Featured Enterprise Apps", @@ -6881,6 +6893,10 @@ "set-moderator_description": "Permission to set other users as moderator of a channel", "set-owner": "Set Owner", "set-owner_description": "Permission to set other users as owner of a channel", + "set-important-message-marker": "Set Important Message Marker", + "set-important-message-marker_description": "Permission to grant other users the ability to mark messages as important", + "Set_as_important_message_marker": "Grant ability to mark important messages", + "Remove_as_important_message_marker": "Remove ability to mark important messages", "set-react-when-readonly": "Set React When ReadOnly", "set-react-when-readonly_description": "Permission to set the ability to react to messages in a read only channel", "set-readonly": "Set ReadOnly",