@nodebb/nodebb-plugin-reactions 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,9 +4,7 @@ Reactions plugin for NodeBB
4
4
  # Screenshots
5
5
 
6
6
  ## Reactions:
7
- ![CleanShot 2023-05-11 at 08 53 48@2x](https://github.com/ShlomoCode/nodebb-plugin-reactions/assets/78599753/613cb49e-994a-4869-a721-c1eeb00959ff)
8
-
7
+ ![demo](./assets/demo.png)
9
8
 
10
9
  ## ACP:
11
- ![CleanShot 2023-05-11 at 04 09 11@2x](https://github.com/ShlomoCode/nodebb-plugin-reactions/assets/78599753/f4a4e5cb-f060-4415-a337-222a157a6e59)
12
-
10
+ ![acp](./assets/acp.png)
package/assets/acp.png ADDED
Binary file
Binary file
@@ -8,6 +8,9 @@
8
8
  "settings.max-reactions-per-post": "Maximum unique reactions per post (0 for unlimited, default: 4)",
9
9
  "settings.max-reactions-per-user-per-post": "Maximum reactions per user per post (0 for unlimited)",
10
10
  "settings.max-reactions-per-user-per-post-help": "The limit is enforced only when adding a new reaction and not when joining an existing reaction.",
11
+ "settings.max-reactions-per-message": "Maximum unique reactions per message (0 for unlimited, default: 4)",
12
+ "settings.max-reactions-per-user-per-message": "Maximum reactions per user per message (0 for unlimited)",
13
+ "settings.max-reactions-per-user-per-message-help": "The limit is enforced only when adding a new reaction and not when joining an existing reaction.",
11
14
  "settings.reaction-reputations": "Reaction Reputations (Optional)",
12
15
  "settings.reaction-reputations-help": "You can assign a reputation to individual reactions. When a reaction is applied to a post, the owner of that post will get this reputation.",
13
16
  "settings.reaction-reputations.add": "Add Rule",
package/library.js CHANGED
@@ -4,6 +4,7 @@ const _ = require.main.require('lodash');
4
4
  const meta = require.main.require('./src/meta');
5
5
  const user = require.main.require('./src/user');
6
6
  const posts = require.main.require('./src/posts');
7
+ const messaging = require.main.require('./src/messaging');
7
8
  const db = require.main.require('./src/database');
8
9
  const routesHelpers = require.main.require('./src/routes/helpers');
9
10
  const websockets = require.main.require('./src/socket.io/index');
@@ -19,13 +20,14 @@ function parse(name) {
19
20
  return emojiParser.buildEmoji(emojiTable[name] || emojiTable[emojiAliases[name]], '');
20
21
  }
21
22
 
22
- const ReactionsPlugin = {};
23
+ const ReactionsPlugin = module.exports;
23
24
 
24
25
  ReactionsPlugin.init = async function (params) {
25
- function renderAdmin(_, res) {
26
- res.render('admin/plugins/reactions', {});
27
- }
28
- routesHelpers.setupAdminPageRoute(params.router, '/admin/plugins/reactions', params.middleware, [], renderAdmin);
26
+ routesHelpers.setupAdminPageRoute(params.router, '/admin/plugins/reactions', (req, res) => {
27
+ res.render('admin/plugins/reactions', {
28
+ title: '[[reactions:reactions]]',
29
+ });
30
+ });
29
31
  };
30
32
 
31
33
  ReactionsPlugin.addAdminNavigation = async function (header) {
@@ -41,13 +43,15 @@ ReactionsPlugin.getPluginConfig = async function (config) {
41
43
  try {
42
44
  const settings = await meta.settings.get('reactions');
43
45
  config.maximumReactions = settings.maximumReactions ? parseInt(settings.maximumReactions, 10) : DEFAULT_MAX_EMOTES;
46
+ config.maximumReactionsPerMessage = settings.maximumReactionsPerMessage ?
47
+ parseInt(settings.maximumReactionsPerMessage, 10) : DEFAULT_MAX_EMOTES;
44
48
  } catch (e) {
45
49
  console.error(e);
46
50
  }
47
51
  return config;
48
52
  };
49
53
 
50
- ReactionsPlugin.getReactions = async function (data) {
54
+ ReactionsPlugin.getPostReactions = async function (data) {
51
55
  if (data.uid === 0) {
52
56
  return data;
53
57
  }
@@ -126,6 +130,81 @@ ReactionsPlugin.getReactions = async function (data) {
126
130
  return data;
127
131
  };
128
132
 
133
+ ReactionsPlugin.getMessageReactions = async function (data) {
134
+ if (data.uid === 0) {
135
+ return data;
136
+ }
137
+
138
+ try {
139
+ const settings = await meta.settings.get('reactions');
140
+ const maximumReactionsPerMessage = settings.maximumReactionsPerMessage || DEFAULT_MAX_EMOTES;
141
+
142
+ const mids = data.messages.map(message => message && parseInt(message.mid, 10));
143
+ const allReactionsForMids = await db.getSetsMembers(mids.map(pid => `mid:${pid}:reactions`));
144
+
145
+ const midToIsMaxReactionsReachedMap = new Map(); // mid -> IsMaxReactionsReached (boolean)
146
+ const midToReactionsMap = new Map(); // mid -> reactions (string[])
147
+ let reactionSets = [];
148
+
149
+ for (let i = 0, len = mids.length; i < len; i++) {
150
+ const mid = mids[i];
151
+ const reactionsList = allReactionsForMids[i];
152
+ const reactionsCount = reactionsList.length;
153
+
154
+ if (reactionsList && reactionsList.length > 0) {
155
+ midToReactionsMap.set(mid, reactionsList);
156
+ midToIsMaxReactionsReachedMap.set(mid, reactionsCount > maximumReactionsPerMessage);
157
+ reactionSets = reactionSets.concat(reactionsList.map(reaction => `mid:${mid}:reaction:${reaction}`));
158
+ }
159
+ }
160
+
161
+ const reactionSetToUsersMap = new Map(); // reactionSet -> { uid, username }
162
+ if (reactionSets.length > 0) {
163
+ const uidsForReactions = await db.getSetsMembers(reactionSets);
164
+ const allUids = _.union(...uidsForReactions).filter(Boolean);
165
+ const usersData = await user.getUsersFields(allUids, ['uid', 'username']);
166
+ const uidToUserdataMap = _.keyBy(usersData, 'uid');
167
+
168
+ for (let i = 0, len = reactionSets.length; i < len; i++) {
169
+ const uidsForReaction = uidsForReactions[i];
170
+ if (uidsForReaction && uidsForReaction.length > 0) {
171
+ const usersData = uidsForReaction.map(uid => uidToUserdataMap[uid]).filter(Boolean);
172
+ reactionSetToUsersMap.set(reactionSets[i], usersData);
173
+ }
174
+ }
175
+ }
176
+
177
+ for (const msg of data.messages) {
178
+ msg.maxReactionsReached = midToIsMaxReactionsReachedMap.get(msg.mid);
179
+ msg.reactions = [];
180
+
181
+ if (midToReactionsMap.has(msg.mid)) {
182
+ for (const reaction of midToReactionsMap.get(msg.mid)) {
183
+ const reactionSet = `mid:${msg.mid}:reaction:${reaction}`;
184
+ if (reactionSetToUsersMap.has(reactionSet)) {
185
+ const usersData = reactionSetToUsersMap.get(reactionSet);
186
+ const reactionCount = usersData.length;
187
+ const reactedUsernames = usersData.map(userData => userData.username).join(', ');
188
+ const reactedUids = usersData.map(userData => userData.uid);
189
+
190
+ msg.reactions.push({
191
+ mid: msg.mid,
192
+ reacted: reactedUids.includes(data.uid),
193
+ reaction,
194
+ usernames: reactedUsernames,
195
+ reactionImage: parse(reaction),
196
+ reactionCount,
197
+ });
198
+ }
199
+ }
200
+ }
201
+ }
202
+ } catch (e) {
203
+ console.error(e);
204
+ }
205
+ return data;
206
+ };
207
+
129
208
  ReactionsPlugin.onReply = async function (data) {
130
209
  if (data.uid !== 0) {
131
210
  data.reactions = [];
@@ -148,7 +227,7 @@ ReactionsPlugin.deleteReactions = async function (hookData) {
148
227
  await db.deleteAll(keys);
149
228
  };
150
229
 
151
- async function sendEvent(data, eventName) {
230
+ async function sendPostEvent(data, eventName) {
152
231
  try {
153
232
  const [reactionCount, totalReactions, uids] = await Promise.all([
154
233
  db.setCount(`pid:${data.pid}:reaction:${data.reaction}`),
@@ -177,6 +256,35 @@ async function sendEvent(data, eventName) {
177
256
  }
178
257
  }
179
258
 
259
+ async function sendMessageEvent(data, eventName) {
260
+ try {
261
+ const [reactionCount, totalReactions, uids] = await Promise.all([
262
+ db.setCount(`mid:${data.mid}:reaction:${data.reaction}`),
263
+ db.setCount(`mid:${data.mid}:reactions`),
264
+ db.getSetMembers(`mid:${data.mid}:reaction:${data.reaction}`),
265
+ ]);
266
+
267
+ const userdata = await user.getUsersFields(uids, ['uid', 'username']);
268
+ const usernames = userdata.map(user => user.username).join(', ');
269
+
270
+ if (parseInt(reactionCount, 10) === 0) {
271
+ await db.setRemove(`mid:${data.mid}:reactions`, data.reaction);
272
+ }
273
+
274
+ await websockets.in(`chat_room_${data.roomId}`).emit(eventName, {
275
+ mid: data.mid,
276
+ uid: data.uid,
277
+ reaction: data.reaction,
278
+ reactionCount,
279
+ totalReactions,
280
+ usernames,
281
+ reactionImage: parse(data.reaction),
282
+ });
283
+ } catch (e) {
284
+ console.error(e);
285
+ }
286
+ }
287
+
180
288
  async function getReactionReputation(reaction) {
181
289
  const settings = await meta.settings.get('reactions');
182
290
  const reactionsReps = settings['reaction-reputations'] || [];
@@ -201,23 +309,27 @@ SocketPlugins.reactions = {
201
309
  throw new Error('[[reactions:error.invalid-reaction]]');
202
310
  }
203
311
 
204
- data.uid = socket.uid;
205
-
206
312
  const settings = await meta.settings.get('reactions');
207
313
  const maximumReactions = settings.maximumReactions || DEFAULT_MAX_EMOTES;
208
- const [totalReactions, emojiIsAlreadyExist, alreadyReacted, reactionReputation] = await Promise.all([
314
+ const [tid, totalReactions, emojiIsAlreadyExist, alreadyReacted, reactionReputation] = await Promise.all([
315
+ posts.getPostField(data.pid, 'tid'),
209
316
  db.setCount(`pid:${data.pid}:reactions`),
210
317
  db.isSetMember(`pid:${data.pid}:reactions`, data.reaction),
211
318
  db.isSetMember(`pid:${data.pid}:reaction:${data.reaction}`, socket.uid),
212
319
  getReactionReputation(data.reaction),
213
320
  ]);
214
-
321
+ if (!tid) {
322
+ throw new Error('[[error:no-post]]');
323
+ }
324
+ data.uid = socket.uid;
325
+ data.tid = tid;
215
326
  if (!emojiIsAlreadyExist) {
216
327
  if (totalReactions > maximumReactions) {
217
328
  throw new Error(`[[reactions:error.maximum-reached]] (${maximumReactions})`);
218
329
  }
219
-
220
- const maximumReactionsPerUserPerPost = settings.maximumReactionsPerUserPerPost ? parseInt(settings.maximumReactionsPerUserPerPost, 10) : 0;
330
+
331
+ const maximumReactionsPerUserPerPost = settings.maximumReactionsPerUserPerPost ?
332
+ parseInt(settings.maximumReactionsPerUserPerPost, 10) : 0;
221
333
  if (maximumReactionsPerUserPerPost > 0) {
222
334
  const emojiesInPost = await db.getSetMembers(`pid:${data.pid}:reactions`);
223
335
  const userPostReactions = await db.isMemberOfSets(emojiesInPost.map(emojiName => `pid:${data.pid}:reaction:${emojiName}`), socket.uid);
@@ -227,7 +339,6 @@ SocketPlugins.reactions = {
227
339
  }
228
340
  }
229
341
  }
230
-
231
342
 
232
343
  await Promise.all([
233
344
  db.setAdd(`pid:${data.pid}:reactions`, data.reaction),
@@ -238,7 +349,7 @@ SocketPlugins.reactions = {
238
349
  await giveOwnerReactionReputation(reactionReputation, data.pid);
239
350
  }
240
351
 
241
- await sendEvent(data, 'event:reactions.addPostReaction');
352
+ await sendPostEvent(data, 'event:reactions.addPostReaction');
242
353
  },
243
354
  removePostReaction: async function (socket, data) {
244
355
  if (!socket.uid) {
@@ -249,30 +360,107 @@ SocketPlugins.reactions = {
249
360
  throw new Error('[[reactions:error.invalid-reaction]]');
250
361
  }
251
362
 
363
+ const [tid, hasReacted, reactionReputation] = await Promise.all([
364
+ posts.getPostField(data.pid, 'tid'),
365
+ db.isSetMember(`pid:${data.pid}:reaction:${data.reaction}`, socket.uid),
366
+ getReactionReputation(data.reaction),
367
+ ]);
368
+ if (!tid) {
369
+ throw new Error('[[error:no-post]]');
370
+ }
252
371
  data.uid = socket.uid;
372
+ data.tid = tid;
253
373
 
254
- try {
255
- const [hasReacted, reactionReputation] = await Promise.all([
256
- db.isSetMember(`pid:${data.pid}:reaction:${data.reaction}`, socket.uid),
257
- getReactionReputation(data.reaction),
258
- ]);
259
- if (hasReacted) {
260
- await db.setRemove(`pid:${data.pid}:reaction:${data.reaction}`, socket.uid);
261
- }
374
+ if (hasReacted) {
375
+ await db.setRemove(`pid:${data.pid}:reaction:${data.reaction}`, socket.uid);
376
+ }
377
+
378
+ const reactionCount = await db.setCount(`pid:${data.pid}:reaction:${data.reaction}`);
379
+ if (reactionCount === 0) {
380
+ await db.setRemove(`pid:${data.pid}:reactions`, data.reaction);
381
+ }
382
+ if (hasReacted && reactionReputation > 0) {
383
+ await giveOwnerReactionReputation(-reactionReputation, data.pid);
384
+ }
262
385
 
263
- const reactionCount = await db.setCount(`pid:${data.pid}:reaction:${data.reaction}`);
264
- if (reactionCount === 0) {
265
- await db.setRemove(`pid:${data.pid}:reactions`, data.reaction);
386
+ await sendPostEvent(data, 'event:reactions.removePostReaction');
387
+ },
388
+ addMessageReaction: async function (socket, data) {
389
+ if (!socket.uid) {
390
+ throw new Error('[[error:not-logged-in]]');
391
+ }
392
+
393
+ if (!emojiTable[data.reaction]) {
394
+ throw new Error('[[reactions:error.invalid-reaction]]');
395
+ }
396
+
397
+ const settings = await meta.settings.get('reactions');
398
+ const maximumReactionsPerMessage = settings.maximumReactionsPerMessage || DEFAULT_MAX_EMOTES;
399
+ const [roomId, totalReactions, emojiIsAlreadyExist] = await Promise.all([
400
+ messaging.getMessageField(data.mid, 'roomId'),
401
+ db.setCount(`mid:${data.mid}:reactions`),
402
+ db.isSetMember(`mid:${data.mid}:reactions`, data.reaction),
403
+ ]);
404
+
405
+ if (!roomId) {
406
+ throw new Error('[[error:no-message]]');
407
+ }
408
+
409
+ data.uid = socket.uid;
410
+ data.roomId = roomId;
411
+
412
+ if (!emojiIsAlreadyExist) {
413
+ if (totalReactions > maximumReactionsPerMessage) {
414
+ throw new Error(`[[reactions:error.maximum-reached]] (${maximumReactionsPerMessage})`);
266
415
  }
267
- if (hasReacted && reactionReputation > 0) {
268
- await giveOwnerReactionReputation(-reactionReputation, data.pid);
416
+
417
+ const maximumReactionsPerUserPerMessage = settings.maximumReactionsPerUserPerMessage ?
418
+ parseInt(settings.maximumReactionsPerUserPerMessage, 10) : 0;
419
+ if (maximumReactionsPerUserPerMessage > 0) {
420
+ const emojiesInMessage = await db.getSetMembers(`mid:${data.mid}:reactions`);
421
+ const userPostReactions = await db.isMemberOfSets(emojiesInMessage.map(emojiName => `mid:${data.mid}:reaction:${emojiName}`), socket.uid);
422
+ const userPostReactionCount = userPostReactions.filter(Boolean).length;
423
+ if (userPostReactionCount > maximumReactionsPerUserPerMessage) {
424
+ throw new Error(`[[reactions:error.maximum-per-user-per-post-reached]] (${maximumReactionsPerUserPerMessage})`);
425
+ }
269
426
  }
270
- await sendEvent(data, 'event:reactions.removePostReaction');
271
- } catch (e) {
272
- console.error(e);
273
427
  }
428
+
429
+ await Promise.all([
430
+ db.setAdd(`mid:${data.mid}:reactions`, data.reaction),
431
+ db.setAdd(`mid:${data.mid}:reaction:${data.reaction}`, socket.uid),
432
+ ]);
433
+
434
+ await sendMessageEvent(data, 'event:reactions.addMessageReaction');
274
435
  },
275
- };
436
+ removeMessageReaction: async function (socket, data) {
437
+ if (!socket.uid) {
438
+ throw new Error('[[error:not-logged-in]]');
439
+ }
440
+
441
+ if (!emojiTable[data.reaction]) {
442
+ throw new Error('[[reactions:error.invalid-reaction]]');
443
+ }
444
+
445
+ const [roomId, hasReacted] = await Promise.all([
446
+ messaging.getMessageField(data.mid, 'roomId'),
447
+ db.isSetMember(`mid:${data.mid}:reaction:${data.reaction}`, socket.uid),
448
+ ]);
449
+ if (!roomId) {
450
+ throw new Error('[[error:no-message]]');
451
+ }
452
+ data.uid = socket.uid;
453
+ data.roomId = roomId;
454
+ if (hasReacted) {
455
+ await db.setRemove(`mid:${data.mid}:reaction:${data.reaction}`, socket.uid);
456
+ }
276
457
 
458
+ const reactionCount = await db.setCount(`mid:${data.mid}:reaction:${data.reaction}`);
459
+ if (reactionCount === 0) {
460
+ await db.setRemove(`mid:${data.mid}:reactions`, data.reaction);
461
+ }
462
+
463
+ await sendMessageEvent(data, 'event:reactions.removeMessageReaction');
464
+ },
465
+ };
277
466
 
278
- module.exports = ReactionsPlugin;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@nodebb/nodebb-plugin-reactions",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "nbbpm": {
5
- "compatibility": "^3.0.0"
5
+ "compatibility": "^3.3.0"
6
6
  },
7
7
  "description": "Reactions plugin for NodeBB",
8
8
  "main": "library.js",
package/plugin.json CHANGED
@@ -26,7 +26,10 @@
26
26
  "hook": "filter:config.get", "method": "getPluginConfig"
27
27
  },
28
28
  {
29
- "hook": "filter:post.getPosts", "method": "getReactions"
29
+ "hook": "filter:post.getPosts", "method": "getPostReactions"
30
+ },
31
+ {
32
+ "hook": "filter:messaging.getMessages", "method": "getMessageReactions"
30
33
  },
31
34
  {
32
35
  "hook": "filter:post.get", "method": "onReply"
package/public/client.js CHANGED
@@ -1,55 +1,17 @@
1
1
  'use strict';
2
2
 
3
3
  $(document).ready(function () {
4
- setupPostReactions();
5
-
6
- function setupPostReactions() {
7
- require(['hooks', 'alerts'], function (hooks, alerts) {
4
+ setupReactions();
5
+ let alerts;
6
+ function setupReactions() {
7
+ createReactionTooltips();
8
+ require(['hooks', 'alerts'], function (hooks, _alerts) {
9
+ alerts = _alerts;
8
10
  hooks.on('action:ajaxify.end', function () {
9
11
  if (ajaxify.data.template.topic) {
10
- setupReactionSockets();
11
- createReactionTooltips();
12
- $('[component="topic"]').on('click', '[component="post/reaction"]', function () {
13
- var tid = $('[component="topic"]').attr('data-tid');
14
- var reactionElement = $(this);
15
- var pid = reactionElement.attr('data-pid');
16
- var reaction = reactionElement.attr('data-reaction');
17
- var reacted = reactionElement.hasClass('reacted');
18
- var event = 'plugins.reactions.' + (reacted ? 'removePostReaction' : 'addPostReaction');
19
- socket.emit(event, {
20
- tid: tid,
21
- pid: pid,
22
- reaction: reaction,
23
- }, function (err) {
24
- if (err) {
25
- alerts.error(err.message);
26
- }
27
- });
28
- });
29
-
30
- $('[component="topic"]').on('click', '[component="post/reaction/add"]', function () {
31
- var reactionAddEl = $(this);
32
- var tid = $('[component="topic"]').attr('data-tid');
33
- var pid = reactionAddEl.attr('data-pid');
34
- require(['emoji-dialog'], function (emojiDialog) {
35
- emojiDialog.toggle(reactionAddEl[0], function (_, name, dialog) {
36
- emojiDialog.dialogActions.close(dialog);
37
-
38
- socket.emit('plugins.reactions.addPostReaction', {
39
- tid: tid,
40
- pid: pid,
41
- reaction: name,
42
- }, function (err) {
43
- if (err) {
44
- alerts.error(err.message);
45
- throw err;
46
- }
47
-
48
- $('[component="post/reaction"][data-pid="' + pid + '"][data-reaction="' + name + '"]').addClass('reacted');
49
- });
50
- });
51
- });
52
- });
12
+ setupPostReactions();
13
+ } else if (ajaxify.data.template.chats && ajaxify.data.roomId) {
14
+ setupMessageReactions();
53
15
  }
54
16
  });
55
17
  });
@@ -61,19 +23,122 @@ $(document).ready(function () {
61
23
  socket.on('event:post_restored', function (data) {
62
24
  $('[component="post/reactions"][data-pid="' + data.pid + '"]').removeClass('hidden');
63
25
  });
26
+
27
+ socket.on('event:chats.delete', function (mid) {
28
+ $('[component="message/reactions"][data-mid="' + mid + '"]').addClass('hidden');
29
+ });
30
+
31
+ socket.on('event:chats.restore', function (msg) {
32
+ $('[component="message/reactions"][data-mid="' + msg.mid + '"]').removeClass('hidden');
33
+ });
34
+ }
35
+
36
+ function setupPostReactions() {
37
+ setupPostReactionSockets();
38
+
39
+ $('[component="topic"]').on('click', '[component="post/reaction"]', function () {
40
+ var reactionElement = $(this);
41
+ var pid = reactionElement.attr('data-pid');
42
+ var reaction = reactionElement.attr('data-reaction');
43
+ var reacted = reactionElement.hasClass('reacted');
44
+ var event = 'plugins.reactions.' + (reacted ? 'removePostReaction' : 'addPostReaction');
45
+ socket.emit(event, {
46
+ pid: pid,
47
+ reaction: reaction,
48
+ }, function (err) {
49
+ if (err) {
50
+ alerts.error(err.message);
51
+ }
52
+ });
53
+ });
54
+
55
+ $('[component="topic"]').on('click', '[component="post/reaction/add"]', function () {
56
+ var reactionAddEl = $(this);
57
+ var pid = reactionAddEl.attr('data-pid');
58
+ require(['emoji-dialog'], function (emojiDialog) {
59
+ emojiDialog.toggle(reactionAddEl[0], function (_, name, dialog) {
60
+ emojiDialog.dialogActions.close(dialog);
61
+
62
+ socket.emit('plugins.reactions.addPostReaction', {
63
+ pid: pid,
64
+ reaction: name,
65
+ }, function (err) {
66
+ if (err) {
67
+ alerts.error(err.message);
68
+ throw err;
69
+ }
70
+
71
+ $('[component="post/reaction"][data-pid="' + pid + '"][data-reaction="' + name + '"]').addClass('reacted');
72
+ });
73
+ });
74
+ });
75
+ });
76
+ }
77
+
78
+ function setupMessageReactions() {
79
+ setupMessageReactionSockets();
80
+
81
+ const messageContent = $('[component="chat/message/content"]');
82
+ messageContent.on('click', '[component="message/reaction"]', function () {
83
+ var reactionElement = $(this);
84
+ var mid = reactionElement.attr('data-mid');
85
+ var reaction = reactionElement.attr('data-reaction');
86
+ var reacted = reactionElement.hasClass('reacted');
87
+ var event = 'plugins.reactions.' + (reacted ? 'removeMessageReaction' : 'addMessageReaction');
88
+ socket.emit(event, {
89
+ mid: mid,
90
+ reaction: reaction,
91
+ }, function (err) {
92
+ if (err) {
93
+ alerts.error(err.message);
94
+ }
95
+ });
96
+ });
97
+
98
+ messageContent.on('click', '[component="message/reaction/add"]', function () {
99
+ const reactionAddEl = $(this);
100
+ const mid = reactionAddEl.attr('data-mid');
101
+
102
+ require(['emoji-dialog'], function (emojiDialog) {
103
+ emojiDialog.toggle(reactionAddEl[0], function (_, name, dialog) {
104
+ emojiDialog.dialogActions.close(dialog);
105
+
106
+ socket.emit('plugins.reactions.addMessageReaction', {
107
+ mid: mid,
108
+ reaction: name,
109
+ }, function (err) {
110
+ if (err) {
111
+ return alerts.error(err.message);
112
+ }
113
+
114
+ $('[component="message/reaction"][data-mid="' + mid + '"][data-reaction="' + name + '"]').addClass('reacted');
115
+ });
116
+ });
117
+ });
118
+ });
64
119
  }
65
120
 
66
- function setupReactionSockets() {
121
+ function setupPostReactionSockets() {
67
122
  socket.off('event:reactions.addPostReaction').on('event:reactions.addPostReaction', function (data) {
68
- updateReactionCount(data);
123
+ updatePostReactionCount(data, 'add');
69
124
  });
70
125
 
71
126
  socket.off('event:reactions.removePostReaction').on('event:reactions.removePostReaction', function (data) {
72
- updateReactionCount(data);
127
+ updatePostReactionCount(data, 'remove');
73
128
  });
74
129
  }
75
130
 
76
- function updateReactionCount(data) {
131
+ function setupMessageReactionSockets() {
132
+ socket.off('event:reactions.addMessageReaction').on('event:reactions.addMessageReaction', function (data) {
133
+ updateMessageReactionCount(data, 'add');
134
+ });
135
+
136
+ socket.off('event:reactions.removeMessageReaction').on('event:reactions.removeMessageReaction', function (data) {
137
+ updateMessageReactionCount(data, 'remove');
138
+ });
139
+ }
140
+
141
+ function updatePostReactionCount(data, type) {
77
142
  var maxReactionsReached = parseInt(data.totalReactions, 10) > config.maximumReactions;
78
143
  $('[component="post/reaction/add"][data-pid="' + data.pid + '"]').toggleClass('max-reactions', maxReactionsReached);
79
144
 
@@ -83,14 +148,14 @@ $(document).ready(function () {
83
148
  reactionEl.tooltip('dispose');
84
149
  reactionEl.remove();
85
150
  }
86
-
151
+ const isSelf = parseInt(data.uid, 10) === app.user.uid;
87
152
  if (reactionEl.length === 0) {
88
153
  app.parseAndTranslate('partials/topic/reaction', {
89
154
  pid: data.pid,
90
155
  reaction: data.reaction,
91
156
  reactionCount: data.reactionCount,
92
157
  usernames: data.usernames,
93
- reacted: (parseInt(data.uid, 10) === app.user.uid),
158
+ reacted: isSelf && type === 'add',
94
159
  reactionImage: data.reactionImage,
95
160
  }, function (html) {
96
161
  $('[component="post/reactions"][data-pid="' + data.pid + '"]').append(html);
@@ -99,20 +164,52 @@ $(document).ready(function () {
99
164
  reactionEl.find('.reaction-emoji-count').attr('data-count', data.reactionCount);
100
165
  reactionEl.attr('data-bs-original-title', data.usernames);
101
166
  reactionEl.attr('aria-label', data.usernames);
102
- reactionEl.toggleClass('reacted', !(parseInt(data.uid, 10) === app.user.uid));
167
+ if (isSelf) {
168
+ reactionEl.toggleClass('reacted', type === 'add');
169
+ }
103
170
  }
104
- createReactionTooltips();
105
171
  }
106
172
 
107
- function createReactionTooltips() {
108
- $('.reaction, .reaction-add').each(function () {
109
- if (!utils.isTouchDevice()) {
110
- $(this).tooltip('dispose');
111
- $(this).tooltip({
112
- placement: 'top',
113
- title: $(this).attr('title') || $(this).attr('data-bs-original-title'),
114
- });
173
+ function updateMessageReactionCount(data, type) {
174
+ var maxReactionsReached = parseInt(data.totalReactions, 10) > config.maximumReactionsPerMessage;
175
+ $('[component="message/reaction/add"][data-mid="' + data.mid + '"]').toggleClass('max-reactions', maxReactionsReached);
176
+
177
+ var reactionEl = $(`[component="message/reaction"][data-mid="${data.mid}"][data-reaction="${data.reaction}"]`);
178
+
179
+ if (parseInt(data.reactionCount, 10) === 0) {
180
+ reactionEl.tooltip('dispose');
181
+ reactionEl.remove();
182
+ }
183
+ const isSelf = (parseInt(data.uid, 10) === app.user.uid);
184
+ if (reactionEl.length === 0) {
185
+ app.parseAndTranslate('partials/chats/reaction', {
186
+ mid: data.mid,
187
+ reaction: data.reaction,
188
+ reactionCount: data.reactionCount,
189
+ usernames: data.usernames,
190
+ reacted: isSelf && type === 'add',
191
+ reactionImage: data.reactionImage,
192
+ }, function (html) {
193
+ $('[component="message/reactions"][data-mid="' + data.mid + '"]').append(html);
194
+ });
195
+ } else {
196
+ reactionEl.find('.reaction-emoji-count').attr('data-count', data.reactionCount);
197
+ reactionEl.attr('data-bs-original-title', data.usernames);
198
+ reactionEl.attr('aria-label', data.usernames);
199
+ if (isSelf) {
200
+ reactionEl.toggleClass('reacted', type === 'add');
115
201
  }
116
- });
202
+ }
203
+ }
204
+
205
+ function createReactionTooltips() {
206
+ if (!utils.isTouchDevice()) {
207
+ $('#content').tooltip({
208
+ selector: '.reaction',
209
+ placement: 'top',
210
+ container: '#content',
211
+ animation: false,
212
+ });
213
+ }
117
214
  }
118
215
  });
@@ -1,6 +1,6 @@
1
1
  .reactions {
2
2
  .reaction {
3
- border: 1px solid #9ddefe;
3
+ border: 1px solid var(--bs-primary-bg-subtle);
4
4
  display: inline-block;
5
5
  border-radius: 4px;
6
6
  padding: 2px 4px;
@@ -10,7 +10,7 @@
10
10
  height: 18px;
11
11
  }
12
12
  &.reacted {
13
- border: 1px solid #039BE5;
13
+ border: 1px solid var(--bs-primary);
14
14
  }
15
15
  .reaction-emoji-count {
16
16
  margin-left: 5px;
@@ -1,33 +1,52 @@
1
- <form role="form" class="reactions-settings">
2
- <div class="row">
3
- <div class="col-sm-2 col-xs-12 settings-header">[[reactions:settings.title]]</div>
4
- <div class="col-sm-10 col-xs-12">
5
- <div class="form-group">
6
- <label>[[reactions:settings.max-reactions-per-post]]</label>
7
- <input type="number" min="0" class="form-control" id="maximumReactions" name="maximumReactions">
8
1
 
9
- <label>[[reactions:settings.max-reactions-per-user-per-post]]</label>
10
- <input type="number" min="0" class="form-control" id="maximumReactionsPerUserPerPost" name="maximumReactionsPerUserPerPost">
11
- <p class="help-text">
12
- [[reactions:settings.max-reactions-per-user-per-post-help]]
13
- </p>
14
- </div>
15
- </div>
16
- </div>
2
+ <div class="acp-page-container">
3
+ <!-- IMPORT admin/partials/settings/header.tpl -->
17
4
 
18
- <div class="row mt-3">
19
- <div class="col-sm-2 col-xs-12 settings-header">[[reactions:settings.reaction-reputations]]</div>
20
- <div class="col-sm-10 col-xs-12">
21
- <p class="help-text">
22
- [[reactions:settings.reaction-reputations-help]]
23
- </p>
24
- <div class="form-group" data-type="sorted-list" data-sorted-list="reaction-reputations" data-item-template="admin/plugins/reactions/partials/sorted-list/emoji-item" data-form-template="admin/plugins/reactions/partials/sorted-list/emoji-form">
25
- <ul data-type="list" class="list-group"></ul>
26
- <button type="button" data-type="add" class="btn btn-info mt-2">[[reactions:settings.reaction-reputations.add]]</button>
27
- </div>
28
- </div>
29
- </div>
30
- </form>
5
+ <div class="row m-0">
6
+ <div id="spy-container" class="col-12 col-md-8 px-0 mb-4" tabindex="0">
7
+ <form role="form" class="reactions-settings">
8
+ <div class="mb-3">
9
+ <h5 class="fw-bold tracking-tight settings-header">[[reactions:settings.title]]</h5>
10
+ <div class="mb-3">
11
+ <label class="form-label">[[reactions:settings.max-reactions-per-post]]</label>
12
+ <input type="number" min="0" class="form-control" id="maximumReactions" name="maximumReactions">
13
+
14
+ </div>
15
+ <div class="mb-3">
16
+ <label class="form-label">[[reactions:settings.max-reactions-per-user-per-post]]</label>
17
+ <input type="number" min="0" class="form-control" id="maximumReactionsPerUserPerPost" name="maximumReactionsPerUserPerPost">
18
+ <p class="form-text">
19
+ [[reactions:settings.max-reactions-per-user-per-post-help]]
20
+ </p>
21
+ </div>
22
+ <div class="mb-3">
23
+ <label class="form-label">[[reactions:settings.max-reactions-per-message]]</label>
24
+ <input type="number" min="0" class="form-control" id="maximumReactionsPerMessage" name="maximumReactionsPerMessage">
31
25
 
26
+ </div>
27
+ <div class="">
28
+ <label class="form-label">[[reactions:settings.max-reactions-per-user-per-message]]</label>
29
+ <input type="number" min="0" class="form-control" id="maximumReactionsPerUserPerMessage" name="maximumReactionsPerUserPerMessage">
30
+ <p class="form-text">
31
+ [[reactions:settings.max-reactions-per-user-per-message-help]]
32
+ </p>
33
+ </div>
34
+ </div>
32
35
 
33
- <!-- IMPORT admin/partials/save_button.tpl -->
36
+ <div class="mb-3">
37
+ <h5 class="fw-bold tracking-tight settings-header">[[reactions:settings.reaction-reputations]]</h5>
38
+
39
+ <p class="form-text">
40
+ [[reactions:settings.reaction-reputations-help]]
41
+ </p>
42
+ <div class="form-group" data-type="sorted-list" data-sorted-list="reaction-reputations" data-item-template="admin/plugins/reactions/partials/sorted-list/emoji-item" data-form-template="admin/plugins/reactions/partials/sorted-list/emoji-form">
43
+ <ul data-type="list" class="list-group"></ul>
44
+ <button type="button" data-type="add" class="btn btn-info mt-2">[[reactions:settings.reaction-reputations.add]]</button>
45
+ </div>
46
+ </div>
47
+ </form>
48
+ </div>
49
+
50
+ <!-- IMPORT admin/partials/settings/toc.tpl -->
51
+ </div>
52
+ </div>
@@ -0,0 +1,4 @@
1
+ <button class="reaction-add btn btn-sm btn-link {{{ if ./maxReactionsReached }}}max-reactions{{{ end }}}" component="message/reaction/add" data-mid="{./mid}" title="[[reactions:add-reaction]]">
2
+ <i class="fa fa-face-smile"></i>
3
+ </button>
4
+
@@ -0,0 +1,4 @@
1
+ <span class="reaction mb-2 {{{ if ./reacted }}}reacted{{{ end }}}" component="message/reaction" data-mid="{./mid}" data-reaction="{./reaction}" title="{./usernames}" data-bs-toggle="tooltip">
2
+ {./reactionImage}
3
+ <small class="reaction-emoji-count" data-count="{./reactionCount}"></small>
4
+ </span>
@@ -0,0 +1,5 @@
1
+ <div class="reactions {{{ if ./deleted}}}hidden{{{ end }}}" component="message/reactions" data-mid="{./mid}">
2
+ {{{ each ./reactions }}}
3
+ <!-- IMPORT partials/chats/reaction.tpl -->
4
+ {{{ end }}}
5
+ </div>
@@ -1,6 +1,6 @@
1
1
  <span class="reactions" component="post/reactions" data-pid="{./pid}">
2
2
  <span class="reaction-add d-inline-block px-2 mx-1 btn-ghost-sm {{{ if ./maxReactionsReached }}}max-reactions{{{ end }}}" component="post/reaction/add" data-pid="{./pid}" title="[[reactions:add-reaction]]">
3
- <i class="fa fa-plus-square-o"></i>
3
+ <i class="fa fa-face-smile text-primary"></i>
4
4
  </span>
5
5
  {{{ each ./reactions }}}
6
6
  <!-- IMPORT partials/topic/reaction.tpl -->