@nodebb/nodebb-plugin-reactions 2.0.0 → 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
@@ -1,2 +1,10 @@
1
1
  # nodebb-plugin-reactions
2
2
  Reactions plugin for NodeBB
3
+
4
+ # Screenshots
5
+
6
+ ## Reactions:
7
+ ![demo](./assets/demo.png)
8
+
9
+ ## ACP:
10
+ ![acp](./assets/acp.png)
package/assets/acp.png ADDED
Binary file
Binary file
@@ -3,8 +3,14 @@
3
3
  "add-reaction": "Add reaction",
4
4
  "error.invalid-reaction": "Invalid reaction",
5
5
  "error.maximum-reached": "Maximum reactions reached",
6
+ "error.maximum-per-user-per-post-reached": "Maximum reactions per user per post reached",
6
7
  "settings.title": "Reactions plugin settings",
7
- "settings.max-reactions-per-post": "Maximum unique reactions per post",
8
+ "settings.max-reactions-per-post": "Maximum unique reactions per post (0 for unlimited, default: 4)",
9
+ "settings.max-reactions-per-user-per-post": "Maximum reactions per user per post (0 for unlimited)",
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.",
8
14
  "settings.reaction-reputations": "Reaction Reputations (Optional)",
9
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.",
10
16
  "settings.reaction-reputations.add": "Add Rule",
@@ -3,8 +3,11 @@
3
3
  "add-reaction": "הוסף תגובה",
4
4
  "error.invalid-reaction": "תגובה לא חוקית",
5
5
  "error.maximum-reached": "הגעת למספר התגובות המקסימלי לפוסט",
6
+ "error.maximum-per-user-per-post-reached": "הגעת למספר התגובות המקסימלי למשתמש לפוסט",
6
7
  "settings.title": "הגדרות תגובות",
7
- "settings.max-reactions-per-post": "מספר התגובות המקסימלי לפוסט",
8
+ "settings.max-reactions-per-post": "מספר התגובות המקסימלי לפוסט (0 = ללא הגבלה, ברירת מחדל: 4)",
9
+ "settings.max-reactions-per-user-per-post": "מספר התגובות המקסימלי למשתמש לפוסט (0 = ללא הגבלה)",
10
+ "settings.max-reactions-per-user-per-post-help": ".ההגבלה נאכפת רק בהוספת תגובה חדשה ולא בהצטרפות לתגובה קיימת",
8
11
  "settings.reaction-reputations": "מוניטין תגובה (אופציונלי)",
9
12
  "settings.reaction-reputations-help": "אתה יכול להקצות מוניטין לתגובות מסוימות. כאשר התגובה נוספת לפוסט, בעל הפוסט יקבל את המוניטין שנקבע.",
10
13
  "settings.reaction-reputations.add": "הוסף כלל",
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');
@@ -13,19 +14,20 @@ const emojiParser = require.main.require('nodebb-plugin-emoji/build/lib/parse.js
13
14
  const emojiTable = require.main.require('nodebb-plugin-emoji/build/emoji/table.json');
14
15
  const emojiAliases = require.main.require('nodebb-plugin-emoji/build/emoji/aliases.json');
15
16
 
16
- const DEFAULT_MAX_EMOTES = 5;
17
+ const DEFAULT_MAX_EMOTES = 4;
17
18
 
18
19
  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
  }
@@ -71,7 +75,7 @@ ReactionsPlugin.getReactions = async function (data) {
71
75
 
72
76
  if (reactionsList && reactionsList.length > 0) {
73
77
  pidToReactionsMap.set(pid, reactionsList);
74
- pidToIsMaxReactionsReachedMap.set(pid, reactionsCount >= maximumReactions);
78
+ pidToIsMaxReactionsReachedMap.set(pid, reactionsCount > maximumReactions);
75
79
  reactionSets = reactionSets.concat(reactionsList.map(reaction => `pid:${pid}:reaction:${reaction}`));
76
80
  }
77
81
  } catch (e) {
@@ -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,19 +309,35 @@ 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, isMember, 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
  ]);
321
+ if (!tid) {
322
+ throw new Error('[[error:no-post]]');
323
+ }
324
+ data.uid = socket.uid;
325
+ data.tid = tid;
326
+ if (!emojiIsAlreadyExist) {
327
+ if (totalReactions > maximumReactions) {
328
+ throw new Error(`[[reactions:error.maximum-reached]] (${maximumReactions})`);
329
+ }
214
330
 
215
- if (!isMember && totalReactions >= maximumReactions) {
216
- throw new Error('[[reactions:error.maximum-reached]]');
331
+ const maximumReactionsPerUserPerPost = settings.maximumReactionsPerUserPerPost ?
332
+ parseInt(settings.maximumReactionsPerUserPerPost, 10) : 0;
333
+ if (maximumReactionsPerUserPerPost > 0) {
334
+ const emojiesInPost = await db.getSetMembers(`pid:${data.pid}:reactions`);
335
+ const userPostReactions = await db.isMemberOfSets(emojiesInPost.map(emojiName => `pid:${data.pid}:reaction:${emojiName}`), socket.uid);
336
+ const userPostReactionCount = userPostReactions.filter(Boolean).length;
337
+ if (userPostReactionCount > maximumReactionsPerUserPerPost) {
338
+ throw new Error(`[[reactions:error.maximum-per-user-per-post-reached]] (${maximumReactionsPerUserPerPost})`);
339
+ }
340
+ }
217
341
  }
218
342
 
219
343
  await Promise.all([
@@ -225,7 +349,7 @@ SocketPlugins.reactions = {
225
349
  await giveOwnerReactionReputation(reactionReputation, data.pid);
226
350
  }
227
351
 
228
- await sendEvent(data, 'event:reactions.addPostReaction');
352
+ await sendPostEvent(data, 'event:reactions.addPostReaction');
229
353
  },
230
354
  removePostReaction: async function (socket, data) {
231
355
  if (!socket.uid) {
@@ -236,30 +360,107 @@ SocketPlugins.reactions = {
236
360
  throw new Error('[[reactions:error.invalid-reaction]]');
237
361
  }
238
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
+ }
239
371
  data.uid = socket.uid;
372
+ data.tid = tid;
240
373
 
241
- try {
242
- const [hasReacted, reactionReputation] = await Promise.all([
243
- db.isSetMember(`pid:${data.pid}:reaction:${data.reaction}`, socket.uid),
244
- getReactionReputation(data.reaction),
245
- ]);
246
- if (hasReacted) {
247
- await db.setRemove(`pid:${data.pid}:reaction:${data.reaction}`, socket.uid);
248
- }
374
+ if (hasReacted) {
375
+ await db.setRemove(`pid:${data.pid}:reaction:${data.reaction}`, socket.uid);
376
+ }
249
377
 
250
- const reactionCount = await db.setCount(`pid:${data.pid}:reaction:${data.reaction}`);
251
- if (reactionCount === 0) {
252
- await db.setRemove(`pid:${data.pid}:reactions`, data.reaction);
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
+ }
385
+
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})`);
253
415
  }
254
- if (hasReacted && reactionReputation > 0) {
255
- 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
+ }
256
426
  }
257
- await sendEvent(data, 'event:reactions.removePostReaction');
258
- } catch (e) {
259
- console.error(e);
260
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');
261
435
  },
262
- };
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
+ }
263
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
+ };
264
466
 
265
- module.exports = ReactionsPlugin;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@nodebb/nodebb-plugin-reactions",
3
- "version": "2.0.0",
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,56 +23,193 @@ $(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) {
77
- var maxReactionsReached = parseInt(data.totalReactions, 10) >= config.maximumReactions;
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) {
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
 
80
- var reactionEl = $('[component="post/reaction"][data-pid="' + data.pid + '"][data-reaction="' + data.reaction + '"]');
145
+ var reactionEl = $(`[component="post/reaction"][data-pid="${data.pid}"][data-reaction="${data.reaction}"]`);
81
146
 
82
147
  if (parseInt(data.reactionCount, 10) === 0) {
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);
97
- createReactionTooltips();
98
162
  });
99
163
  } else {
100
164
  reactionEl.find('.reaction-emoji-count').attr('data-count', data.reactionCount);
101
- reactionEl.attr('data-original-title', data.usernames);
102
- reactionEl.toggleClass('reacted', !(parseInt(data.uid, 10) === app.user.uid));
165
+ reactionEl.attr('data-bs-original-title', data.usernames);
166
+ reactionEl.attr('aria-label', data.usernames);
167
+ if (isSelf) {
168
+ reactionEl.toggleClass('reacted', type === 'add');
169
+ }
103
170
  }
104
171
  }
105
172
 
106
- function createReactionTooltips() {
107
- $('.reaction, .reaction-add').each(function () {
108
- if (!utils.isTouchDevice()) {
109
- $(this).tooltip({
110
- placement: 'top',
111
- title: $(this).attr('title'),
112
- });
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');
113
201
  }
114
- });
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
+ }
115
214
  }
116
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,26 +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="text" class="form-control" id="maximumReactions" name="maximumReactions">
8
- </div>
9
- </div>
10
- </div>
11
- <div class="row">
12
- <div class="col-sm-2 col-xs-12 settings-header">[[reactions:settings.reaction-reputations]]</div>
13
- <div class="col-sm-10 col-xs-12 mt-2">
14
- <p class="help-text">
15
- [[reactions:settings.reaction-reputations-help]]
16
- </p>
17
- <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">
18
- <ul data-type="list" class="list-group"></ul>
19
- <button type="button" data-type="add" class="btn btn-info mt-2">[[reactions:settings.reaction-reputations.add]]</button>
20
- </div>
21
- </div>
22
- </div>
23
- </form>
24
1
 
2
+ <div class="acp-page-container">
3
+ <!-- IMPORT admin/partials/settings/header.tpl -->
4
+
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">
25
25
 
26
- <!-- IMPORT admin/partials/save_button.tpl -->
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>
35
+
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 -->