Plugins cannot tell that a chat message was deleted
Describe the bug Everything a plugin needs in order to notice a deletion is already loaded in the client and dropped at the last step.
The chat message subscription already selects the fields (chat-message-list/page/queries.ts), Message (Types/message.ts) declares them as deletedAt: string | null and deletedBy: { name } | null, and the page component hands the whole object to setLoadedMessageGathering. They then reach the plugins-engine intact and are dropped there — twice, in two files under plugins-engine/data-consumption/domain/chat/loaded-chat-messages/.
hook-manager.ts, selecting what to gather:
const [chatMessagesData] = useLoadedPageGathering((message: Partial<Message>) => ({
createdAt: message.createdAt,
message: message.message,
messageId: message.messageId,
user: message.user,
senderRole: message.senderRole,
messageMetadata: message.messageMetadata,
}));utils.ts, shaping what is handed to the plugin:
data: responseData.map((chatMessagesData) => ({
createdAt: chatMessagesData.createdAt,
message: chatMessagesData.message,
messageId: chatMessagesData.messageId,
senderUserId: chatMessagesData.user?.userId,
senderRole: chatMessagesData.senderRole,
messageMetadata: chatMessagesData.messageMetadata,
}) as PluginSdk.LoadedChatMessage),So useLoadedChatMessages gives a plugin no way to distinguish a deleted message from a live one. The fix is to carry deletedAt and deletedBy through both. No server change, no query change.
Why it matters
ChatMessageDAO.softDelete nulls message and messageAsHtml and records deletedAt / deletedByUserId, but leaves messageMetadata untouched. A plugin selecting its own messages by metadata therefore keeps matching a deleted message forever — sample-server-commands-plugin in the SDK repository does exactly that:
return (messageMetadata.pluginName === pluginName) && messageMetadata.custom;#25756 fixed the delivery side, so the element of a deleted message is now correctly dropped. But the plugin still has no way to know it should stop requesting that id or drop its own state for it, and it cannot infer it: a message that is merely scrolled out of the page is absent from the DOM-element payload in exactly the same way as a deleted one.
The client already relies on this same information to serve plugins, just not in the plugins-engine: manipulableMessageCount in chat-message-list/page/component.tsx filters on !message.deletedAt to decide when the DOM-element payload is complete (added by #25756). The deletion state is load-bearing one layer down and discarded one layer up.
Secondary: Message.message is typed string but can be null
Since softDelete nulls the column, the top-level declaration in Types/message.ts is wrong:
message: string;
messageAsHtml: string;The nested replyToMessage in the same interface already gets this right (string | null). Any consumer trusting the top-level declaration crashes on the first deleted message. This is independent of the mapping fix and affects core consumers too.
To Reproduce
- Load
samples/sample-server-commands-pluginfrom the plugin SDK repository in a meeting - Click "Send custom chat message"
- Delete that message from the chat UI
- Observe the objects returned by
useLoadedChatMessagesin the plugin
Expected behavior
The plugin can tell the message was deleted by testing deletedAt !== null — not by testing message === null, and not by observing that a DOM element stopped arriving — and can then drop its own state for it and stop requesting its DOM element.
Actual behavior
The message keeps appearing in useLoadedChatMessages indistinguishable from a live one, except that message silently becomes null, which the type does not admit. messageMetadata is unchanged, so metadata-based selection keeps matching it.
BBB version:
- BigBlueButton 4.0.0-rc.2
Source: bigbluebutton/bigbluebutton