@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 +8 -0
- package/assets/acp.png +0 -0
- package/assets/demo.png +0 -0
- package/languages/en-GB/reactions.json +7 -1
- package/languages/he/reactions.json +4 -1
- package/library.js +234 -33
- package/package.json +2 -2
- package/plugin.json +4 -1
- package/public/client.js +165 -66
- package/scss/reactions.scss +2 -2
- package/templates/admin/plugins/reactions.tpl +50 -24
- package/templates/partials/chats/add-reaction.tpl +4 -0
- package/templates/partials/chats/reaction.tpl +4 -0
- package/templates/partials/chats/reactions.tpl +5 -0
- package/templates/partials/topic/reactions.tpl +1 -1
package/README.md
CHANGED
package/assets/acp.png
ADDED
|
Binary file
|
package/assets/demo.png
ADDED
|
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 =
|
|
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
|
-
|
|
26
|
-
res.render('admin/plugins/reactions', {
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
216
|
-
|
|
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
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
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": "
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
function
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
121
|
+
function setupPostReactionSockets() {
|
|
67
122
|
socket.off('event:reactions.addPostReaction').on('event:reactions.addPostReaction', function (data) {
|
|
68
|
-
|
|
123
|
+
updatePostReactionCount(data, 'add');
|
|
69
124
|
});
|
|
70
125
|
|
|
71
126
|
socket.off('event:reactions.removePostReaction').on('event:reactions.removePostReaction', function (data) {
|
|
72
|
-
|
|
127
|
+
updatePostReactionCount(data, 'remove');
|
|
73
128
|
});
|
|
74
129
|
}
|
|
75
130
|
|
|
76
|
-
function
|
|
77
|
-
|
|
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 = $(
|
|
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:
|
|
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.
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
});
|
package/scss/reactions.scss
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.reactions {
|
|
2
2
|
.reaction {
|
|
3
|
-
border: 1px solid
|
|
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
|
|
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
|
-
|
|
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
|
+
<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>
|
|
@@ -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-
|
|
3
|
+
<i class="fa fa-face-smile text-primary"></i>
|
|
4
4
|
</span>
|
|
5
5
|
{{{ each ./reactions }}}
|
|
6
6
|
<!-- IMPORT partials/topic/reaction.tpl -->
|