Skip to content

Comments

feat(apps): Implement deleteNotifyUser to remove ephemeral mess…#38413

Open
yush-1018 wants to merge 5 commits intoRocketChat:developfrom
yush-1018:feat/delete-notification-messages
Open

feat(apps): Implement deleteNotifyUser to remove ephemeral mess…#38413
yush-1018 wants to merge 5 commits intoRocketChat:developfrom
yush-1018:feat/delete-notification-messages

Conversation

@yush-1018
Copy link

@yush-1018 yush-1018 commented Jan 29, 2026

This PR enables Rocket.Chat Apps to delete notification (ephemeral) messages. Since these messages are not saved in the database, I implemented a deleteNotifyUser method that triggers a client-side event. When the client receives the message with a _deleted: true flag, it automatically removes it from the UI.

Fixes: #35801

Summary by CodeRabbit

  • New Features
    • Apps can now remove previously sent ephemeral notifications for a specific user or for all users in a room; deleted notifications are removed from users' message feeds and UI.
    • Notifier API expanded to support programmatic deletion of user notifications.
  • Examples
    • Demo command added showing sending an ephemeral notification, waiting, then deleting it to illustrate the flow.

@yush-1018 yush-1018 requested review from a team as code owners January 29, 2026 10:24
@changeset-bot
Copy link

changeset-bot bot commented Jan 29, 2026

⚠️ No Changeset found

Latest commit: 16fa490

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dionisio-bot
Copy link
Contributor

dionisio-bot bot commented Jan 29, 2026

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is missing the required milestone or project

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@CLAassistant
Copy link

CLAassistant commented Jan 29, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

No actionable comments were generated in the recent review. 🎉

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e702e5 and b4f75b0.

📒 Files selected for processing (1)
  • ExampleApp.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • ExampleApp.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer

Walkthrough

Adds delete-notify support across the Apps Engine: new Notifier API method, bridge abstractions, server bridge implementations to broadcast ephemeral deletion payloads (_deleted: true), and client handling to remove such messages from local state.

Changes

Cohort / File(s) Summary
Notifier API & Implementations
packages/apps-engine/src/definition/accessors/INotifier.ts, packages/apps-engine/src/server/accessors/Notifier.ts, packages/apps-engine/deno-runtime/lib/accessors/notifier.ts
Added deleteNotifyUser(user, message) to the public Notifier interface and implemented it in server and Deno runtime accessors; ensures message sender (app user) and delegates to the message bridge.
Message Bridge Pattern
packages/apps-engine/src/server/bridges/MessageBridge.ts, packages/apps-engine/tests/test-data/bridges/messageBridge.ts
Added doDeleteNotifyUser() wrapper with permission check and declared abstract deleteNotifyUser() on the MessageBridge API; test bridge includes a stubbed deleteNotifyUser.
Server Bridge Implementation
apps/meteor/app/apps/server/bridges/messages.ts
Implemented deleteNotifyUser() and deleteNotifyRoom() on AppMessageBridge: convert app messages, mark payloads with _deleted: true, and broadcast ephemeral deletion notifications to a single user or to each user in a room.
Client Stream Handling
apps/meteor/client/startup/incomingMessages.ts
Updated notify-user stream handler to accept an optional _deleted flag; when _deleted is present deletes the message from Messages.state (early return), otherwise continues to store/publish the message.
Example / Tests
ExampleApp.ts, packages/apps-engine/tests/.../messageBridge.ts
Added example app demonstrating sending and later deleting an ephemeral notification; test bridge updated to include the new method stub.

Sequence Diagram

sequenceDiagram
    participant App as App
    participant Notifier as Notifier
    participant Bridge as MessageBridge
    participant ServerBridge as AppMessageBridge
    participant Client as Client Stream Handler
    participant Messages as Messages State

    App->>Notifier: deleteNotifyUser(user, message)
    Notifier->>Notifier: ensure message.u (app user) exists
    Notifier->>Bridge: doDeleteNotifyUser(user, message, appId)
    Bridge->>Bridge: check write permissions
    Bridge->>ServerBridge: deleteNotifyUser(user, message, appId)
    ServerBridge->>ServerBridge: convert app message -> platform message
    ServerBridge->>ServerBridge: attach `_deleted: true`
    ServerBridge->>Client: broadcast ephemeral `notify-user` message
    Client->>Client: receive stream (msg._deleted)
    Client->>Messages: Messages.state.delete(msg._id)
    Messages->>Messages: remove message from local state
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped a note, then flagged it gone,
Sent a _deleted whisper at dawn,
Bridges rattled, streams gave a twirl,
Puff — the ephemeral vanished from the world,
Carrot crumbs left, I take a bow. 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(apps): Implement deleteNotifyUser to remove ephemeral mess…' clearly summarizes the main change—adding a deleteNotifyUser method to remove ephemeral/notification messages in the Apps engine.
Linked Issues check ✅ Passed The PR fully implements the requested solution from issue #35801: a deleteNotifyUser method to delete notification messages with client-side removal via _deleted flag and an example app demonstrating usage.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing deleteNotifyUser functionality across the apps engine layers and demonstrating its usage—no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/meteor/app/apps/server/bridges/messages.ts`:
- Around line 85-98: In deleteNotifyUser, ensure the converted app message has
an identifier before broadcasting deletion: after calling
this.orch.getConverters()?.get('messages').convertAppMessage(message) check that
msg and msg._id are present (and msg.rid if needed) and bail out (or log) if
missing so you don't send a notify.ephemeralMessage delete that clients cannot
match; update the guard around convertAppMessage to verify msg._id and avoid
broadcasting when it's absent.
🧹 Nitpick comments (1)
apps/meteor/client/startup/incomingMessages.ts (1)

13-16: Use a discriminated union for delete payload typing.
IMessage & { _deleted?: boolean } implies a full message even when the delete payload likely only carries _id. A union keeps the delete case minimal and narrows types correctly.

Suggested refactor
 import type { IMessage } from '@rocket.chat/core-typings';
 import { Meteor } from 'meteor/meteor';

+type NotifyUserMessage = (IMessage & { _deleted?: false }) | { _id: IMessage['_id']; _deleted: true };
+
 import { sdk } from '../../app/utils/client/lib/SDKClient';
 import { onLoggedIn } from '../lib/loggedIn';
 import { getUserId } from '../lib/user';
 import { Messages } from '../stores';
@@
-		return sdk.stream('notify-user', [`${getUserId()}/message`], (msg: IMessage & { _deleted?: boolean }) => {
+		return sdk.stream('notify-user', [`${getUserId()}/message`], (msg: NotifyUserMessage) => {
 			if (msg._deleted) {
 				return Messages.state.delete(msg._id);
 			}

Comment on lines +85 to +98
protected async deleteNotifyUser(user: IAppsUser, message: IAppsMessage, appId: string): Promise<void> {
this.orch.debugLog(`The App ${appId} is deleting a notification message.`);

const msg = await this.orch.getConverters()?.get('messages').convertAppMessage(message);

if (!msg) {
return;
}

void api.broadcast('notify.ephemeralMessage', user.id, msg.rid, {
...msg,
_deleted: true,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate a message id before broadcasting deletion.
If Line 88’s conversion yields a message without _id, the client can’t match and remove the notification, turning the delete into a silent no-op. Consider guarding for a missing id.

🛠️ Proposed guard
 	const msg = await this.orch.getConverters()?.get('messages').convertAppMessage(message);

 	if (!msg) {
 		return;
 	}
+
+	if (!msg._id) {
+		this.orch.debugLog(`The App ${appId} attempted to delete a notification without a message id.`);
+		return;
+	}

 	void api.broadcast('notify.ephemeralMessage', user.id, msg.rid, {
 		...msg,
 		_deleted: true,
 	});
🤖 Prompt for AI Agents
In `@apps/meteor/app/apps/server/bridges/messages.ts` around lines 85 - 98, In
deleteNotifyUser, ensure the converted app message has an identifier before
broadcasting deletion: after calling
this.orch.getConverters()?.get('messages').convertAppMessage(message) check that
msg and msg._id are present (and msg.rid if needed) and bail out (or log) if
missing so you don't send a notify.ephemeralMessage delete that clients cannot
match; update the guard around convertAppMessage to verify msg._id and avoid
broadcasting when it's absent.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 7 files

@d-gubert d-gubert changed the title feat(App Engine): Implement deleteNotifyUser to remove ephemeral mess… feat(apps): Implement deleteNotifyUser to remove ephemeral mess… Jan 31, 2026
@d-gubert
Copy link
Member

Hey @yush-1018 , can you provide an example app that shows how to use this new API?

@yush-1018
Copy link
Author

Hey @d-gubert,
Here is an example app showing how to use the new deleteNotifyUser API. It creates a slash command (/test-delete-notify) that sends an ephemeral message and then deletes it after 3 seconds.

import {
IAppAccessors,
IConfigurationExtend,
IHttp,
ILogger,
IModify,
IPersistence,
IRead,
} from '@rocket.chat/apps-engine/definition/accessors';
import { App } from '@rocket.chat/apps-engine/definition/App';
import { IMessage } from '@rocket.chat/apps-engine/definition/messages';
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
import {
ISlashCommand,
SlashCommandContext,
} from '@rocket.chat/apps-engine/definition/slashcommands';

export class DeleteNotifyUserApp extends App {
constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) {
super(info, logger, accessors);
}

public async extendConfiguration(
    configuration: IConfigurationExtend
): Promise<void> {
    await configuration.slashCommands.provideSlashCommand(
        new TestDeleteCommand()
    );
}

}

class TestDeleteCommand implements ISlashCommand {
public command = 'test-delete-notify';
public i18nParamsExample = '';
public i18nDescription = 'Tests the deleteNotifyUser API';
public providesPreview = false;

public async executor(
    context: SlashCommandContext,
    read: IRead,
    modify: IModify,
    http: IHttp,
    persis: IPersistence
): Promise<void> {
    const user = context.getSender();
    const room = context.getRoom();
    const notifier = modify.getNotifier();

    // 1. Construct the message
    const message: IMessage = notifier
        .getMessageBuilder()
        .setRoom(room)
        .setText('This message will be deleted in 3 seconds.')
        .getMessage();

    // IMPORTANT:
    // Since ephemeral messages are not persisted, they don't get an auto-generated ID.
    // We manually assign an ID to ensure we reference the correct message instance for deletion.
    message.id = 'test-delete-id-' + Date.now();

    // 2. Notify the user
    await notifier.notifyUser(user, message);

    // 3. Wait for 3 seconds
    await new Promise((resolve) => setTimeout(resolve, 3000));

    // 4. Delete the notification
    await notifier.deleteNotifyUser(user, message);
}

}

@codecov
Copy link

codecov bot commented Feb 19, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.60%. Comparing base (d6ef0db) to head (18b2274).
⚠️ Report is 19 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #38413      +/-   ##
===========================================
+ Coverage    70.58%   70.60%   +0.01%     
===========================================
  Files         3180     3180              
  Lines       111934   111933       -1     
  Branches     20245    20221      -24     
===========================================
+ Hits         79012    79030      +18     
+ Misses       30868    30848      -20     
- Partials      2054     2055       +1     
Flag Coverage Δ
unit 71.59% <ø> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@yush-1018 yush-1018 marked this pull request as draft February 20, 2026 09:31
@yush-1018 yush-1018 marked this pull request as ready for review February 20, 2026 18:07
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 9 files

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
ExampleApp.ts (4)

39-45: Prefix unused parameters with _ to signal intentional non-use.

read, http, and persis are required by the ISlashCommand interface but are never used in the body. Prefixing with _ satisfies TypeScript's noUnusedParameters rule and signals intent.

♻️ Proposed refactor
     public async executor(
         context: SlashCommandContext,
-        read: IRead,
+        _read: IRead,
         modify: IModify,
-        http: IHttp,
-        persis: IPersistence
+        _http: IHttp,
+        _persis: IPersistence
     ): Promise<void> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ExampleApp.ts` around lines 39 - 45, The executor method declared for the
ISlashCommand implementation has unused parameters (read, http, persis); update
the method signature in the executor function to prefix those parameters with an
underscore (e.g., _read, _http, _persis) so TypeScript's noUnusedParameters rule
is satisfied and intent is clear, leaving the rest of the method body and other
parameter names (context, modify) unchanged.

50-70: Remove inline comments from the implementation.

Comments on lines 50, 57–60, 63, 66, and 69 violate the coding guidelines ("Avoid code comments in the implementation"). The key design constraint (manual ID assignment for ephemeral messages) belongs in the PR description, README, or a JSDoc block on the class/method — not as inline comments.

As per coding guidelines: "Avoid code comments in the implementation."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ExampleApp.ts` around lines 50 - 70, Remove the inline comments surrounding
the ephemeral-message flow in this snippet (the comment blocks around
construction, the "VERY IMPORTANT" paragraph, and numbered steps) and keep the
implementation intact (message assignment and setRoom/setText/getMessage usage).
Move the rationale about manual ID assignment for ephemeral messages into a
JSDoc on the enclosing class or method (or the PR/README) so the requirement is
documented, and leave the explicit manual ID assignment (message.id =
'test-delete-id-' + Date.now()) along with the notifyUser and deleteNotifyUser
calls (notifier.getMessageBuilder, setRoom, setText, notifyUser,
deleteNotifyUser) unchanged.

20-22: Redundant passthrough constructor can be removed.

The constructor does nothing but forward arguments to super; TypeScript will emit the same behavior automatically without it.

♻️ Proposed refactor
 export class DeleteNotifyUserApp extends App {
-    constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) {
-        super(info, logger, accessors);
-    }
-
     public async extendConfiguration(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ExampleApp.ts` around lines 20 - 22, The constructor in class ExampleApp is a
redundant passthrough that only calls super(info, logger, accessors); remove the
explicit constructor method from ExampleApp (the symbols to change: the
constructor declaration in ExampleApp) so the base class constructor will be
invoked automatically; before removing, verify there is no custom initialization
or parameter handling in the constructor body and, if needed, move any such
logic elsewhere (none present here), then run a TypeScript build to confirm no
regressions.

67-67: setTimeout blocks the executor, but the 3-second delay is acceptable for example code.

The slash command executor blocks the Deno subprocess request slot for the full 3-second delay. While the Apps Engine runtime has a configurable timeout (default 30 seconds, controlled via APPS_ENGINE_RUNTIME_TIMEOUT), this approach still has limitations:

  1. It holds the executor slot during the delay, preventing reuse for other requests.
  2. The delay won't survive an app restart between the notify and delete calls.

For production apps, the IScheduler API (via modify.getScheduler()) is the idiomatic pattern for deferred work—it's non-blocking and restart-safe. For example code, the current setTimeout approach works, but consider demonstrating the scheduler pattern for better guidance to developers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ExampleApp.ts` at line 67, The current await new Promise(...) (setTimeout)
blocks the executor; replace it with the Apps Engine scheduler by calling
modify.getScheduler() (IScheduler) and scheduling a delayed task (e.g.,
scheduler.scheduleOnce or scheduler.createJob/schedule) to perform the later
delete/cleanup instead of awaiting setTimeout; capture whatever context you need
(message id, channel id, etc.) in the scheduled job payload so the scheduled
handler can run the existing notify and delete logic, and remove the blocking
await in the slash command executor.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b1bea9 and 0e702e5.

📒 Files selected for processing (2)
  • ExampleApp.ts
  • apps/meteor/app/apps/server/bridges/messages.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/meteor/app/apps/server/bridges/messages.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/playwright.mdc)

**/*.{ts,tsx,js}: Write concise, technical TypeScript/JavaScript with accurate typing in Playwright tests
Avoid code comments in the implementation

Files:

  • ExampleApp.ts
🧠 Learnings (1)
📚 Learning: 2025-12-02T22:23:49.593Z
Learnt from: d-gubert
Repo: RocketChat/Rocket.Chat PR: 37654
File: apps/meteor/client/hooks/useAppSlashCommands.ts:32-38
Timestamp: 2025-12-02T22:23:49.593Z
Learning: In apps/meteor/client/hooks/useAppSlashCommands.ts, the `data?.forEach((command) => slashCommands.add(command))` call during render is intentional. The query is configured with `structuralSharing: false` to prevent React Query from keeping stable data references, and `slashCommands.add` is idempotent, so executing on every render is acceptable and ensures the command registry stays current.

Applied to files:

  • ExampleApp.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ExampleApp.ts`:
- Line 61: The current assignment message.id = 'test-delete-id-' + Date.now()
can collide under concurrent invocations; update the assignment inside the
/test-delete-notify handler (where message.id is set) to use a cryptographically
unique ID such as crypto.randomUUID() or a composite key like
`${userId}-${Date.now()}` so each ephemeral message client-side is unique and
cannot be stale-targeted by concurrent requests.

---

Nitpick comments:
In `@ExampleApp.ts`:
- Around line 39-45: The executor method declared for the ISlashCommand
implementation has unused parameters (read, http, persis); update the method
signature in the executor function to prefix those parameters with an underscore
(e.g., _read, _http, _persis) so TypeScript's noUnusedParameters rule is
satisfied and intent is clear, leaving the rest of the method body and other
parameter names (context, modify) unchanged.
- Around line 50-70: Remove the inline comments surrounding the
ephemeral-message flow in this snippet (the comment blocks around construction,
the "VERY IMPORTANT" paragraph, and numbered steps) and keep the implementation
intact (message assignment and setRoom/setText/getMessage usage). Move the
rationale about manual ID assignment for ephemeral messages into a JSDoc on the
enclosing class or method (or the PR/README) so the requirement is documented,
and leave the explicit manual ID assignment (message.id = 'test-delete-id-' +
Date.now()) along with the notifyUser and deleteNotifyUser calls
(notifier.getMessageBuilder, setRoom, setText, notifyUser, deleteNotifyUser)
unchanged.
- Around line 20-22: The constructor in class ExampleApp is a redundant
passthrough that only calls super(info, logger, accessors); remove the explicit
constructor method from ExampleApp (the symbols to change: the constructor
declaration in ExampleApp) so the base class constructor will be invoked
automatically; before removing, verify there is no custom initialization or
parameter handling in the constructor body and, if needed, move any such logic
elsewhere (none present here), then run a TypeScript build to confirm no
regressions.
- Line 67: The current await new Promise(...) (setTimeout) blocks the executor;
replace it with the Apps Engine scheduler by calling modify.getScheduler()
(IScheduler) and scheduling a delayed task (e.g., scheduler.scheduleOnce or
scheduler.createJob/schedule) to perform the later delete/cleanup instead of
awaiting setTimeout; capture whatever context you need (message id, channel id,
etc.) in the scheduled job payload so the scheduled handler can run the existing
notify and delete logic, and remove the blocking await in the slash command
executor.

@yush-1018 yush-1018 marked this pull request as draft February 22, 2026 20:44
@yush-1018 yush-1018 marked this pull request as ready for review February 22, 2026 20:46
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 9 files

@yush-1018
Copy link
Author

Hi @d-gubert,
Please review for any changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(apps) : Delete notification messages on based on some event

3 participants