Skip to content

Commit d033b98

Browse files
Fixing issues related to naming,dom query, etc
1 parent 1546ba1 commit d033b98

File tree

7 files changed

+121
-133
lines changed

7 files changed

+121
-133
lines changed

src/plugins/reactions/index.js

Lines changed: 42 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@
1515
*/
1616

1717
import { converse, api, u, _converse } from '@converse/headless';
18+
import { updateMessageReactions, findMessage } from './utils.js';
1819
import './reaction-picker.js';
1920

2021
import { __ } from 'i18n';
2122

22-
converse.plugins.add('reactions', {
23+
const { Strophe } = converse.env;
24+
25+
Strophe.addNamespace('REACTIONS', 'urn:xmpp:reactions:0');
26+
27+
converse.plugins.add('converse-reactions', {
2328

2429
dependencies: ['converse-disco', 'converse-chatview', 'converse-muc-views'],
2530

@@ -31,14 +36,13 @@ converse.plugins.add('reactions', {
3136
* - Handling connection/reconnection events
3237
*/
3338
initialize () {
34-
const { Strophe } = converse.env;
3539
this.allowed_emojis = new Map(); // Store allowed emojis per JID
3640

3741
/**
3842
* Register the "urn:xmpp:reactions:0" feature
3943
*/
4044
api.listen.on('addClientFeatures', () => {
41-
api.disco.own.features.add('urn:xmpp:reactions:0');
45+
api.disco.own.features.add(Strophe.NS.REACTIONS);
4246
});
4347

4448
/**
@@ -50,7 +54,7 @@ converse.plugins.add('reactions', {
5054
if (query) {
5155
const from_jid = stanza.getAttribute('from');
5256
const bare_jid = Strophe.getBareJidFromJid(from_jid);
53-
const feature = query.querySelector(`feature[var="urn:xmpp:reactions:0#restricted"]`);
57+
const feature = query.querySelector(`feature[var="${Strophe.NS.REACTIONS}#restricted"]`);
5458
if (feature) {
5559
const allowed = Array.from(feature.querySelectorAll('allow')).map(el => el.textContent);
5660
this.allowed_emojis.set(bare_jid, allowed);
@@ -67,33 +71,14 @@ converse.plugins.add('reactions', {
6771
* @listens getMessageActionButtons
6872
*/
6973
api.listen.on('getMessageActionButtons', (el, buttons) => {
70-
const is_own_message = el.model.get('sender') === 'me';
71-
if (!is_own_message) {
72-
const chatbox = el.model.collection.chatbox;
73-
const jid = chatbox.get('jid');
74-
const type = chatbox.get('type');
75-
76-
// Check for support in 1:1 chats
77-
if (type === 'chat') {
78-
const entity = _converse.api.disco.entities.get(jid);
79-
// If we have disco info, check for the feature
80-
if (entity && entity.features && entity.features.length > 0) {
81-
const supportsReactions = entity.features.findWhere({'var': 'urn:xmpp:reactions:0'});
82-
if (!supportsReactions) {
83-
return buttons;
84-
}
85-
}
86-
// If unknown, we default to showing it (or we could trigger a disco check here)
87-
}
88-
8974
buttons.push({
9075
'i18n_text': __('Add Reaction'),
9176
'handler': (ev) => this.onReactionButtonClicked(el, ev),
9277
'button_class': 'chat-msg__action-reaction',
9378
'icon_class': 'fas fa-smile',
9479
'name': 'reaction',
9580
});
96-
}
81+
9782
return buttons;
9883
});
9984

@@ -110,7 +95,7 @@ converse.plugins.add('reactions', {
11095
*/
11196
const handler = (stanza) => {
11297
// Check for reactions element per XEP-0444
113-
const reactions = stanza.getElementsByTagNameNS('urn:xmpp:reactions:0', 'reactions');
98+
const reactions = stanza.getElementsByTagNameNS(Strophe.NS.REACTIONS, 'reactions');
11499

115100
if (reactions.length > 0) {
116101
this.onReactionReceived(stanza, reactions[0]);
@@ -135,87 +120,32 @@ converse.plugins.add('reactions', {
135120
}
136121
},
137122

138-
/**
139-
* Helper function to update a message with a new reaction
140-
* @param {Object} message - The message model to update
141-
* @param {string} from_jid - The JID of the user reacting
142-
* @param {Array<string>} emojis - The list of emojis (can be empty for removal)
143-
*/
144-
updateMessageReactions (message, from_jid, emojis) {
145-
// IMPORTANT: Clone the reactions object to ensure Backbone detects the change
146-
const current_reactions = message.get('reactions') || {};
147-
const reactions = JSON.parse(JSON.stringify(current_reactions));
148-
149-
// Remove user's previous reactions (clear slate for this user)
150-
for (const existingEmoji in reactions) {
151-
const index = reactions[existingEmoji].indexOf(from_jid);
152-
if (index !== -1) {
153-
reactions[existingEmoji].splice(index, 1);
154-
// Remove emoji key if no one else reacted with it
155-
if (reactions[existingEmoji].length === 0) {
156-
delete reactions[existingEmoji];
157-
}
158-
}
159-
}
160-
161-
// Add the new reactions
162-
emojis.forEach(emoji => {
163-
if (!reactions[emoji]) {
164-
reactions[emoji] = [];
165-
}
166-
if (!reactions[emoji].includes(from_jid)) {
167-
reactions[emoji].push(from_jid);
168-
}
169-
});
170-
171-
message.save({ 'reactions': reactions });
172-
},
173-
174123
/**
175124
* Process a received reaction stanza
176125
* Updates the target message's reactions data structure
177126
*
178127
* @param {Element} stanza - The XMPP message stanza containing the reaction
179-
* @param {Element} reactionsElement - The <reactions> element from the stanza
128+
* @param {Element} reactions_element - The <reactions> element from the stanza
180129
*/
181-
async onReactionReceived (stanza, reactionsElement) {
130+
async onReactionReceived (stanza, reactions_element) {
182131
const from_jid = stanza.getAttribute('from');
183-
const id = reactionsElement.getAttribute('id'); // Target message ID
132+
const id = reactions_element.getAttribute('id'); // Target message ID
184133

185134
// Extract emojis from <reaction> child elements
186-
const reactionElements = reactionsElement.getElementsByTagName('reaction');
187-
const emojis = Array.from(reactionElements).map(el => el.textContent).filter(e => e);
188-
135+
const reaction_elements = reactions_element.getElementsByTagName('reaction');
136+
const emojis = Array.from(reaction_elements).map(el => el.textContent).filter(e => e);
137+
189138
if (!id) return;
190139

191140
// Strategy 1: Try to find chatbox by sender's bare JID
192141
const { Strophe } = converse.env;
193142
const bare_jid = Strophe.getBareJidFromJid(from_jid);
194143
let chatbox = api.chatboxes.get(bare_jid);
195144

196-
/**
197-
* Helper to find message by ID in a chatbox
198-
* @param {Object} box - The chatbox to search in
199-
* @param {string} msgId - The message ID to find
200-
* @returns {Object|null} - The message model or null
201-
*/
202-
const findMessage = (box, msgId) => {
203-
if (!box || !box.messages) {
204-
return null;
205-
}
206-
// Try direct lookup first
207-
let msg = box.messages.get(msgId);
208-
if (!msg) {
209-
// Fallback to findWhere for older messages
210-
msg = box.messages.findWhere({ 'msgid': msgId });
211-
}
212-
return msg;
213-
};
214-
215145
if (chatbox) {
216146
const message = findMessage(chatbox, id);
217147
if (message) {
218-
this.updateMessageReactions(message, from_jid, emojis);
148+
updateMessageReactions(message, from_jid, emojis);
219149
return;
220150
}
221151
}
@@ -226,7 +156,7 @@ converse.plugins.add('reactions', {
226156
for (const cb of allChatboxes) {
227157
const message = findMessage(cb, id);
228158
if (message) {
229-
this.updateMessageReactions(message, from_jid, emojis);
159+
updateMessageReactions(message, from_jid, emojis);
230160
return;
231161
}
232162
}
@@ -248,9 +178,9 @@ converse.plugins.add('reactions', {
248178

249179
// Toggle: if clicking same button, close picker instead of reopening
250180
if (existing_picker) {
251-
const isSameTarget = /** @type {any} */(existing_picker).target === target;
181+
const is_same_target = /** @type {any} */(existing_picker).target === target;
252182
existing_picker.remove();
253-
if (isSameTarget) {
183+
if (is_same_target) {
254184
return;
255185
}
256186
}
@@ -293,8 +223,8 @@ converse.plugins.add('reactions', {
293223
document.removeEventListener('click', onClickOutside);
294224
return;
295225
}
296-
const clickTarget = /** @type {Node} */(ev.target);
297-
if (!picker.contains(clickTarget) && !target.contains(clickTarget)) {
226+
const click_target = /** @type {Node} */(ev.target);
227+
if (!picker.contains(click_target) && !target.contains(click_target)) {
298228
picker.remove();
299229
document.removeEventListener('click', onClickOutside);
300230
}
@@ -322,7 +252,7 @@ converse.plugins.add('reactions', {
322252
* @param {string} emoji - The emoji reaction (can be unicode or shortname like :joy:)
323253
*/
324254
sendReaction (message, emoji) {
325-
const { $msg } = converse.env;
255+
const { stx, Stanza } = converse.env;
326256
const chatbox = message.collection.chatbox;
327257
const msgId = message.get('msgid');
328258
const to_jid = chatbox.get('jid');
@@ -333,21 +263,21 @@ converse.plugins.add('reactions', {
333263

334264
// Convert emoji shortname (e.g. :joy:) to unicode (e.g. 😂)
335265
// Check if emoji is already unicode (from emoji picker) or needs conversion (from shortname buttons)
336-
let emojiUnicode = emoji;
266+
let emoji_unicode = emoji;
337267
if (emoji.startsWith(':') && emoji.endsWith(':')) {
338-
const emojiArray = u.shortnamesToEmojis(emoji, { unicode_only: true });
339-
emojiUnicode = Array.isArray(emojiArray) ? emojiArray.join('') : emojiArray;
268+
const emoji_array = u.shortnamesToEmojis(emoji, { unicode_only: true });
269+
emoji_unicode = Array.isArray(emoji_array) ? emoji_array.join('') : emoji_array;
340270
}
341271

342272
// Filter out custom emojis (stickers) which don't have a unicode representation
343-
if (emojiUnicode.startsWith(':') && emojiUnicode.endsWith(':')) {
273+
if (emoji_unicode.startsWith(':') && emoji_unicode.endsWith(':')) {
344274
return;
345275
}
346276

347277
const my_jid = api.connection.get().jid;
348-
const currentReactions = message.get('reactions') || {};
278+
const current_reactions = message.get('reactions') || {};
349279
// Clone to ensure Backbone detects the change
350-
const reactions = JSON.parse(JSON.stringify(currentReactions));
280+
const reactions = JSON.parse(JSON.stringify(current_reactions));
351281

352282
// Determine current user's reactions
353283
const myReactions = new Set();
@@ -358,33 +288,29 @@ converse.plugins.add('reactions', {
358288
}
359289

360290
// Toggle the clicked emoji
361-
if (myReactions.has(emojiUnicode)) {
362-
myReactions.delete(emojiUnicode);
291+
if (myReactions.has(emoji_unicode)) {
292+
myReactions.delete(emoji_unicode);
363293
} else {
364-
myReactions.add(emojiUnicode);
294+
myReactions.add(emoji_unicode);
365295
}
366296

367297
// Build XEP-0444 reaction stanza with ALL current reactions
368-
const reactionStanza = $msg({
369-
'to': to_jid,
370-
'type': type,
371-
'id': u.getUniqueId('reaction')
372-
}).c('reactions', {
373-
'xmlns': 'urn:xmpp:reactions:0',
374-
'id': msgId // ID of the message being reacted to
375-
});
376-
377-
myReactions.forEach(r => {
378-
reactionStanza.c('reaction').t(r).up();
379-
});
298+
const reactions_xml = Array.from(myReactions).map(r => `<reaction>${r}</reaction>`).join('');
299+
const reaction_stanza = stx`
300+
<message to="${to_jid}" type="${type}" id="${u.getUniqueId('reaction')}" xmlns="jabber:client">
301+
<reactions xmlns="${Strophe.NS.REACTIONS}" id="${msgId}">
302+
${Stanza.unsafeXML(reactions_xml)}
303+
</reactions>
304+
</message>
305+
`;
380306

381307
// Send stanza to XMPP server
382-
api.send(reactionStanza);
308+
api.send(reaction_stanza);
383309

384310
// Optimistic local update for immediate UI feedback
385311
// Only for 1:1 chats where no server reflection occurs for the sender
386312
if (type === 'chat') {
387-
this.updateMessageReactions(message, my_jid, Array.from(myReactions));
313+
updateMessageReactions(message, my_jid, Array.from(myReactions));
388314
}
389315
}
390316
});

src/plugins/reactions/reaction-picker.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,6 @@ export default class ReactionPicker extends CustomElement {
8080
*/
8181
render () {
8282
const anchor_name = `--reaction-anchor-${this.picker_id}`;
83-
const is_own_message = this.model?.get('sender') === 'me';
84-
85-
// Don't show reaction picker on own messages
86-
if (is_own_message) {
87-
return '';
88-
}
89-
9083
const popular_emojis = this.allowed_emojis ?
9184
POPULAR_EMOJIS.filter(sn => this.allowed_emojis.includes(u.shortnamesToEmojis(sn))) :
9285
POPULAR_EMOJIS;
@@ -101,7 +94,7 @@ export default class ReactionPicker extends CustomElement {
10194
`)}
10295
10396
<!-- Full emoji picker dropdown -->
104-
<div class="dropdown">
97+
<div class="dropdown emoji-picker__dropdown">
10598
<button class="reaction-item more dropdown-toggle"
10699
type="button"
107100
id="${this.picker_id}-dropdown"
@@ -119,7 +112,10 @@ export default class ReactionPicker extends CustomElement {
119112
.state=${this.emoji_picker_state}
120113
.model=${this.model.collection.chatbox}
121114
.allowed_emojis=${this.allowed_emojis}
122-
@emojiSelected=${(ev) => this.onEmojiSelected(ev.detail.value)}
115+
@emojiSelected=${(ev) => {
116+
ev.stopPropagation();
117+
this.onEmojiSelected(ev.detail.value);
118+
}}
123119
?render_emojis=${true}
124120
current_category="${this.emoji_picker_state.get('current_category') || ''}"
125121
current_skintone="${this.emoji_picker_state.get('current_skintone') || ''}"

src/plugins/reactions/tests/reactions.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe("Message Reactions (XEP-0444)", function () {
5151
})
5252
);
5353

54-
it("does not appear for own messages",
54+
it("appears for own messages",
5555
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
5656
const { api } = _converse;
5757
await mock.waitForRoster(_converse, 'current', 1);
@@ -72,9 +72,9 @@ describe("Message Reactions (XEP-0444)", function () {
7272
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
7373
const msg_el = view.querySelector('.chat-msg');
7474

75-
// Reaction button should not appear for own messages
75+
// Reaction button should appear for own messages
7676
const reaction_btn = msg_el.querySelector('.chat-msg__action-reaction');
77-
expect(reaction_btn).toBe(null);
77+
expect(reaction_btn).not.toBe(null);
7878
})
7979
);
8080

0 commit comments

Comments
 (0)