mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core: support mention notification
This commit is contained in:
1 parent
d66c161e66
commit
ca00dda575
14 files changed
+67
-24
No files matched your search
@@ -812,6 +812,7 @@ Use admin specificed: 由管理员指定
|
|||||||
Use algorithm calculated: 使用算法计算
|
Use algorithm calculated: 使用算法计算
|
||||||
Use average of above: 使用上面的平均值
|
Use average of above: 使用上面的平均值
|
||||||
User {0} already exists.: 用户 {0} 已存在。
|
User {0} already exists.: 用户 {0} 已存在。
|
||||||
|
User {0} mentioned you in {1:link}: 用户 {0} 在 {1:link} 中提到了你。
|
||||||
User {0} not found.: 用户 {0} 不存在。
|
User {0} not found.: 用户 {0} 不存在。
|
||||||
User can join this domain by visiting the following URL: 用户可以访问此链接来加入此域
|
User can join this domain by visiting the following URL: 用户可以访问此链接来加入此域
|
||||||
User ID: 用户 ID
|
User ID: 用户 ID
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "2.36.13",
|
"version": "2.36.14",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "src/loader",
|
"main": "src/loader",
|
||||||
"module": "src/loader",
|
"module": "src/loader",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import paginate from '../lib/paginate';
|
|||||||
import { PERM, PRIV } from '../model/builtin';
|
import { PERM, PRIV } from '../model/builtin';
|
||||||
import * as discussion from '../model/discussion';
|
import * as discussion from '../model/discussion';
|
||||||
import * as document from '../model/document';
|
import * as document from '../model/document';
|
||||||
|
import message from '../model/message';
|
||||||
import * as oplog from '../model/oplog';
|
import * as oplog from '../model/oplog';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
@@ -224,7 +225,15 @@ class DiscussionDetailHandler extends DiscussionHandler {
|
|||||||
async postReply(domainId: string, did: ObjectID, content: string) {
|
async postReply(domainId: string, did: ObjectID, content: string) {
|
||||||
this.checkPerm(PERM.PERM_REPLY_DISCUSSION);
|
this.checkPerm(PERM.PERM_REPLY_DISCUSSION);
|
||||||
await this.limitRate('add_discussion', 3600, 60);
|
await this.limitRate('add_discussion', 3600, 60);
|
||||||
// Notify related users
|
const targets = new Set(Array.from(content.matchAll(/@\[\]\(\/user\/(\d+)\)/g)).map((i) => +i[1]));
|
||||||
|
const uids = Object.keys(await user.getList(domainId, Array.from(targets))).map((i) => +i);
|
||||||
|
const msg = JSON.stringify({
|
||||||
|
message: 'User {0} mentioned you in {1:link}',
|
||||||
|
params: [this.user.uname, this.request.path],
|
||||||
|
});
|
||||||
|
for (const uid of uids) {
|
||||||
|
message.send(1, uid, msg, message.FLAG_RICHTEXT | message.FLAG_UNREAD);
|
||||||
|
}
|
||||||
await discussion.addReply(domainId, did, this.user._id, content, this.request.ip);
|
await discussion.addReply(domainId, did, this.user._id, content, this.request.ip);
|
||||||
this.back();
|
this.back();
|
||||||
}
|
}
|
||||||
@@ -234,6 +243,15 @@ class DiscussionDetailHandler extends DiscussionHandler {
|
|||||||
async postTailReply(domainId: string, drid: ObjectID, content: string) {
|
async postTailReply(domainId: string, drid: ObjectID, content: string) {
|
||||||
this.checkPerm(PERM.PERM_REPLY_DISCUSSION);
|
this.checkPerm(PERM.PERM_REPLY_DISCUSSION);
|
||||||
await this.limitRate('add_discussion', 3600, 60);
|
await this.limitRate('add_discussion', 3600, 60);
|
||||||
|
const targets = new Set(Array.from(content.matchAll(/@\[\]\(\/user\/(\d+)\)/g)).map((i) => +i[1]));
|
||||||
|
const uids = Object.keys(await user.getList(domainId, Array.from(targets))).map((i) => +i);
|
||||||
|
const msg = JSON.stringify({
|
||||||
|
message: 'User {0} mentioned you in {1:link}',
|
||||||
|
params: [this.user.uname, this.request.path],
|
||||||
|
});
|
||||||
|
for (const uid of uids) {
|
||||||
|
message.send(1, uid, msg, message.FLAG_RICHTEXT | message.FLAG_UNREAD);
|
||||||
|
}
|
||||||
await discussion.addTailReply(domainId, drid, this.user._id, content, this.request.ip);
|
await discussion.addTailReply(domainId, drid, this.user._id, content, this.request.ip);
|
||||||
this.back();
|
this.back();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,12 +368,8 @@ class HomeMessagesHandler extends Handler {
|
|||||||
}
|
}
|
||||||
parsed[target].messages.push(m);
|
parsed[target].messages.push(m);
|
||||||
}
|
}
|
||||||
const path = [
|
|
||||||
['Hydro', 'homepage'],
|
|
||||||
['home_messages', null],
|
|
||||||
];
|
|
||||||
await user.setById(this.user._id, { unreadMsg: 0 });
|
await user.setById(this.user._id, { unreadMsg: 0 });
|
||||||
this.response.body = { messages: parsed, path };
|
this.response.body = { messages: parsed };
|
||||||
this.response.template = 'home_messages.html';
|
this.response.template = 'home_messages.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const coll = db.collection('message');
|
|||||||
class MessageModel {
|
class MessageModel {
|
||||||
static FLAG_UNREAD = 1;
|
static FLAG_UNREAD = 1;
|
||||||
static FLAG_ALERT = 2;
|
static FLAG_ALERT = 2;
|
||||||
|
static FLAG_RICHTEXT = 4;
|
||||||
|
|
||||||
@ArgMethod
|
@ArgMethod
|
||||||
static async send(
|
static async send(
|
||||||
@@ -33,7 +34,7 @@ class MessageModel {
|
|||||||
|
|
||||||
@ArgMethod
|
@ArgMethod
|
||||||
static async getByUser(uid: number): Promise<MessageDoc[]> {
|
static async getByUser(uid: number): Promise<MessageDoc[]> {
|
||||||
return await coll.find({ $or: [{ from: uid }, { to: uid }] }).sort('_id', 1).toArray();
|
return await coll.find({ $or: [{ from: uid }, { to: uid }] }).sort('_id', 1).limit(1000).toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getMany(query: FilterQuery<MessageDoc>, sort: any, page: number, limit: number): Promise<MessageDoc[]> {
|
static async getMany(query: FilterQuery<MessageDoc>, sort: any, page: number, limit: number): Promise<MessageDoc[]> {
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ const messagePage = new AutoloadPage('messagePage', (pagename) => {
|
|||||||
<p>${i18n(msg.mdoc.content)}</p>
|
<p>${i18n(msg.mdoc.content)}</p>
|
||||||
</div>`,
|
</div>`,
|
||||||
}).open();
|
}).open();
|
||||||
|
} else if (msg.udoc._id === 1 && msg.mdoc.flag & 4) {
|
||||||
|
new VjNotification({
|
||||||
|
message: 'You received a system message, click here to view.',
|
||||||
|
duration: 15000,
|
||||||
|
action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'),
|
||||||
|
}).show();
|
||||||
} else {
|
} else {
|
||||||
// Is message
|
// Is message
|
||||||
new VjNotification({
|
new VjNotification({
|
||||||
|
|||||||
@@ -41,11 +41,24 @@ export default connect(mapStateToProps)(class MessagePadDialogueContentContainer
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderContent(msg) {
|
renderContent(msg) {
|
||||||
|
// TODO: FLAG_RICHTEXT
|
||||||
if (msg.from === 1) {
|
if (msg.from === 1) {
|
||||||
// Is system message
|
// Is system message
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(msg.content);
|
const data = JSON.parse(msg.content);
|
||||||
return i18n(data.message, ...data.params);
|
const str = i18n(data.message).replace(/\{([^{}]+)\}/g, (match, key) => `%substitude%${key}%substitude%`);
|
||||||
|
const arr = str.split('%substitude%');
|
||||||
|
data.params = data.params || {};
|
||||||
|
for (let i = 1; i < arr.length; i += 2) {
|
||||||
|
if (arr[i].endsWith(':link')) {
|
||||||
|
const link = data.params[arr[i].split(':link')[0]];
|
||||||
|
if (!link) continue;
|
||||||
|
arr[i] = <a style={{ color: 'wheat' }} href={link} key={i} target="_blank" rel="noreferrer">{link}</a>;
|
||||||
|
} else {
|
||||||
|
arr[i] = <span style={{ color: 'wheat' }} key={i}>{data.params[arr[i]]}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
return i18n(msg.content);
|
return i18n(msg.content);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import _ from 'lodash';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import 'jquery-scroll-lock';
|
import 'jquery-scroll-lock';
|
||||||
|
|
||||||
|
import i18n from 'vj/utils/i18n';
|
||||||
import ListItem from './DialogueListItemComponent';
|
import ListItem from './DialogueListItemComponent';
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
@@ -38,7 +39,12 @@ export default connect(mapStateToProps, mapDispatchToProps)(class MessagePadDial
|
|||||||
<ListItem
|
<ListItem
|
||||||
key={dialogue._id}
|
key={dialogue._id}
|
||||||
userName={dialogue.udoc.uname}
|
userName={dialogue.udoc.uname}
|
||||||
summary={(_.last(dialogue.messages)?.content) || ''}
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
summary={_.last(dialogue.messages)
|
||||||
|
? (_.last(dialogue.messages).flag & 4)
|
||||||
|
? i18n('[Richtext message]')
|
||||||
|
: _.last(dialogue.messages).content
|
||||||
|
: ''}
|
||||||
faceUrl={dialogue.udoc.avatarUrl}
|
faceUrl={dialogue.udoc.avatarUrl}
|
||||||
active={dialogue._id === this.props.activeId}
|
active={dialogue._id === this.props.activeId}
|
||||||
onClick={() => this.props.handleClick(dialogue._id)}
|
onClick={() => this.props.handleClick(dialogue._id)}
|
||||||
|
|||||||
@@ -123,12 +123,10 @@ monaco.editor.registerCommand('hydro.openUserPage', (accesser, uid) => {
|
|||||||
monaco.languages.registerCodeLensProvider('markdown', {
|
monaco.languages.registerCodeLensProvider('markdown', {
|
||||||
async provideCodeLenses(model) {
|
async provideCodeLenses(model) {
|
||||||
const users = model.findMatches('\\[\\]\\(/user/(\\d+)\\)', true, true, true, null, true);
|
const users = model.findMatches('\\[\\]\\(/user/(\\d+)\\)', true, true, true, null, true);
|
||||||
const { data } = await api(`
|
const { data } = await api(gql`
|
||||||
query {
|
users(ids: ${users.map((i) => +i.matches[1])}) {
|
||||||
users(ids: [${users.map((i) => i.matches[1]).join(',')}]) {
|
_id
|
||||||
_id
|
uname
|
||||||
uname
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
return {
|
return {
|
||||||
@@ -172,6 +170,7 @@ monaco.languages.registerCompletionItemProvider('markdown', {
|
|||||||
_id
|
_id
|
||||||
uname
|
uname
|
||||||
avatarUrl
|
avatarUrl
|
||||||
|
priv
|
||||||
}
|
}
|
||||||
`, ['data', 'users']);
|
`, ['data', 'users']);
|
||||||
return {
|
return {
|
||||||
@@ -179,8 +178,9 @@ monaco.languages.registerCompletionItemProvider('markdown', {
|
|||||||
label: `@${i.uname} (UID=${i._id})`,
|
label: `@${i.uname} (UID=${i._id})`,
|
||||||
kind: monaco.languages.CompletionItemKind.Property,
|
kind: monaco.languages.CompletionItemKind.Property,
|
||||||
documentation: { value: ``, isTrusted: true },
|
documentation: { value: ``, isTrusted: true },
|
||||||
insertText: `[](/user/${i._id}) `,
|
insertText: `@[](/user/${i._id}) `,
|
||||||
range,
|
range,
|
||||||
|
sortText: i.priv === 0 ? '0' : '1',
|
||||||
tags: i.priv === 0 ? [1] : [],
|
tags: i.priv === 0 ? [1] : [],
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
__langname: 简体中文
|
__langname: 简体中文
|
||||||
'"{data}" copied to clipboard!': “{data}”已复制到剪贴板!
|
'"{data}" copied to clipboard!': “{data}”已复制到剪贴板!
|
||||||
|
'[Richtext message]': '[富文本消息]'
|
||||||
'{} is a moderator of this domain.': '{}是这个域的管理者之一。'
|
'{} is a moderator of this domain.': '{}是这个域的管理者之一。'
|
||||||
'{0} comments': '{0} 条评论'
|
'{0} comments': '{0} 条评论'
|
||||||
'{0} discussions': '{0} 条讨论'
|
'{0} discussions': '{0} 条讨论'
|
||||||
@@ -873,6 +874,7 @@ You have uncompleted domain creation operation. You can click "Continue" to fini
|
|||||||
You haven't attended this contest yet.: 您还没有参加本次比赛。
|
You haven't attended this contest yet.: 您还没有参加本次比赛。
|
||||||
You haven't claimed this homework yet.: 您还未认领过该作业。
|
You haven't claimed this homework yet.: 您还未认领过该作业。
|
||||||
You need to enter the invitation code to join the domain.: 您需要输入邀请码来加入此域。
|
You need to enter the invitation code to join the domain.: 您需要输入邀请码来加入此域。
|
||||||
|
You received a system message, click here to view.: 您收到一条系统通知,点击查看。
|
||||||
You're not logged in.: 您没有登录。
|
You're not logged in.: 您没有登录。
|
||||||
You've already attended this contest.: 您已经参加本次比赛。
|
You've already attended this contest.: 您已经参加本次比赛。
|
||||||
You've already claimed this homework.: 您已认领过该作业。
|
You've already claimed this homework.: 您已认领过该作业。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.21.11",
|
"version": "4.22.0",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "hydro.js",
|
"main": "hydro.js",
|
||||||
|
|||||||
@@ -44,7 +44,10 @@ const page = new NamedPage('home_messages', () => {
|
|||||||
avatar: msg.udoc.avatarUrl,
|
avatar: msg.udoc.avatarUrl,
|
||||||
message: msg.mdoc.content,
|
message: msg.mdoc.content,
|
||||||
duration: 15000,
|
duration: 15000,
|
||||||
action: () => createDialog(msg.udoc),
|
action: () => store.dispatch({
|
||||||
|
type: 'DIALOGUES_SWITCH_TO',
|
||||||
|
payload: msg.udoc._id,
|
||||||
|
}),
|
||||||
}).show();
|
}).show();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default async (q: string, path: string[] = []) => {
|
|||||||
|
|
||||||
export const gql = (
|
export const gql = (
|
||||||
pieces: TemplateStringsArray,
|
pieces: TemplateStringsArray,
|
||||||
...templates: (string | number)[]
|
...templates: (string | number | string[] | number[])[]
|
||||||
) => {
|
) => {
|
||||||
let res = '';
|
let res = '';
|
||||||
for (let i = 0; i < pieces.length; i++) {
|
for (let i = 0; i < pieces.length; i++) {
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import substitute from './substitute';
|
import substitute from './substitute';
|
||||||
|
|
||||||
export default function i18n(str, ...params) {
|
export default function i18n(str, ...params) {
|
||||||
if (window.LOCALES && window.LOCALES[str]) {
|
return substitute(window.LOCALES?.[str] || str, params);
|
||||||
return substitute(window.LOCALES[str], params);
|
|
||||||
}
|
|
||||||
return substitute(str, params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Hydro.utils.i18n = i18n;
|
window.Hydro.utils.i18n = i18n;
|
||||||
Reference in new issue
Block a user