Skip to content

Conversation

@marcellintacite
Copy link
Contributor

@marcellintacite marcellintacite commented Nov 23, 2025

Feature: Add support for XEP-0444 Message Reactions

Description

This PR implements XEP-0444: Message Reactions, allowing users to react to messages with emojis. It introduces a new plugin (reactions) that handles the UI for picking reactions, displaying them on messages, and managing the underlying XMPP stanza logic.

Key Changes

1. New Plugin: reactions

  • Created a standalone plugin structure in src/plugins/reactions/.
  • Implemented logic to parse incoming <reaction> elements from message stanzas.
  • Implemented logic to send reaction stanzas with the correct XML namespace (urn:xmpp:reactions:0).
  • Added optimistic UI updates (reactions appear immediately before server confirmation).

2. UI Components

  • Reaction Picker (converse-reaction-picker):
    • Added a "Quick Actions" bar showing popular emojis (👍, ❤️, 😂, 😮).
    • Integrated a full emoji picker dropdown (lazy-loaded for performance).
    • Styled with glassmorphism effects to blend with the chat interface.
    • Uses CSS Anchor Positioning (with fallbacks) for robust dropdown placement.
  • Message Bubbles:
    • Updated message templates to render reaction "pills" below messages.
    • Styled to resemble modern chat apps (Slack-like): rounded pills, hover effects, and active states.
    • Reactions are aggregated (e.g., "👍 3").
    • Clicking an existing reaction toggles it (edit your vote).

3. Technical Details

  • Standard Compliance: Fully compliant with XEP-0444: Message Reactions.
  • Styling: Reused existing emoji picker styles (src/shared/chat/styles/emoji.scss) to ensure consistency between the chat input and the reaction picker.

Screenshots / Video

emoji_converse.mp4

This still a Draft

  • Add a changelog entry for your change in CHANGES.md
  • When adding a configuration variable, please make sure to
    document it in docs/source/configuration.rst
  • Please add a test for your change. Tests can be run in the commandline
    with make check or you can run them in the browser by running make serve

@marcellintacite marcellintacite marked this pull request as draft November 23, 2025 07:51
@JohnXLivingston
Copy link
Contributor

@marcellintacite , did you see that it is possible to add «custom emojis» in the ConverseJS emoji picker? (see bellow for an explanation). I think those emojis should not be selectable as a message reaction.

With this feature, you can link a code name (for example :foo:) to an image. This is not a standard, and this will only work for people using ConverseJS on the same server as you.
You can test it here for example, with the code :pen:: demo.
If you open the emoji picker, you will see these custom emoji at the top of the list, in a section called "stickers" ("autocollants" in french).

@JohnXLivingston
Copy link
Contributor

I thinks there are 2 other missing things in your implementation:

Discovering support

ConverseJS must declare to other client that it handles message reactions, by adding the feature urn:xmpp:reactions:0 to the discovery response. See https://xmpp.org/extensions/xep-0444.html#disco-base

The XEP says it MUST be implemented.

Also, if you are chatting with a user (in a 1 to 1 conversation) that does not support this feature, maybe we should not display the action in the chatbox.
For MUC, maybe we could just display the feature in all cases.

Restricted reactions

The XEP allow some clients or MUC to limit the set of supported emojis. See https://xmpp.org/extensions/xep-0444.html#disco-restricted
The XEP says it SHOULD be implemented.

So, you should check if there is such limitation (for a user in 1 to 1 conversations, and for the MUC in rooms), and filter the emoji picker.

@Neustradamus
Copy link

@marcellintacite: Have you seen @JohnXLivingston comments?

@marcellintacite
Copy link
Contributor Author

@marcellintacite: Have you seen @JohnXLivingston comments?

Yes, i am working on it

@Neustradamus
Copy link

@marcellintacite: Good :)
When it is like this, do not hesitate to comment to confirm that you work on it ^^

@marcellintacite
Copy link
Contributor Author

@marcellintacite: Good :) When it is like this, do not hesitate to comment to confirm that you work on it ^^

Alright , thanks. I am sorry i didn't remember. I'll do

@marcellintacite
Copy link
Contributor Author

@marcellintacite , did you see that it is possible to add «custom emojis» in the ConverseJS emoji picker? (see bellow for an explanation). I think those emojis should not be selectable as a message reaction.

With this feature, you can link a code name (for example :foo:) to an image. This is not a standard, and this will only work for people using ConverseJS on the same server as you. You can test it here for example, with the code :pen:: demo. If you open the emoji picker, you will see these custom emoji at the top of the list, in a section called "stickers" ("autocollants" in french).

I added a support for them

@marcellintacite
Copy link
Contributor Author

I thinks there are 2 other missing things in your implementation:

Discovering support

ConverseJS must declare to other client that it handles message reactions, by adding the feature urn:xmpp:reactions:0 to the discovery response. See https://xmpp.org/extensions/xep-0444.html#disco-base

The XEP says it MUST be implemented.

Also, if you are chatting with a user (in a 1 to 1 conversation) that does not support this feature, maybe we should not display the action in the chatbox. For MUC, maybe we could just display the feature in all cases.

Restricted reactions

The XEP allow some clients or MUC to limit the set of supported emojis. See https://xmpp.org/extensions/xep-0444.html#disco-restricted The XEP says it SHOULD be implemented.

So, you should check if there is such limitation (for a user in 1 to 1 conversations, and for the MUC in rooms), and filter the emoji picker.

can you check my implementation please

*/
api.listen.on('getMessageActionButtons', (el, buttons) => {
const is_own_message = el.model.get('sender') === 'me';
if (!is_own_message) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why removing the buttons on our messages? Nothing says we can't react to our own messages :)

Copy link
Member

Choose a reason for hiding this comment

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

@marcellintacite I tend to agree that you should be able to react to your own messages. At least in Slack you can do so.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will be working on it

*/
shouldBeHidden (shortname) {
// Helper method for the template which decides whether an
// emoji should be hidden, based on which skin tone is
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment should be updated. It is no more only based on the skin tone.

Copy link
Member

Choose a reason for hiding this comment

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

@marcellintacite Looks like you marked some comments from @JohnXLivingston as resolved without addressing them?

I would add that the (updated) comment should go above the function into the docstring.

@JohnXLivingston
Copy link
Contributor

@marcellintacite , did you see that it is possible to add «custom emojis» in the ConverseJS emoji picker? (see bellow for an explanation). I think those emojis should not be selectable as a message reaction.
With this feature, you can link a code name (for example :foo:) to an image. This is not a standard, and this will only work for people using ConverseJS on the same server as you. You can test it here for example, with the code :pen:: demo. If you open the emoji picker, you will see these custom emoji at the top of the list, in a section called "stickers" ("autocollants" in french).

I added a support for them

What do you mean by "added a support for them"? I can't find the related code.
I'm not sure we understand each other. IMHO, custom emoji must be removed from the picker for message reaction. As there are not real emoji.
I think the shouldBeHidden method should just remove all "emojis" that don't have any associated unicode character. (you can add an option to the picker, like you did with allowed_emojis, to say that we only want unicode emojis).

@JohnXLivingston
Copy link
Contributor

can you check my implementation please

Yeah, the XEP compliance seems OK now :)

@marcellintacite
Copy link
Contributor Author

@marcellintacite , did you see that it is possible to add «custom emojis» in the ConverseJS emoji picker? (see bellow for an explanation). I think those emojis should not be selectable as a message reaction.
With this feature, you can link a code name (for example :foo:) to an image. This is not a standard, and this will only work for people using ConverseJS on the same server as you. You can test it here for example, with the code :pen:: demo. If you open the emoji picker, you will see these custom emoji at the top of the list, in a section called "stickers" ("autocollants" in french).

I added a support for them

What do you mean by "added a support for them"? I can't find the related code. I'm not sure we understand each other. IMHO, custom emoji must be removed from the picker for message reaction. As there are not real emoji. I think the shouldBeHidden method should just remove all "emojis" that don't have any associated unicode character. (you can add an option to the picker, like you did with allowed_emojis, to say that we only want unicode emojis).

Thanks ,
I've updated the PR to address your feedback. Users can now react to their own messages, as I've removed the is_own_message check, and I've ensured custom stickers are properly filtered out of the reaction picker by enforcing a unicode codepoint check. Additionally, i have updated the code comments to accurately reflect the filtering logic.

@marcellintacite
Copy link
Contributor Author

Hi @JohnXLivingston , could you please take a look at the MR? I need to finalize it for review now that it's in draft form.

@JohnXLivingston
Copy link
Contributor

Hi @JohnXLivingston , could you please take a look at the MR? I need to finalize it for review now that it's in draft form.

I took a quick look. Seems good now. I think you can mark it as ready for a review.

@marcellintacite marcellintacite marked this pull request as ready for review January 2, 2026 16:57
@jcbrand
Copy link
Member

jcbrand commented Jan 5, 2026

@marcellintacite there are 10 failing tests.

Looks like a bunch of them are because the CAPS version string is different, since you're now advertising support for a new additional XMPP feature.

You'll have to update the tests to check for the new version string.

See here for example:

Stanzas don't match:
	Actual:
	<presence xmlns="jabber:client" id="8ff86976-eed3-4dd8-ba12-82979e5efb50" from="[email protected]/orchard" to="[email protected]/JC"><x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/><password>secret</password></x><c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org/" ver="IVrSARVJ+weVvRNIXt5E3K0e1NE="/></presence>
	Expected:
	<presence xmlns="jabber:client" from="[email protected]/orchard" id="8ff86976-eed3-4dd8-ba12-82979e5efb50" to="[email protected]/JC"><x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/><password>secret</password></x><c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org/" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s="/></presence>
	    at <Jasmine>
	    at src/plugins/bookmark-views/tests/bookmarks.js:268:29
	    at async UserContext.<anonymous> (src/headless/tests/mock.js:648:13)

The old version string is qgxN8hmrdSa2/4/7PUoM9bPFN2s= and it needs to be replaced with the new one which is IVrSARVJ+weVvRNIXt5E3K0e1NE=

@marcellintacite
Copy link
Contributor Author

@marcellintacite there are 10 failing tests.

Looks like a bunch of them are because the CAPS version string is different, since you're now advertising support for a new additional XMPP feature.

You'll have to update the tests to check for the new version string.

See here for example:

Stanzas don't match:
	Actual:
	<presence xmlns="jabber:client" id="8ff86976-eed3-4dd8-ba12-82979e5efb50" from="[email protected]/orchard" to="[email protected]/JC"><x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/><password>secret</password></x><c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org/" ver="IVrSARVJ+weVvRNIXt5E3K0e1NE="/></presence>
	Expected:
	<presence xmlns="jabber:client" from="[email protected]/orchard" id="8ff86976-eed3-4dd8-ba12-82979e5efb50" to="[email protected]/JC"><x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/><password>secret</password></x><c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org/" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s="/></presence>
	    at <Jasmine>
	    at src/plugins/bookmark-views/tests/bookmarks.js:268:29
	    at async UserContext.<anonymous> (src/headless/tests/mock.js:648:13)

The old version string is qgxN8hmrdSa2/4/7PUoM9bPFN2s= and it needs to be replaced with the new one which is IVrSARVJ+weVvRNIXt5E3K0e1NE=

Thanks, i am working on it

@marcellintacite
Copy link
Contributor Author

Hello @jcbrand ,

I've updated all the test files to use the new CAPS version string and rebased with the latest changes.

I also added the dependencies array to the reactions plugin to ensure converse-disco is loaded first

@jcbrand
Copy link
Member

jcbrand commented Jan 13, 2026

Thanks @marcellintacite, can you please do a rebase onto the master branch?

Currently there are a bunch of unrelated commits in this PR.

@marcellintacite
Copy link
Contributor Author

Thanks @marcellintacite, can you please do a rebase onto the master branch?

Currently there are a bunch of unrelated commits in this PR.

Done

Copy link
Member

@jcbrand jcbrand left a comment

Choose a reason for hiding this comment

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

Thanks @marcellintacite. I did a first review from my side and left some review comments.

registerEvents() {
this.onKeyDown = (ev) => this.#onKeyDown(ev);
this.dropdown.addEventListener("hide.bs.dropdown", () => this.onDropdownHide());
this.dropdown?.addEventListener("hide.bs.dropdown", () => this.onDropdownHide());
Copy link
Member

Choose a reason for hiding this comment

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

Why would this.dropdown sometimes be undefined?

This seems like a code smell (red flag) that masks a potential underlying problem.

*/
shouldBeHidden (shortname) {
// Helper method for the template which decides whether an
// emoji should be hidden, based on which skin tone is
Copy link
Member

Choose a reason for hiding this comment

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

@marcellintacite Looks like you marked some comments from @JohnXLivingston as resolved without addressing them?

I would add that the (updated) comment should go above the function into the docstring.

*/
api.listen.on('getMessageActionButtons', (el, buttons) => {
const is_own_message = el.model.get('sender') === 'me';
if (!is_own_message) {
Copy link
Member

Choose a reason for hiding this comment

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

@marcellintacite I tend to agree that you should be able to react to your own messages. At least in Slack you can do so.

}

// Create reaction picker component
const pickerEl = document.createElement('converse-reaction-picker');
Copy link
Member

Choose a reason for hiding this comment

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

We are trying to avoid imperative statements like document.createElement and container.appendChild to update the DOM and instead do everything declaratively via the html tagged template literal from lit-html.

So ideally there should be a <converse-reaction-picker></converse-reaction-picker> component rendered inside a template somewhere.

I understand that the template might be outside of this plugin, which is OK, then that other plugin needs to declare a dependency on this plugin.

If this plugin is not loaded, then the <converse-reaction-picker> element will just not render.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I have to render the component directly inside the chatview/templates/chat.js
file?

Copy link
Member

@jcbrand jcbrand Jan 20, 2026

Choose a reason for hiding this comment

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

The chatview plugin concerns itself with one-on-one chats, so that's not the best place.

Perhaps it can go into src/shared/chat/templates/message.js ? I think that template is used by both MUCs and 1:1 chats, but you should please double-check.

The picker should render only conditionally, based on a state value, similar to how the OGP metadata is rendered only when ogp_metadata is set.

See here:

${el.model.get('ogp_metadata')?.map((m) => {

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.

4 participants