@nodebb/nodebb-plugin-reactions 2.1.0 → 2.1.2

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.
@@ -5,9 +5,11 @@
5
5
  "error.maximum-reached": "Maximum reactions reached",
6
6
  "error.maximum-per-user-per-post-reached": "Maximum reactions per user per post reached",
7
7
  "settings.title": "Reactions plugin settings",
8
+ "settings.enable-post-reactions": "Enable post reactions",
8
9
  "settings.max-reactions-per-post": "Maximum unique reactions per post (0 for unlimited, default: 4)",
9
10
  "settings.max-reactions-per-user-per-post": "Maximum reactions per user per post (0 for unlimited)",
10
11
  "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.",
12
+ "settings.enable-message-reactions": "Enable message reactions",
11
13
  "settings.max-reactions-per-message": "Maximum unique reactions per message (0 for unlimited, default: 4)",
12
14
  "settings.max-reactions-per-user-per-message": "Maximum reactions per user per message (0 for unlimited)",
13
15
  "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.",
package/library.js CHANGED
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const _ = require.main.require('lodash');
4
3
  const meta = require.main.require('./src/meta');
5
4
  const user = require.main.require('./src/user');
6
5
  const posts = require.main.require('./src/posts');
7
6
  const messaging = require.main.require('./src/messaging');
7
+ const privileges = require.main.require('./src/privileges');
8
8
  const db = require.main.require('./src/database');
9
9
  const routesHelpers = require.main.require('./src/routes/helpers');
10
10
  const websockets = require.main.require('./src/socket.io/index');
@@ -45,12 +45,27 @@ ReactionsPlugin.getPluginConfig = async function (config) {
45
45
  config.maximumReactions = settings.maximumReactions ? parseInt(settings.maximumReactions, 10) : DEFAULT_MAX_EMOTES;
46
46
  config.maximumReactionsPerMessage = settings.maximumReactionsPerMessage ?
47
47
  parseInt(settings.maximumReactionsPerMessage, 10) : DEFAULT_MAX_EMOTES;
48
+ config.enablePostReactions = settings.enablePostReactions === 'on';
49
+ config.enableMessageReactions = settings.enableMessageReactions === 'on';
48
50
  } catch (e) {
49
51
  console.error(e);
50
52
  }
51
53
  return config;
52
54
  };
53
55
 
56
+ ReactionsPlugin.filterSettingsGet = async function (hookData) {
57
+ if (hookData.plugin === 'reactions') {
58
+ const { values } = hookData;
59
+ if (!values.hasOwnProperty('enablePostReactions')) {
60
+ values.enablePostReactions = 'on';
61
+ }
62
+ if (!values.hasOwnProperty('enableMessageReactions')) {
63
+ values.enableMessageReactions = 'on';
64
+ }
65
+ }
66
+ return hookData;
67
+ };
68
+
54
69
  ReactionsPlugin.getPostReactions = async function (data) {
55
70
  if (data.uid === 0) {
56
71
  return data;
@@ -58,6 +73,9 @@ ReactionsPlugin.getPostReactions = async function (data) {
58
73
 
59
74
  try {
60
75
  const settings = await meta.settings.get('reactions');
76
+ if (settings.enablePostReactions === 'off') {
77
+ return data;
78
+ }
61
79
  const maximumReactions = settings.maximumReactions || DEFAULT_MAX_EMOTES;
62
80
 
63
81
  const pids = data.posts.map(post => post && parseInt(post.pid, 10));
@@ -83,42 +101,24 @@ ReactionsPlugin.getPostReactions = async function (data) {
83
101
  }
84
102
  }
85
103
 
86
- const reactionSetToUsersMap = new Map(); // reactionSet -> { uid, username }
87
- if (reactionSets.length > 0) {
88
- const uidsForReactions = await db.getSetsMembers(reactionSets);
89
- const allUids = _.union(...uidsForReactions).filter(Boolean);
90
- const usersData = await user.getUsersFields(allUids, ['uid', 'username']);
91
- const uidToUserdataMap = _.keyBy(usersData, 'uid');
92
-
93
- for (let i = 0, len = reactionSets.length; i < len; i++) {
94
- const uidsForReaction = uidsForReactions[i];
95
- if (uidsForReaction && uidsForReaction.length > 0) {
96
- const usersData = uidsForReaction.map(uid => uidToUserdataMap[uid]).filter(Boolean);
97
- reactionSetToUsersMap.set(reactionSets[i], usersData);
98
- }
99
- }
100
- }
104
+ const reactionSetToUsersMap = await getReactionSetsUidsMap(reactionSets);
101
105
 
102
106
  for (const post of data.posts) {
103
107
  post.maxReactionsReached = pidToIsMaxReactionsReachedMap.get(post.pid);
104
108
  post.reactions = [];
105
109
 
106
- if (pidToReactionsMap.has(post.pid)) {
107
- for (const reaction of pidToReactionsMap.get(post.pid)) {
110
+ const reactions = pidToReactionsMap.get(post.pid);
111
+ if (reactions) {
112
+ for (const reaction of reactions) {
108
113
  const reactionSet = `pid:${post.pid}:reaction:${reaction}`;
109
- if (reactionSetToUsersMap.has(reactionSet)) {
110
- const usersData = reactionSetToUsersMap.get(reactionSet);
111
- const reactionCount = usersData.length;
112
- const reactedUsernames = usersData.map(userData => userData.username).join(', ');
113
- const reactedUids = usersData.map(userData => userData.uid);
114
-
114
+ const uids = reactionSetToUsersMap.get(reactionSet);
115
+ if (Array.isArray(uids)) {
115
116
  post.reactions.push({
116
117
  pid: post.pid,
117
- reacted: reactedUids.includes(data.uid),
118
+ reacted: uids.includes(String(data.uid)),
118
119
  reaction,
119
- usernames: reactedUsernames,
120
120
  reactionImage: parse(reaction),
121
- reactionCount,
121
+ reactionCount: uids.length,
122
122
  });
123
123
  }
124
124
  }
@@ -137,6 +137,9 @@ ReactionsPlugin.getMessageReactions = async function (data) {
137
137
 
138
138
  try {
139
139
  const settings = await meta.settings.get('reactions');
140
+ if (settings.enableMessageReactions === 'off') {
141
+ return data;
142
+ }
140
143
  const maximumReactionsPerMessage = settings.maximumReactionsPerMessage || DEFAULT_MAX_EMOTES;
141
144
 
142
145
  const mids = data.messages.map(message => message && parseInt(message.mid, 10));
@@ -158,42 +161,23 @@ ReactionsPlugin.getMessageReactions = async function (data) {
158
161
  }
159
162
  }
160
163
 
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
- }
164
+ const reactionSetToUsersMap = await getReactionSetsUidsMap(reactionSets);
176
165
 
177
166
  for (const msg of data.messages) {
178
167
  msg.maxReactionsReached = midToIsMaxReactionsReachedMap.get(msg.mid);
179
168
  msg.reactions = [];
180
-
181
- if (midToReactionsMap.has(msg.mid)) {
182
- for (const reaction of midToReactionsMap.get(msg.mid)) {
169
+ const reactions = midToReactionsMap.get(msg.mid);
170
+ if (reactions) {
171
+ for (const reaction of reactions) {
183
172
  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
-
173
+ const uids = reactionSetToUsersMap.get(reactionSet);
174
+ if (Array.isArray(uids)) {
190
175
  msg.reactions.push({
191
176
  mid: msg.mid,
192
- reacted: reactedUids.includes(data.uid),
177
+ reacted: uids.includes(String(data.uid)),
193
178
  reaction,
194
- usernames: reactedUsernames,
195
179
  reactionImage: parse(reaction),
196
- reactionCount,
180
+ reactionCount: uids.length,
197
181
  });
198
182
  }
199
183
  }
@@ -205,6 +189,22 @@ ReactionsPlugin.getMessageReactions = async function (data) {
205
189
  return data;
206
190
  };
207
191
 
192
+
193
+ async function getReactionSetsUidsMap(reactionSets) {
194
+ const reactionSetToUsersMap = new Map(); // reactionSet -> uids
195
+ if (reactionSets.length > 0) {
196
+ const uidsForReactions = await db.getSetsMembers(reactionSets);
197
+
198
+ for (let i = 0, len = reactionSets.length; i < len; i++) {
199
+ const uidsForReaction = uidsForReactions[i];
200
+ if (uidsForReaction && uidsForReaction.length > 0) {
201
+ reactionSetToUsersMap.set(reactionSets[i], uidsForReaction);
202
+ }
203
+ }
204
+ }
205
+ return reactionSetToUsersMap;
206
+ }
207
+
208
208
  ReactionsPlugin.onReply = async function (data) {
209
209
  if (data.uid !== 0) {
210
210
  data.reactions = [];
@@ -229,15 +229,11 @@ ReactionsPlugin.deleteReactions = async function (hookData) {
229
229
 
230
230
  async function sendPostEvent(data, eventName) {
231
231
  try {
232
- const [reactionCount, totalReactions, uids] = await Promise.all([
232
+ const [reactionCount, totalReactions] = await Promise.all([
233
233
  db.setCount(`pid:${data.pid}:reaction:${data.reaction}`),
234
234
  db.setCount(`pid:${data.pid}:reactions`),
235
- db.getSetMembers(`pid:${data.pid}:reaction:${data.reaction}`),
236
235
  ]);
237
236
 
238
- const userdata = await user.getUsersFields(uids, ['uid', 'username']);
239
- const usernames = userdata.map(user => user.username).join(', ');
240
-
241
237
  if (parseInt(reactionCount, 10) === 0) {
242
238
  await db.setRemove(`pid:${data.pid}:reactions`, data.reaction);
243
239
  }
@@ -248,7 +244,6 @@ async function sendPostEvent(data, eventName) {
248
244
  reaction: data.reaction,
249
245
  reactionCount,
250
246
  totalReactions,
251
- usernames,
252
247
  reactionImage: parse(data.reaction),
253
248
  });
254
249
  } catch (e) {
@@ -258,15 +253,11 @@ async function sendPostEvent(data, eventName) {
258
253
 
259
254
  async function sendMessageEvent(data, eventName) {
260
255
  try {
261
- const [reactionCount, totalReactions, uids] = await Promise.all([
256
+ const [reactionCount, totalReactions] = await Promise.all([
262
257
  db.setCount(`mid:${data.mid}:reaction:${data.reaction}`),
263
258
  db.setCount(`mid:${data.mid}:reactions`),
264
- db.getSetMembers(`mid:${data.mid}:reaction:${data.reaction}`),
265
259
  ]);
266
260
 
267
- const userdata = await user.getUsersFields(uids, ['uid', 'username']);
268
- const usernames = userdata.map(user => user.username).join(', ');
269
-
270
261
  if (parseInt(reactionCount, 10) === 0) {
271
262
  await db.setRemove(`mid:${data.mid}:reactions`, data.reaction);
272
263
  }
@@ -277,7 +268,6 @@ async function sendMessageEvent(data, eventName) {
277
268
  reaction: data.reaction,
278
269
  reactionCount,
279
270
  totalReactions,
280
- usernames,
281
271
  reactionImage: parse(data.reaction),
282
272
  });
283
273
  } catch (e) {
@@ -310,6 +300,9 @@ SocketPlugins.reactions = {
310
300
  }
311
301
 
312
302
  const settings = await meta.settings.get('reactions');
303
+ if (settings.enablePostReactions === 'off') {
304
+ throw new Error('[[error:post-reactions-disabled]]');
305
+ }
313
306
  const maximumReactions = settings.maximumReactions || DEFAULT_MAX_EMOTES;
314
307
  const [tid, totalReactions, emojiIsAlreadyExist, alreadyReacted, reactionReputation] = await Promise.all([
315
308
  posts.getPostField(data.pid, 'tid'),
@@ -360,11 +353,15 @@ SocketPlugins.reactions = {
360
353
  throw new Error('[[reactions:error.invalid-reaction]]');
361
354
  }
362
355
 
363
- const [tid, hasReacted, reactionReputation] = await Promise.all([
356
+ const [settings, tid, hasReacted, reactionReputation] = await Promise.all([
357
+ meta.settings.get('reactions'),
364
358
  posts.getPostField(data.pid, 'tid'),
365
359
  db.isSetMember(`pid:${data.pid}:reaction:${data.reaction}`, socket.uid),
366
360
  getReactionReputation(data.reaction),
367
361
  ]);
362
+ if (settings.enablePostReactions === 'off') {
363
+ throw new Error('[[error:post-reactions-disabled]]');
364
+ }
368
365
  if (!tid) {
369
366
  throw new Error('[[error:no-post]]');
370
367
  }
@@ -395,6 +392,9 @@ SocketPlugins.reactions = {
395
392
  }
396
393
 
397
394
  const settings = await meta.settings.get('reactions');
395
+ if (settings.enableMessageReactions === 'off') {
396
+ throw new Error('[[error:post-reactions-disabled]]');
397
+ }
398
398
  const maximumReactionsPerMessage = settings.maximumReactionsPerMessage || DEFAULT_MAX_EMOTES;
399
399
  const [roomId, totalReactions, emojiIsAlreadyExist] = await Promise.all([
400
400
  messaging.getMessageField(data.mid, 'roomId'),
@@ -442,10 +442,14 @@ SocketPlugins.reactions = {
442
442
  throw new Error('[[reactions:error.invalid-reaction]]');
443
443
  }
444
444
 
445
- const [roomId, hasReacted] = await Promise.all([
445
+ const [settings, roomId, hasReacted] = await Promise.all([
446
+ meta.settings.get('reactions'),
446
447
  messaging.getMessageField(data.mid, 'roomId'),
447
448
  db.isSetMember(`mid:${data.mid}:reaction:${data.reaction}`, socket.uid),
448
449
  ]);
450
+ if (settings.enableMessageReactions === 'off') {
451
+ throw new Error('[[error:post-reactions-disabled]]');
452
+ }
449
453
  if (!roomId) {
450
454
  throw new Error('[[error:no-message]]');
451
455
  }
@@ -462,5 +466,43 @@ SocketPlugins.reactions = {
462
466
 
463
467
  await sendMessageEvent(data, 'event:reactions.removeMessageReaction');
464
468
  },
469
+ getReactionUsernames: async function (socket, data) {
470
+ if (!socket.uid) {
471
+ throw new Error('[[error:not-logged-in]]');
472
+ }
473
+ if (!emojiTable[data.reaction]) {
474
+ throw new Error('[[reactions:error.invalid-reaction]]');
475
+ }
476
+ let set = '';
477
+ if (data.type === 'post') {
478
+ if (!await privileges.posts.can('topics:read', data.pid, socket.uid)) {
479
+ throw new Error('[[error:not-allowed]]');
480
+ }
481
+ set = `pid:${data.pid}:reaction:${data.reaction}`;
482
+ } else if (data.type === 'message') {
483
+ const roomId = await messaging.getMessageField(data.mid, 'roomId');
484
+ if (!await messaging.canViewMessage(data.mid, roomId, socket.uid)) {
485
+ throw new Error('[[error:not-allowed]]');
486
+ }
487
+ set = `mid:${data.mid}:reaction:${data.reaction}`;
488
+ } else {
489
+ throw new Error('[[error:invalid-data]]');
490
+ }
491
+ let uids = await db.getSetMembers(set);
492
+ const cutoff = 6;
493
+
494
+ let otherCount = 0;
495
+ if (uids.length > cutoff) {
496
+ otherCount = uids.length - (cutoff - 1);
497
+ uids = uids.slice(0, cutoff - 1);
498
+ }
499
+
500
+ const usernames = await user.getUsernamesByUids(uids);
501
+ return {
502
+ cutoff: cutoff,
503
+ otherCount,
504
+ usernames,
505
+ };
506
+ },
465
507
  };
466
508
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodebb/nodebb-plugin-reactions",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "nbbpm": {
5
5
  "compatibility": "^3.3.0"
6
6
  },
package/plugin.json CHANGED
@@ -25,6 +25,9 @@
25
25
  {
26
26
  "hook": "filter:config.get", "method": "getPluginConfig"
27
27
  },
28
+ {
29
+ "hook": "filter:settings.get", "method": "filterSettingsGet"
30
+ },
28
31
  {
29
32
  "hook": "filter:post.getPosts", "method": "getPostReactions"
30
33
  },
package/public/client.js CHANGED
@@ -3,6 +3,8 @@
3
3
  $(document).ready(function () {
4
4
  setupReactions();
5
5
  let alerts;
6
+ let mouseOverReactionEl;
7
+ let tooltipTimeoutId = 0;
6
8
  function setupReactions() {
7
9
  createReactionTooltips();
8
10
  require(['hooks', 'alerts'], function (hooks, _alerts) {
@@ -10,7 +12,11 @@ $(document).ready(function () {
10
12
  hooks.on('action:ajaxify.end', function () {
11
13
  if (ajaxify.data.template.topic) {
12
14
  setupPostReactions();
13
- } else if (ajaxify.data.template.chats && ajaxify.data.roomId) {
15
+ }
16
+ });
17
+ // switchChat uses action:chat.loaded and not action:ajaxify.end
18
+ hooks.on('action:chat.loaded', function () {
19
+ if (ajaxify.data.template.chats && ajaxify.data.roomId) {
14
20
  setupMessageReactions();
15
21
  }
16
22
  });
@@ -186,11 +192,18 @@ $(document).ready(function () {
186
192
  mid: data.mid,
187
193
  reaction: data.reaction,
188
194
  reactionCount: data.reactionCount,
189
- usernames: data.usernames,
190
195
  reacted: isSelf && type === 'add',
191
196
  reactionImage: data.reactionImage,
192
197
  }, function (html) {
193
- $('[component="message/reactions"][data-mid="' + data.mid + '"]').append(html);
198
+ require(['forum/chats/messages'], function (messages) {
199
+ const reactionEl = $('[component="message/reactions"][data-mid="' + data.mid + '"]');
200
+ const chatContentEl = reactionEl.parents('[component="chat/message/content"]');
201
+ const isAtBottom = messages.isAtBottom(chatContentEl);
202
+ reactionEl.append(html);
203
+ if (isAtBottom || isSelf) {
204
+ messages.scrollToBottom(chatContentEl);
205
+ }
206
+ });
194
207
  });
195
208
  } else {
196
209
  reactionEl.find('.reaction-emoji-count').attr('data-count', data.reactionCount);
@@ -203,13 +216,65 @@ $(document).ready(function () {
203
216
  }
204
217
 
205
218
  function createReactionTooltips() {
206
- if (!utils.isTouchDevice()) {
207
- $('#content').tooltip({
208
- selector: '.reaction',
209
- placement: 'top',
210
- container: '#content',
211
- animation: false,
212
- });
213
- }
219
+ require(['bootstrap', 'translator'], function (bootstrap, translator) {
220
+ async function createTooltip(data) {
221
+ if (!mouseOverReactionEl || !mouseOverReactionEl.length) {
222
+ return;
223
+ }
224
+ const el = mouseOverReactionEl;
225
+ let usernames = data.usernames.filter(name => name !== '[[global:former_user]]');
226
+ if (!usernames.length) {
227
+ return;
228
+ }
229
+ if (usernames.length + data.otherCount > data.cutoff) {
230
+ usernames = usernames.join(', ').replace(/,/g, '|');
231
+ usernames = await translator.translate('[[topic:users_and_others, ' + usernames + ', ' + data.otherCount + ']]');
232
+ usernames = usernames.replace(/\|/g, ',');
233
+ } else {
234
+ usernames = usernames.join(', ');
235
+ }
236
+
237
+ el.attr('title', usernames);
238
+ (new bootstrap.Tooltip(el, {
239
+ container: '#content',
240
+ html: true,
241
+ placement: 'top',
242
+ animation: false,
243
+ })).show();
244
+ }
245
+
246
+ if (!utils.isTouchDevice()) {
247
+ $('#content').on('mouseenter', '.reaction', function () {
248
+ const $this = $(this);
249
+ mouseOverReactionEl = $this;
250
+ const mid = $this.attr('data-mid');
251
+ const pid = $this.attr('data-pid');
252
+ tooltipTimeoutId = setTimeout(async () => {
253
+ if (mouseOverReactionEl && mouseOverReactionEl.length) {
254
+ const d = await socket.emit('plugins.reactions.getReactionUsernames', {
255
+ type: pid ? 'post' : 'message',
256
+ mid: mid,
257
+ pid: pid,
258
+ reaction: $this.attr('data-reaction'),
259
+ });
260
+ createTooltip(d);
261
+ }
262
+ }, 200);
263
+ });
264
+ $('#content').on('mouseleave', '.reaction', function () {
265
+ if (tooltipTimeoutId) {
266
+ clearTimeout(tooltipTimeoutId);
267
+ tooltipTimeoutId = 0;
268
+ }
269
+ mouseOverReactionEl = null;
270
+ const $this = $(this);
271
+ const tooltip = bootstrap.Tooltip.getInstance(this);
272
+ if (tooltip) {
273
+ tooltip.dispose();
274
+ $this.attr('title', '');
275
+ }
276
+ });
277
+ }
278
+ });
214
279
  }
215
280
  });
@@ -7,10 +7,13 @@
7
7
  <form role="form" class="reactions-settings">
8
8
  <div class="mb-3">
9
9
  <h5 class="fw-bold tracking-tight settings-header">[[reactions:settings.title]]</h5>
10
+ <div class="form-check form-switch mb-3">
11
+ <input id="enablePostReactions" name="enablePostReactions" type="checkbox" class="form-check-input">
12
+ <label for="enablePostReactions" class="form-check-label">[[reactions:settings.enable-post-reactions]]</label>
13
+ </div>
10
14
  <div class="mb-3">
11
15
  <label class="form-label">[[reactions:settings.max-reactions-per-post]]</label>
12
16
  <input type="number" min="0" class="form-control" id="maximumReactions" name="maximumReactions">
13
-
14
17
  </div>
15
18
  <div class="mb-3">
16
19
  <label class="form-label">[[reactions:settings.max-reactions-per-user-per-post]]</label>
@@ -19,6 +22,11 @@
19
22
  [[reactions:settings.max-reactions-per-user-per-post-help]]
20
23
  </p>
21
24
  </div>
25
+ <hr/>
26
+ <div class="form-check form-switch mb-3">
27
+ <input id="enableMessageReactions" name="enableMessageReactions" type="checkbox" class="form-check-input">
28
+ <label for="enableMessageReactions" class="form-check-label">[[reactions:settings.enable-message-reactions]]</label>
29
+ </div>
22
30
  <div class="mb-3">
23
31
  <label class="form-label">[[reactions:settings.max-reactions-per-message]]</label>
24
32
  <input type="number" min="0" class="form-control" id="maximumReactionsPerMessage" name="maximumReactionsPerMessage">
@@ -1,4 +1,6 @@
1
+ {{{ if config.enableMessageReactions }}}
1
2
  <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
3
  <i class="fa fa-face-smile"></i>
3
4
  </button>
5
+ {{{ end }}}
4
6
 
@@ -1,4 +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">
1
+ <span class="reaction mb-2 {{{ if ./reacted }}}reacted{{{ end }}}" component="message/reaction" data-mid="{./mid}" data-reaction="{./reaction}">
2
2
  {./reactionImage}
3
3
  <small class="reaction-emoji-count" data-count="{./reactionCount}"></small>
4
4
  </span>
@@ -1,5 +1,7 @@
1
+ {{{ if config.enableMessageReactions }}}
1
2
  <div class="reactions {{{ if ./deleted}}}hidden{{{ end }}}" component="message/reactions" data-mid="{./mid}">
2
3
  {{{ each ./reactions }}}
3
4
  <!-- IMPORT partials/chats/reaction.tpl -->
4
5
  {{{ end }}}
5
- </div>
6
+ </div>
7
+ {{{ end }}}
@@ -1,4 +1,4 @@
1
- <span class="reaction {{{ if ./reacted }}}reacted{{{ end }}}" component="post/reaction" data-pid="{./pid}" data-reaction="{./reaction}" title="{./usernames}">
1
+ <span class="reaction {{{ if ./reacted }}}reacted{{{ end }}}" component="post/reaction" data-pid="{./pid}" data-reaction="{./reaction}">
2
2
  {./reactionImage}
3
3
  <small class="reaction-emoji-count" data-count="{./reactionCount}"></small>
4
4
  </span>
@@ -1,3 +1,4 @@
1
+ {{{ if config.enablePostReactions }}}
1
2
  <span class="reactions" component="post/reactions" data-pid="{./pid}">
2
3
  <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
4
  <i class="fa fa-face-smile text-primary"></i>
@@ -5,4 +6,5 @@
5
6
  {{{ each ./reactions }}}
6
7
  <!-- IMPORT partials/topic/reaction.tpl -->
7
8
  {{{ end }}}
8
- </span>
9
+ </span>
10
+ {{{ end }}}