mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
ui: add discussion reaction (#369)
Co-authored-by: undefined <i@undefined.moe>
This commit is contained in:
1 parent
1fa5eef2e0
commit
dc7d53b59c
7 files changed
+136
-111
No files matched your search
@@ -27,31 +27,6 @@ $downvote-color = red
|
|||||||
|
|
||||||
&:first-child
|
&:first-child
|
||||||
border-top: 0
|
border-top: 0
|
||||||
|
|
||||||
.reactions
|
|
||||||
.reaction
|
|
||||||
white-space: nowrap
|
|
||||||
width: auto
|
|
||||||
border-radius: 1.75rem
|
|
||||||
overflow: visible
|
|
||||||
font-size: rem($font-size-small)
|
|
||||||
display: inline-block
|
|
||||||
line-height: rem(22px)
|
|
||||||
padding: rem(0 10px)
|
|
||||||
vertical-align: middle
|
|
||||||
border-radius: rem(12px)
|
|
||||||
border: 1px solid $supplementary-border-color
|
|
||||||
cursor: pointer
|
|
||||||
&.active, &:hover
|
|
||||||
text-decoration: none
|
|
||||||
border-color: $primary-color
|
|
||||||
color: #FFF !important
|
|
||||||
&.active
|
|
||||||
background-color: lighten($primary-color, 10%) !important
|
|
||||||
&:hover
|
|
||||||
background-color: lighten($primary-color, 30%) !important
|
|
||||||
.emoji
|
|
||||||
margin-right: rem(5px)
|
|
||||||
|
|
||||||
.dczcomments__reply
|
.dczcomments__reply
|
||||||
>.media
|
>.media
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import 'jquery.easing';
|
import 'jquery.easing';
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import { Popover } from '@blueprintjs/core';
|
|
||||||
import { AutoloadPage } from 'vj/misc/Page';
|
import { AutoloadPage } from 'vj/misc/Page';
|
||||||
import CommentBox from 'vj/components/discussion/CommentBox';
|
import CommentBox from 'vj/components/discussion/CommentBox';
|
||||||
import { ConfirmDialog } from 'vj/components/dialog';
|
import { ConfirmDialog } from 'vj/components/dialog';
|
||||||
@@ -12,7 +9,6 @@ import { slideDown, slideUp } from 'vj/utils/slide';
|
|||||||
import request from 'vj/utils/request';
|
import request from 'vj/utils/request';
|
||||||
import i18n from 'vj/utils/i18n';
|
import i18n from 'vj/utils/i18n';
|
||||||
import tpl from 'vj/utils/tpl';
|
import tpl from 'vj/utils/tpl';
|
||||||
import { chunk } from 'lodash';
|
|
||||||
|
|
||||||
const $replyTemplate = $('.commentbox-container').eq(0).clone();
|
const $replyTemplate = $('.commentbox-container').eq(0).clone();
|
||||||
|
|
||||||
@@ -189,62 +185,6 @@ function onCommentClickDeleteReply(ev) {
|
|||||||
onCommentClickDelete('reply', 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' : ''}"><span class="emoji">${key}</span> ${reactions[key]}</div>\n`;
|
|
||||||
}
|
|
||||||
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} small-${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', () => {
|
const commentsPage = new AutoloadPage('commentsPage', () => {
|
||||||
$(document).on('click', '[name="dczcomments__dummy-box"]', onClickDummyBox);
|
$(document).on('click', '[name="dczcomments__dummy-box"]', onClickDummyBox);
|
||||||
$(document).on('click', '[data-op="reply"][data-type="comment"]', onCommentClickReplyComment);
|
$(document).on('click', '[data-op="reply"][data-type="comment"]', onCommentClickReplyComment);
|
||||||
@@ -253,26 +193,6 @@ const commentsPage = new AutoloadPage('commentsPage', () => {
|
|||||||
$(document).on('click', '[data-op="edit"][data-type="reply"]', onCommentClickEditReply);
|
$(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="comment"]', onCommentClickDeleteComment);
|
||||||
$(document).on('click', '[data-op="delete"][data-type="reply"]', onCommentClickDeleteReply);
|
$(document).on('click', '[data-op="delete"][data-type="reply"]', onCommentClickDeleteReply);
|
||||||
|
|
||||||
const canUseReaction = $('[data-op="react"]').length > 0;
|
|
||||||
$('[data-op="react"]').each((i, e) => {
|
|
||||||
ReactDOM.createRoot(e).render(<Reaction payload={$(e).data('form')} ele={$(e).closest('.media__body').find('.reactions')} />);
|
|
||||||
});
|
|
||||||
$(document).on('click', '.reaction', async (e) => {
|
|
||||||
if (!canUseReaction) {
|
|
||||||
(window as any).showSignInDialog();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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;
|
export default commentsPage;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
.reactions
|
||||||
|
display: inline
|
||||||
|
|
||||||
|
.reaction
|
||||||
|
white-space: nowrap
|
||||||
|
width: auto
|
||||||
|
border-radius: 1.75rem
|
||||||
|
overflow: visible
|
||||||
|
font-size: rem($font-size-small)
|
||||||
|
display: inline-block
|
||||||
|
line-height: rem(22px)
|
||||||
|
padding: rem(0 10px)
|
||||||
|
vertical-align: middle
|
||||||
|
border-radius: rem(12px)
|
||||||
|
border: 1px solid $supplementary-border-color
|
||||||
|
cursor: pointer
|
||||||
|
&.active, &:hover
|
||||||
|
text-decoration: none
|
||||||
|
border-color: $primary-color
|
||||||
|
color: #FFF !important
|
||||||
|
&.active
|
||||||
|
background-color: lighten($primary-color, 10%) !important
|
||||||
|
&:hover
|
||||||
|
background-color: lighten($primary-color, 30%) !important
|
||||||
|
.emoji
|
||||||
|
margin-right: rem(5px)
|
||||||
|
|
||||||
|
.popover-reaction-item
|
||||||
|
&:hover
|
||||||
|
background-color: $toolbar-bg-hover
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import 'jquery.easing';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { Popover } from '@blueprintjs/core';
|
||||||
|
import { AutoloadPage } from 'vj/misc/Page';
|
||||||
|
|
||||||
|
import request from 'vj/utils/request';
|
||||||
|
import { chunk } from 'lodash';
|
||||||
|
|
||||||
|
function renderReactions(reactions, self, rootEle) {
|
||||||
|
let html = '';
|
||||||
|
for (const key in reactions) {
|
||||||
|
if (!reactions[key]) continue;
|
||||||
|
html += `<div class="reaction${self[key] ? ' active' : ''}""><span class="emoji">${key}</span> ${reactions[key]}</div>\n`;
|
||||||
|
}
|
||||||
|
rootEle.html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEmojiClick(payload, emoji, ele) {
|
||||||
|
const res = await request.post('', { ...payload, emoji });
|
||||||
|
if (!res.sdoc) return;
|
||||||
|
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} small-${12 / elesPerRow} columns popover-reaction-item`}
|
||||||
|
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 reactionPage = new AutoloadPage('reactionPage', () => {
|
||||||
|
const canUseReaction = $('[data-op="react"]').length > 0;
|
||||||
|
$('[data-op="react"]').each((i, e) => {
|
||||||
|
ReactDOM.createRoot(e).render(
|
||||||
|
<Reaction payload={$(e).data('form')} ele={$(`.reactions[data-${$(e).data('form').type}='${$(e).data('form').id}']`)} />,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
$(document).on('click', '.reaction', async (e) => {
|
||||||
|
if (!canUseReaction) {
|
||||||
|
(window as any).showSignInDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = $(e.currentTarget);
|
||||||
|
const res = await request.post('', {
|
||||||
|
operation: 'reaction',
|
||||||
|
type: target.parent().data('type'),
|
||||||
|
id: target.parent().data(target.parent().data('type')),
|
||||||
|
emoji: target.text().trim().split(' ')[0],
|
||||||
|
reverse: target.hasClass('active'),
|
||||||
|
});
|
||||||
|
renderReactions(res.doc?.react, res.sdoc?.react, target.parent());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default reactionPage;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.37.13",
|
"version": "4.37.14",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "hydro.js",
|
"main": "hydro.js",
|
||||||
|
|||||||
@@ -112,11 +112,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="typo" data-emoji-enabled data-drid="{{ doc.docId }}" 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 }}
|
{{ doc['content']|markdown|safe }}
|
||||||
<div class="reactions list">
|
<div class="reactions list" data-type="drid" data-drid="{{ doc._id }}">
|
||||||
{% for key in Object.keys(doc.react or {}) %}
|
{% for e in Object.entries(doc.react or {})|sort(true, false, 1)|selectattr(1) %}
|
||||||
{% if doc.react[key] %}
|
<div class="reaction{% if reactions[doc.docId][e[0]] %} active{% endif %}"><span class="emoji">{{ e[0] }}</span> {{ e[1] }}</div>
|
||||||
<div class="reaction{% if reactions[doc.docId][key] %} active{% endif %}"><span class="emoji">{{ key }}</span> {{ doc.react[key] }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,6 +59,16 @@
|
|||||||
<li><a href="{{ url('wiki_help', anchor='contact') }}">
|
<li><a href="{{ url('wiki_help', anchor='contact') }}">
|
||||||
<span class="icon icon-warning"></span> {{ _('Report') }}
|
<span class="icon icon-warning"></span> {{ _('Report') }}
|
||||||
</a></li>
|
</a></li>
|
||||||
|
{% if handler.user.hasPerm(perm.PERM_ADD_REACTION) %}
|
||||||
|
<li><a href="javascript:;" data-op="react" data-type="discuss" data-form="{{ {operation: 'reaction', type:'did', id: ddoc._id }|json }}">
|
||||||
|
<span class="icon icon-emoji"></span>
|
||||||
|
</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<div class="reactions list" data-type="did" data-did="{{ ddoc._id }}">
|
||||||
|
{% for e in Object.entries(ddoc.react or {})|sort(true, false, 1)|selectattr(1) %}
|
||||||
|
<div class="reaction{% if reactions[ddoc.docId][e[0]] %} active{% endif %}"><span class="emoji">{{ e[0] }}</span> {{ e[1] }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
|||||||
Reference in new issue
Block a user