-
Notifications
You must be signed in to change notification settings - Fork 13.5k
feat: Adding scheduled messages #40310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 5 commits
e0d41ec
a792924
02e39ac
53f9023
f361b56
8823ec4
402768d
762aba9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| '@rocket.chat/models': minor | ||
| '@rocket.chat/meteor': minor | ||
| '@rocket.chat/ui-composer': minor | ||
| '@rocket.chat/i18n': minor | ||
| --- | ||
|
|
||
| Implemented complete message scheduling functionality, allowing users to schedule messages to be sent at a specific date and time. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| '@rocket.chat/ui-composer': minor | ||
| '@rocket.chat/models': minor | ||
| '@rocket.chat/i18n': minor | ||
| '@rocket.chat/meteor': minor | ||
| --- | ||
|
|
||
| Added scheduled messages |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| import type { IMessage } from '@rocket.chat/core-typings'; | ||
| import { Messages } from '@rocket.chat/models'; | ||
| import { ajv, validateBadRequestErrorResponse, validateUnauthorizedErrorResponse } from '@rocket.chat/rest-typings'; | ||
| import { check, Match } from 'meteor/check'; | ||
| import { Meteor } from 'meteor/meteor'; | ||
|
|
||
| import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; | ||
| import { API } from '../api'; | ||
| import { getPaginationItems } from '../helpers/getPaginationItems'; | ||
|
|
||
| API.v1.post( | ||
| 'chat.scheduleMessage', | ||
| { | ||
| authRequired: true, | ||
| response: { | ||
| 200: ajv.compile<{ message: IMessage }>({ | ||
| type: 'object', | ||
| properties: { | ||
| message: { $ref: '#/components/schemas/IMessage' }, | ||
| success: { type: 'boolean', enum: [true] }, | ||
| }, | ||
| required: ['message', 'success'], | ||
| additionalProperties: false, | ||
| }), | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| }, | ||
| }, | ||
| async function action(this: any) { | ||
| check(this.bodyParams, { | ||
| roomId: String, | ||
| message: String, | ||
| scheduledAt: String, | ||
| tmid: Match.Maybe(String), | ||
| }); | ||
|
|
||
| const { roomId, message, scheduledAt, tmid } = this.bodyParams as { | ||
| roomId: string; | ||
| message: string; | ||
| scheduledAt: string; | ||
| tmid?: string; | ||
| }; | ||
|
|
||
| const scheduledDate = new Date(scheduledAt); | ||
| if (isNaN(scheduledDate.getTime())) { | ||
| throw new Meteor.Error('error-invalid-date', 'Invalid date format', { method: 'chat.scheduleMessage' }); | ||
| } | ||
|
|
||
| if (scheduledDate <= new Date()) { | ||
| throw new Meteor.Error('error-past-date', 'Scheduled date must be in the future', { method: 'chat.scheduleMessage' }); | ||
| } | ||
|
|
||
| if (tmid) { | ||
| const parentMessage = await Messages.findOneById(tmid, { projection: { rid: 1 } }); | ||
| if (!parentMessage || parentMessage.rid !== roomId) { | ||
| throw new Meteor.Error('error-invalid-tmid', 'Thread message does not belong to this room', { method: 'chat.scheduleMessage' }); | ||
| } | ||
| } | ||
|
|
||
| await canSendMessageAsync(roomId, this.user); | ||
|
|
||
| const messageData = { | ||
| rid: roomId, | ||
| msg: message, | ||
| u: { | ||
| _id: this.user._id, | ||
| username: this.user.username, | ||
| name: this.user.name, | ||
| }, | ||
| ts: new Date(), | ||
| scheduledAt: scheduledDate, | ||
| scheduled: true, | ||
| ...(tmid && { tmid }), | ||
| }; | ||
|
|
||
| const result = await Messages.insertOne(messageData as IMessage); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| return API.v1.success({ | ||
| message: { | ||
| ...messageData, | ||
| _id: result.insertedId, | ||
| _updatedAt: new Date(), | ||
| }, | ||
| }); | ||
| }, | ||
| ); | ||
|
|
||
| API.v1.get( | ||
| 'chat.getScheduledMessages', | ||
| { | ||
| authRequired: true, | ||
| response: { | ||
| 200: ajv.compile<{ messages: IMessage[]; count: number; offset: number; total: number }>({ | ||
| type: 'object', | ||
| properties: { | ||
| messages: { type: 'array', items: { $ref: '#/components/schemas/IMessage' } }, | ||
| count: { type: 'number' }, | ||
| offset: { type: 'number' }, | ||
| total: { type: 'number' }, | ||
| success: { type: 'boolean', enum: [true] }, | ||
| }, | ||
| required: ['messages', 'count', 'offset', 'total', 'success'], | ||
| additionalProperties: false, | ||
| }), | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| }, | ||
| }, | ||
| async function action(this: any) { | ||
| const { roomId } = this.queryParams; | ||
| const { offset, count } = await getPaginationItems(this.queryParams); | ||
|
|
||
| check(roomId, String); | ||
|
|
||
| await canSendMessageAsync(roomId, this.user); | ||
|
|
||
| const messages = await Messages.find( | ||
| { | ||
| rid: roomId, | ||
| 'u._id': this.userId, | ||
| scheduled: true, | ||
| scheduledAt: { $exists: true }, | ||
| }, | ||
| { | ||
| sort: { scheduledAt: 1 }, | ||
| skip: Number(offset), | ||
| limit: Number(count), | ||
| }, | ||
| ).toArray(); | ||
|
|
||
| const total = await Messages.countDocuments({ | ||
| rid: roomId, | ||
| 'u._id': this.userId, | ||
| scheduled: true, | ||
| scheduledAt: { $exists: true }, | ||
| }); | ||
|
|
||
| return API.v1.success({ | ||
| messages, | ||
| count: messages.length, | ||
| offset, | ||
| total, | ||
| }); | ||
| }, | ||
| ); | ||
|
|
||
| API.v1.post( | ||
| 'chat.cancelScheduledMessage', | ||
| { | ||
| authRequired: true, | ||
| response: { | ||
| 200: ajv.compile<{ success: boolean }>({ | ||
| type: 'object', | ||
| properties: { | ||
| success: { type: 'boolean', enum: [true] }, | ||
| }, | ||
| required: ['success'], | ||
| additionalProperties: false, | ||
| }), | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| }, | ||
| }, | ||
| async function action(this: any) { | ||
| check(this.bodyParams, { | ||
| messageId: String, | ||
| }); | ||
|
|
||
| const { messageId } = this.bodyParams as { messageId: string }; | ||
|
|
||
| const result = await Messages.deleteOne({ | ||
| _id: messageId, | ||
| 'u._id': this.userId, | ||
| scheduled: true, | ||
| }); | ||
|
|
||
| if (result.deletedCount === 0) { | ||
| throw new Meteor.Error('error-message-not-found', 'Scheduled message not found or already sent', { | ||
| method: 'chat.cancelScheduledMessage', | ||
| }); | ||
| } | ||
|
|
||
| return API.v1.success({ success: true }); | ||
| }, | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import type { IRoom } from '@rocket.chat/core-typings'; | ||
|
|
||
| import { t } from '../../../../app/utils/lib/i18n'; | ||
| import { sdk } from '../../../../app/utils/client/lib/SDKClient'; | ||
| import { dispatchToastMessage } from '../../../lib/toast'; | ||
|
|
||
| export const scheduleMessage = async ({ | ||
| rid, | ||
| msg, | ||
| scheduledAt, | ||
| tmid, | ||
| }: { | ||
| rid: IRoom['_id']; | ||
| msg: string; | ||
| scheduledAt: Date; | ||
| tmid?: string; | ||
| }): Promise<void> => { | ||
| try { | ||
| await sdk.rest.post('/v1/chat.scheduleMessage', { | ||
| roomId: rid, | ||
| message: msg, | ||
| scheduledAt: scheduledAt.toISOString(), | ||
| ...(tmid && { tmid }), | ||
| }); | ||
|
|
||
| dispatchToastMessage({ type: 'success', message: t('Message_sent') }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Misleading success toast — message was scheduled, not sent.
🛠️ Suggested change- dispatchToastMessage({ type: 'success', message: t('Message_sent') });
+ dispatchToastMessage({ type: 'success', message: t('Message_scheduled') });The same issue exists in 🤖 Prompt for AI Agents |
||
| } catch (error: any) { | ||
| dispatchToastMessage({ type: 'error', message: error.message || t('Error') }); | ||
| throw error; | ||
| } | ||
| }; | ||
|
Comment on lines
+7
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for any importer of the scheduleMessage flow
rg -nP -C2 "from\s+['\"][^'\"]*chats/flows/scheduleMessage['\"]"
rg -nP -C2 "scheduleMessage\s*\(" -g '!**/scheduleMessage.ts'Repository: RocketChat/Rocket.Chat Length of output: 48 🏁 Script executed: # More flexible search for imports - check different import styles
rg -n "scheduleMessage" apps/meteor/client/lib/chats/flows/
# Search for the function name in all TypeScript/JavaScript files
rg -n "scheduleMessage" -t ts -t tsx --max-count=20
# Check if it's exported from an index file
rg -n "scheduleMessage" apps/meteor/client/lib/chats/ -t ts -t tsx
# Look at useScheduleMessageButton to see what it actually imports
fd "useScheduleMessageButton" --type fRepository: RocketChat/Rocket.Chat Length of output: 430 🏁 Script executed: # Read the useScheduleMessageButton file to see what it actually uses
cat -n apps/meteor/client/views/room/composer/messageBox/hooks/useScheduleMessageButton.tsxRepository: RocketChat/Rocket.Chat Length of output: 3510 🏁 Script executed: # Final comprehensive search - look for any import or require of scheduleMessage
# excluding the definition file itself
rg -n "scheduleMessage" --type ts --max-count=50 | grep -v "scheduleMessage.ts:7:" | grep -v "scheduleMessage.ts:19:"
# Also check if it's exported from any barrel/index files
fd "index\.(ts|js)" apps/meteor/client/lib/chats/flows/ -exec cat -n {} \;Repository: RocketChat/Rocket.Chat Length of output: 2077 🏁 Script executed: # Check if flows directory has an index file
find apps/meteor/client/lib/chats/flows -name "index.*" -exec cat {} \;
# Also verify sendMessage is actually imported and used to confirm the pattern difference
rg -n "from.*chats/flows/sendMessage" --max-count=10Repository: RocketChat/Rocket.Chat Length of output: 193 The
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; | ||
| import { useSetModal } from '@rocket.chat/ui-contexts'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import { useMemo } from 'react'; | ||
|
|
||
| import { MessageBoxScheduledMessagesModal } from '../../MessageBoxScheduledMessagesModal'; | ||
|
|
||
| export const useViewScheduledMessagesAction = (roomId: string): GenericMenuItemProps => { | ||
| const { t } = useTranslation(); | ||
| const setModal = useSetModal(); | ||
|
|
||
| return useMemo<GenericMenuItemProps>( | ||
| () => ({ | ||
| id: 'view-scheduled-messages', | ||
| content: t('View_Scheduled_Messages'), | ||
| icon: 'clock', | ||
| disabled: false, | ||
| onClick: () => { | ||
| setModal(<MessageBoxScheduledMessagesModal roomId={roomId} onClose={() => setModal(null)} />); | ||
| }, | ||
| }), | ||
| [t, roomId, setModal], | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| export const useScheduleMenuItems = (): GenericMenuItemProps[] => { | ||
| const { t } = useTranslation(); | ||
|
|
||
| return [ | ||
| { | ||
| id: 'schedule-new-message', | ||
| icon: 'clock', | ||
| content: t('Schedule_new_message'), | ||
| }, | ||
| { | ||
| id: 'view-scheduled-messages', | ||
| icon: 'list', | ||
| content: t('View_scheduled_messages'), | ||
| }, | ||
| ]; | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.