mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core&ui: support reaction
This commit is contained in:
1 parent
48565ce711
commit
913658d144
12 files changed
+180
-37
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Discussio
|
||||
}
|
||||
|
||||
export async function react(domainId: string, docType: keyof document.DocType, did: ObjectID, id: string, uid: number, reverse = false) {
|
||||
const updated = await document.setIfNotStatus(domainId, docType, did, uid, `react.${id}`, reverse ? 0 : 1, reverse ? 0 : 1, {});
|
||||
if (updated) await document.inc(domainId, docType, did, `react.${id}`, reverse ? -1 : 1);
|
||||
let doc;
|
||||
const sdoc = await document.setIfNotStatus(domainId, docType, did, uid, `react.${id}`, reverse ? 0 : 1, reverse ? 0 : 1, {});
|
||||
if (sdoc) doc = await document.inc(domainId, docType, did, `react.${id}`, reverse ? -1 : 1);
|
||||
else doc = await document.get(domainId, docType, did, ['react']);
|
||||
return [doc, sdoc];
|
||||
}
|
||||
|
||||
export async function getReaction(domainId: string, docType: keyof document.DocType, did: ObjectID, uid: number) {
|
||||
const doc = await document.getStatus(domainId, docType, did, uid);
|
||||
return doc?.react || {};
|
||||
}
|
||||
|
||||
export async function addTailReply(
|
||||
@@ -357,6 +365,7 @@ global.Hydro.model.discussion = {
|
||||
editTailReply,
|
||||
delTailReply,
|
||||
react,
|
||||
getReaction,
|
||||
setStar,
|
||||
getStatus,
|
||||
setStatus,
|
||||
|
||||
@@ -292,7 +292,7 @@ export class ProblemModel {
|
||||
const q: any = { docId: { $in: pids } };
|
||||
let pdocs = await document.getMulti(domainId, document.TYPE_PROBLEM, q)
|
||||
.project(buildProjection(projection)).toArray();
|
||||
if (group.length > 0) {
|
||||
if (group.length > 0 && canViewHidden !== true) {
|
||||
pdocs = pdocs.filter((i) => !i.assign?.length || Set.intersection(group, i.assign).size);
|
||||
}
|
||||
if (canViewHidden !== true) {
|
||||
|
||||
@@ -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<Type | boolean | Validator | Converter>) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
+82
-7
@@ -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 += `<div class="reaction${self[key] ? ' active' : ''}"><p>${key} ${reactions[key]}</p></div>`;
|
||||
}
|
||||
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
|
||||
<Popover usePortal interactionKind="hover" isOpen={finish ? false : (focus ? true : undefined)}>
|
||||
<span className="icon icon-emoji"></span>
|
||||
<div>
|
||||
{chunk(emojiList, elesPerRow).map((line, i) => (
|
||||
<div className="row" key={+i} style={{ paddingBottom: 4, paddingTop: 4 }}>
|
||||
{line.map((emoji) => (
|
||||
<div
|
||||
key={emoji}
|
||||
className={`medium-${12 / elesPerRow} columns`}
|
||||
onClick={() => handleEmojiClick(payload, emoji, ele).then(() => updateFinish(true))}
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<div className="row" style={{ paddingTop: 7, paddingBottom: 4 }}>
|
||||
<div className="medium-12 columns">
|
||||
<input name="emojiSuggest" onFocus={() => updateFocus(true)} onBlur={() => updateFocus(false)}></input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
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(<Reaction payload={$(e).data('form')} ele={$(e).closest('.media__body').find('.reactions')} />, 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;
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><path d="M475 0c262.34 0 475 212.66 475 475S737.34 950 475 950 0 737.34 0 475 212.66 0 475 0zm0 95C265.13 95 95 265.13 95 475s170.13 380 380 380 380-170.13 380-380S684.87 95 475 95zM285 475l.71 10.22C295.38 589.93 376.43 665 475 665c101.75 0 184.83-80 189.77-180.52L665 475h95l-.6 11.31C748.34 643.74 625.25 760 475 760c-153.82 0-279.18-121.86-284.8-274.32L190 475zm71.25-190A71.25 71.25 0 11285 356.25 71.25 71.25 0 01356.25 285zm237.5 0a71.25 71.25 0 11-71.25 71.25A71.25 71.25 0 01593.75 285z"/></svg>
|
||||
|
After Width: | Height: | Size: 568 B |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.31.3",
|
||||
"version": "4.31.4",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "hydro.js",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{% import "components/user.html" as user with context %}
|
||||
|
||||
{% macro action(tooltip, operation, type, data, icon) %}
|
||||
<a href="javascript:;"
|
||||
{% if tooltip %} data-tooltip="{{ tooltip }}"{% endif %}
|
||||
data-op="{{ operation }}" data-type="{{ type }}"
|
||||
{% if data %} data-form="{{ data|json }}"{% endif %}>
|
||||
<span class="icon icon-{{ icon }}"></span>
|
||||
</a>
|
||||
{% 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 = {}
|
||||
) %}
|
||||
<ul class="dczcomments__list view--discussion">
|
||||
{% if handler.user.hasPerm(comment_post_perm) %}
|
||||
@@ -82,30 +92,34 @@
|
||||
</div>
|
||||
<div class="dczcomments__operations nojs--hide">
|
||||
{% if handler.user.hasPerm(reply_post_perm) %}
|
||||
<a href="javascript:;" data-tooltip="{{ _('Reply') }}" name="dczcomments__op-reply-comment"
|
||||
data-form="{{ {operation: reply_post_op, drid: doc._id}|json }}"
|
||||
><span class="icon icon-reply"></span></a>
|
||||
{{ 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) %}
|
||||
<a href="javascript:;" data-tooltip="{{ _('Edit') }}" name="dczcomments__op-edit-comment"
|
||||
data-form="{{ {operation: comment_edit_op, drid: doc._id}|json }}"
|
||||
><span class="icon icon-edit"></span></a>
|
||||
{{ 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) %}
|
||||
<a href="javascript:;" data-tooltip="{{ _('Delete') }}" name="dczcomments__op-delete-comment"
|
||||
data-form="{{ {operation: comment_delete_op, drid: doc._id}|json }}"
|
||||
><span class="icon icon-delete"></span></a>
|
||||
{{ 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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="typo" data-emoji-enabled data-raw-url="{{ url('discussion_reply_raw', did=doc.parentId, drid=doc._id) }}">
|
||||
<div class="typo" data-emoji-enabled data-drid="{{ doc.docId }}" data-raw-url="{{ url('discussion_reply_raw', did=doc.parentId, drid=doc._id) }}">
|
||||
{{ doc['content']|markdown|safe }}
|
||||
<div class="reactions row">
|
||||
{% for key in Object.keys(doc.react or {}) %}
|
||||
{% if doc.react[key] %}
|
||||
<div class="reaction{% if reactions[doc.docId][key] %} active{% endif %}"><p>{{ key }} {{ doc.react[key] }}</p></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="commentbox-edit-target"></div>
|
||||
<ul class="dczcomments__replies commentbox-reply-target">
|
||||
{%- for rdoc in doc['reply'] -%}
|
||||
{% set udoc = udict[rdoc.owner] %}
|
||||
<li class="dczcomments__reply{% if mode_create %} commentbox-container{% endif %}">
|
||||
<li class="dczcomments__reply">
|
||||
<div class="media">
|
||||
<div class="media__left top">
|
||||
<img src="{{ avatarUrl(udoc.avatar, 50) }}" width="50" height="50" class="medium user-profile-avatar">
|
||||
@@ -118,17 +132,13 @@
|
||||
</div>
|
||||
<div class="dczcomments__operations nojs--hide">
|
||||
{% if handler.user.hasPerm(reply_post_perm) %}
|
||||
<a href="javascript:;" data-tooltip="{{ _('Reply') }}" name="dczcomments__op-reply-reply"><span class="icon icon-reply"></span></a>
|
||||
{{ action(_('Reply'), 'reply', 'reply', {}, 'reply') }}
|
||||
{% endif %}
|
||||
{% if handler.user.own(doc) or handler.user.hasPerm(reply_edit_perm) %}
|
||||
<a href="javascript:;" data-tooltip="{{ _('Edit') }}" name="dczcomments__op-edit-reply"
|
||||
data-form="{{ {operation: reply_edit_op, drid: doc._id, drrid: rdoc._id}|json }}"
|
||||
><span class="icon icon-edit"></span></a>
|
||||
{{ 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) %}
|
||||
<a href="javascript:;" data-tooltip="{{ _('Delete') }}" name="dczcomments__op-delete-reply"
|
||||
data-form="{{ {operation: reply_delete_op, drid: doc._id, drrid: rdoc._id}|json }}"
|
||||
><span class="icon icon-delete"></span></a>
|
||||
{{ action(_('Delete'), 'delete', 'reply', {operation: reply_delete_op, drid: doc._id, drrid: rdoc._id}, 'delete') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in new issue
Block a user