From 913658d144c824dde5f6ba7deec2b1cc9e8405c1 Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 7 Feb 2022 01:59:19 +0800 Subject: [PATCH] core&ui: support reaction --- packages/hydrooj/package.json | 4 +- packages/hydrooj/src/handler/discussion.ts | 15 +++- packages/hydrooj/src/model/discussion.ts | 15 +++- packages/hydrooj/src/model/problem.ts | 2 +- packages/hydrooj/src/service/server.ts | 6 ++ packages/ui-default/backendlib/template.js | 1 + .../components/discussion/comments.page.styl | 31 +++++++ .../{comments.page.js => comments.page.tsx} | 89 +++++++++++++++++-- packages/ui-default/misc/icons/emoji.svg | 1 + packages/ui-default/package.json | 2 +- .../components/comments_discussion.html | 48 ++++++---- .../templates/discussion_detail.html | 3 +- 12 files changed, 180 insertions(+), 37 deletions(-) rename packages/ui-default/components/discussion/{comments.page.js => comments.page.tsx} (62%) create mode 100644 packages/ui-default/misc/icons/emoji.svg diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index e7c4acdb8..a3b82b331 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "3.4.6", + "version": "3.4.7", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", @@ -20,6 +20,8 @@ "cac": "^6.7.12", "cookies": "^0.8.0", "detect-browser": "^5.3.0", + "emoji-regex": "^10.0.0", + "emojis-list": "2.1.0", "esbuild": "0.14.3", "fs-extra": "^10.0.0", "graphql": "^16.3.0", diff --git a/packages/hydrooj/src/handler/discussion.ts b/packages/hydrooj/src/handler/discussion.ts index 10dd5b5e8..08d0a5783 100644 --- a/packages/hydrooj/src/handler/discussion.ts +++ b/packages/hydrooj/src/handler/discussion.ts @@ -179,6 +179,11 @@ class DiscussionDetailHandler extends DiscussionHandler { for (const drdoc of drdocs) { if (drdoc.reply) uids.push(...drdoc.reply.map((drrdoc) => drrdoc.owner)); } + const reactions = { [did.toHexString()]: dsdoc?.react || {} }; + await Promise.all(drdocs.map((drdoc) => + discussion.getReaction(domainId, document.TYPE_DISCUSSION_REPLY, drdoc._id, this.user._id).then((reaction) => { + reactions[drdoc._id.toHexString()] = reaction; + }))); const udict = await user.getList(domainId, uids); if (!dsdoc?.view) { await Promise.all([ @@ -194,7 +199,7 @@ class DiscussionDetailHandler extends DiscussionHandler { ]; this.response.template = 'discussion_detail.html'; this.response.body = { - path, ddoc: this.ddoc, dsdoc, drdocs, page, pcount, drcount, udict, vnode: this.vnode, + path, ddoc: this.ddoc, dsdoc, drdocs, page, pcount, drcount, udict, vnode: this.vnode, reactions, }; } @@ -203,12 +208,14 @@ class DiscussionDetailHandler extends DiscussionHandler { } @param('type', Types.Range(['did', 'drid'])) - @param('did', Types.ObjectID) - @param('id', Types.Name) + @param('id', Types.ObjectID) + @param('emoji', Types.Emoji) @param('reverse', Types.Boolean) async postReaction(domainId: string, type: string, did: ObjectID, id: string, reverse = false) { this.checkPerm(PERM.PERM_ADD_REACTION); - await discussion.react(domainId, type === 'did' ? document.TYPE_DISCUSSION : document.TYPE_DISCUSSION_REPLY, did, id, this.user._id, reverse); + const docType = type === 'did' ? document.TYPE_DISCUSSION : document.TYPE_DISCUSSION_REPLY; + const [doc, sdoc] = await discussion.react(domainId, docType, did, id, this.user._id, reverse); + this.response.body = { doc, sdoc }; this.back(); } diff --git a/packages/hydrooj/src/model/discussion.ts b/packages/hydrooj/src/model/discussion.ts index 607c84a6d..458d5ee6f 100644 --- a/packages/hydrooj/src/model/discussion.ts +++ b/packages/hydrooj/src/model/discussion.ts @@ -56,7 +56,7 @@ export const PROJECTION_LIST: DiscussionDoc.Field[] = [ 'parentId', 'parentType', 'title', ]; export const PROJECTION_PUBLIC: DiscussionDoc.Field[] = [ - ...PROJECTION_LIST, 'content', 'history', + ...PROJECTION_LIST, 'content', 'history', 'react', 'maintainer', ]; export const typeDisplay = { @@ -184,8 +184,16 @@ export function getListReply(domainId: string, did: ObjectID): Promise 0) { + if (group.length > 0 && canViewHidden !== true) { pdocs = pdocs.filter((i) => !i.assign?.length || Set.intersection(group, i.assign).size); } if (canViewHidden !== true) { diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index 2596a31fd..777d58fa5 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -4,6 +4,7 @@ import http from 'http'; import { PassThrough } from 'stream'; import cac from 'cac'; import Cookies from 'cookies'; +import emojiRegex from 'emoji-regex'; import Koa, { Context } from 'koa'; import Body from 'koa-body'; import Compress from 'koa-compress'; @@ -86,6 +87,7 @@ export interface Types { NumericArray: Type, CommaSeperatedArray: Type, Set: Type, + Emoji: Type, } export const Types: Types = { @@ -172,6 +174,10 @@ export const Types: Types = { if (v instanceof Array) return new Set(v); return v ? new Set([v]) : new Set(); }, null], + Emoji: [ + (v: string) => v.matchAll(emojiRegex()).next().value[0], + (v) => emojiRegex().test(v), + ], }; function _buildParam(name: string, source: 'get' | 'post' | 'all' | 'route', ...args: Array) { diff --git a/packages/ui-default/backendlib/template.js b/packages/ui-default/backendlib/template.js index 699f7c4ac..0570b4e18 100644 --- a/packages/ui-default/backendlib/template.js +++ b/packages/ui-default/backendlib/template.js @@ -121,6 +121,7 @@ env.addGlobal('static_url', (assetName) => { // eslint-disable-next-line no-eval env.addGlobal('eval', eval); env.addGlobal('Date', Date); +env.addGlobal('Object', Object); env.addGlobal('Math', Math); env.addGlobal('process', process); env.addGlobal('global', global); diff --git a/packages/ui-default/components/discussion/comments.page.styl b/packages/ui-default/components/discussion/comments.page.styl index 66c33541d..44a3eb5e1 100644 --- a/packages/ui-default/components/discussion/comments.page.styl +++ b/packages/ui-default/components/discussion/comments.page.styl @@ -27,6 +27,37 @@ $downvote-color = red &:first-child border-top: 0 + + .reactions + display: flex + flex-direction: row + width: 100% + flex-wrap: wrap + margin-top: 0.25rem + overflow: visible + .reaction + display: flex + flex-direction: row + height: 1.75rem + white-space: nowrap + width: auto + margin: 0.125rem + padding: 0 0.5rem + border: 2px solid transparent + border-radius: 1.75rem + font-weight: 500 + overflow: visible + border-color: lighten(blue, 75%) + &.active + background-color: lighten(blue, 60%) + &:hover + background-color: lighten(blue, 75%) !important + p + padding-top: 0px + margin-top: 0px + padding-left: 0px + margin-right: 2px + line-height: 1.5 .dczcomments__reply >.media diff --git a/packages/ui-default/components/discussion/comments.page.js b/packages/ui-default/components/discussion/comments.page.tsx similarity index 62% rename from packages/ui-default/components/discussion/comments.page.js rename to packages/ui-default/components/discussion/comments.page.tsx index a43e0da55..a8bdef39c 100644 --- a/packages/ui-default/components/discussion/comments.page.js +++ b/packages/ui-default/components/discussion/comments.page.tsx @@ -1,5 +1,8 @@ import 'jquery.easing'; +import * as React from 'react'; +import ReactDOM from 'react-dom'; +import { Popover } from '@blueprintjs/core'; import { AutoloadPage } from 'vj/misc/Page'; import CommentBox from 'vj/components/discussion/CommentBox'; import { ConfirmDialog } from 'vj/components/dialog'; @@ -9,6 +12,7 @@ import { slideDown, slideUp } from 'vj/utils/slide'; import request from 'vj/utils/request'; import i18n from 'vj/utils/i18n'; import tpl from 'vj/utils/tpl'; +import { chunk } from 'lodash'; const $replyTemplate = $('.commentbox-container').eq(0).clone(); @@ -72,7 +76,7 @@ function onClickDummyBox(ev) { .focus(); } -async function onCommentClickReplyComment(ev, options = {}) { +async function onCommentClickReplyComment(ev, options: any = {}) { const $evTarget = $(ev.currentTarget); if (CommentBox.get($evTarget)) { @@ -184,14 +188,85 @@ function onCommentClickDeleteReply(ev) { onCommentClickDelete('reply', ev); } +function renderReactions(reactions, self, rootEle) { + let html = ''; + for (const key in reactions) { + if (!reactions[key]) continue; + html += `

${key} ${reactions[key]}

`; + } + rootEle.html(html); +} + +async function handleEmojiClick(payload, emoji, ele) { + const res = await request.post('', { ...payload, emoji }); + renderReactions(res.doc?.react, res.sdoc?.react, ele); +} + +function getRow(count) { + if (count <= 2) return 2; + if (count <= 3) return 3; + if (count <= 4) return 4; + if (count <= 6) return 6; + return 12; +} + +function Reaction({ payload, ele }) { + const emojiList: string[] = (UiContext.emojiList || '👍 👎 😄 😕 ❤️ 🤔 🤣 🌿 🍋 🕊️ 👀 🤣').split(' '); + const elesPerRow = getRow(Math.sqrt(emojiList.length)); + const [focus, updateFocus] = React.useState(false); + const [finish, updateFinish] = React.useState(false); + if (finish) setTimeout(() => updateFinish(false), 1000); + return ( + // eslint-disable-next-line no-nested-ternary + + +
+ {chunk(emojiList, elesPerRow).map((line, i) => ( +
+ {line.map((emoji) => ( +
handleEmojiClick(payload, emoji, ele).then(() => updateFinish(true))} + > + {emoji} +
+ ))} +
+ ))} +
+
+ updateFocus(true)} onBlur={() => updateFocus(false)}> +
+
+
+
+ ); +} + const commentsPage = new AutoloadPage('commentsPage', () => { $(document).on('click', '[name="dczcomments__dummy-box"]', onClickDummyBox); - $(document).on('click', '[name="dczcomments__op-reply-comment"]', onCommentClickReplyComment); - $(document).on('click', '[name="dczcomments__op-reply-reply"]', onCommentClickReplyReply); - $(document).on('click', '[name="dczcomments__op-edit-comment"]', onCommentClickEditComment); - $(document).on('click', '[name="dczcomments__op-edit-reply"]', onCommentClickEditReply); - $(document).on('click', '[name="dczcomments__op-delete-comment"]', onCommentClickDeleteComment); - $(document).on('click', '[name="dczcomments__op-delete-reply"]', onCommentClickDeleteReply); + $(document).on('click', '[data-op="reply"][data-type="comment"]', onCommentClickReplyComment); + $(document).on('click', '[data-op="reply"][data-type="reply"]', onCommentClickReplyReply); + $(document).on('click', '[data-op="edit"][data-type="comment"]', onCommentClickEditComment); + $(document).on('click', '[data-op="edit"][data-type="reply"]', onCommentClickEditReply); + $(document).on('click', '[data-op="delete"][data-type="comment"]', onCommentClickDeleteComment); + $(document).on('click', '[data-op="delete"][data-type="reply"]', onCommentClickDeleteReply); + + $('[data-op="react"]').each((i, e) => { + ReactDOM.render(, e); + }); + $(document).on('click', '.reaction', async (e) => { + const target = $(e.currentTarget); + const res = await request.post('', { + operation: 'reaction', + type: 'drid', + id: target.parent().parent().data('drid'), + emoji: target.text().trim().split(' ')[0], + reverse: target.hasClass('active'), + }); + renderReactions(res.doc?.react, res.sdoc?.react, target.parent()); + }); }); export default commentsPage; diff --git a/packages/ui-default/misc/icons/emoji.svg b/packages/ui-default/misc/icons/emoji.svg new file mode 100644 index 000000000..50ffbc9e7 --- /dev/null +++ b/packages/ui-default/misc/icons/emoji.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 8554bc605..7a38b0f66 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.31.3", + "version": "4.31.4", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/templates/components/comments_discussion.html b/packages/ui-default/templates/components/comments_discussion.html index fe52e0118..9e1ad0dd9 100644 --- a/packages/ui-default/templates/components/comments_discussion.html +++ b/packages/ui-default/templates/components/comments_discussion.html @@ -1,5 +1,14 @@ {% import "components/user.html" as user with context %} +{% macro action(tooltip, operation, type, data, icon) %} + + + +{% endmacro %} + {% macro render( view, docs, udict, comment_ref, @@ -24,7 +33,8 @@ reply_edit_op = 'edit_tail_reply', reply_edit_text = 'Update', comment_delete_op = 'delete_reply', - reply_delete_op = 'delete_tail_reply' + reply_delete_op = 'delete_tail_reply', + reactions = {} ) %}
    {% if handler.user.hasPerm(comment_post_perm) %} @@ -82,30 +92,34 @@
    {% if handler.user.hasPerm(reply_post_perm) %} - + {{ action(_('Reply'), 'reply', 'comment', {operation: reply_post_op, drid: doc._id}, 'reply') }} {% endif %} {% if (handler.user.hasPerm(comment_edit_self_perm) and handler.user.own(doc)) or handler.user.hasPerm(comment_edit_perm) %} - + {{ action(_('Edit'), 'edit', 'comment', {operation: comment_edit_op, drid: doc._id}, 'edit') }} {% endif %} {% if (handler.user.hasPerm(comment_delete_self_perm) and handler.user.own(doc)) or handler.user.hasPerm(comment_delete_perm) %} - + {{ action(_('Delete'), 'delete', 'comment', {operation: comment_delete_op, drid: doc._id}, 'delete') }} + {% endif %} + {% if (handler.user.hasPerm(perm.PERM_ADD_REACTION)) %} + {{ action('', 'react', 'comment', {operation: 'reaction', type:'drid', id: doc._id}, 'emoji') }} {% endif %}
    -
    +
    {{ doc['content']|markdown|safe }} +
    + {% for key in Object.keys(doc.react or {}) %} + {% if doc.react[key] %} +

    {{ key }} {{ doc.react[key] }}

    + {% endif %} + {% endfor %} +
      {%- for rdoc in doc['reply'] -%} {% set udoc = udict[rdoc.owner] %} -
    • +
    • @@ -118,17 +132,13 @@
      {% if handler.user.hasPerm(reply_post_perm) %} - + {{ action(_('Reply'), 'reply', 'reply', {}, 'reply') }} {% endif %} {% if handler.user.own(doc) or handler.user.hasPerm(reply_edit_perm) %} - + {{ action(_('Edit'), 'edit', 'reply', {operation: reply_edit_op, drid: doc._id, drrid: rdoc._id}, 'edit') }} {% endif %} {% if handler.user.own(doc) or handler.user.hasPerm(reply_delete_perm) %} - + {{ action(_('Delete'), 'delete', 'reply', {operation: reply_delete_op, drid: doc._id, drrid: rdoc._id}, 'delete') }} {% endif %}
      diff --git a/packages/ui-default/templates/discussion_detail.html b/packages/ui-default/templates/discussion_detail.html index 26fca335b..e2ec4778d 100644 --- a/packages/ui-default/templates/discussion_detail.html +++ b/packages/ui-default/templates/discussion_detail.html @@ -77,7 +77,8 @@ reply_edit_perm = perm.PERM_NEVER, reply_delete_perm = perm.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION if handler.user.own(ddoc) else perm.PERM_DELETE_DISCUSSION_REPLY, reply_edit_self_perm = perm.PERM_EDIT_DISCUSSION_REPLY_SELF, - reply_delete_self_perm = perm.PERM_DELETE_DISCUSSION_REPLY_SELF + reply_delete_self_perm = perm.PERM_DELETE_DISCUSSION_REPLY_SELF, + reactions = reactions ) }} {{ paginator.render(page, pcount) }} {% if drcount == 0 %}