1515 */
1616
1717import { converse , api , u , _converse } from '@converse/headless' ;
18+ import { updateMessageReactions , findMessage } from './utils.js' ;
1819import './reaction-picker.js' ;
1920
2021import { __ } 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} ) ;
0 commit comments