diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index a9cd1c81b..4bb7f3da0 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -437,6 +437,7 @@ List View: 列表视图 Live: 正在进行 Live...: 正在进行… Location: 位置 +Lock Topic: 锁定主题 Login to Attend Contest: 登录后参加比赛 Login to Claim Homework: 登录后认领作业 Login to Create a Discussion: 登录后创建讨论 @@ -750,6 +751,7 @@ Terms of Service: 服务条款 Test data comes from: 测试数据来自 Testdata Config: 测试数据配置 Text: 文本 +The discussion is locked, you can not reply anymore.: 此讨论已经锁定,您不能再回复。 The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 作业已超过截止时间,但仍在延期时间内。您递交题目将无法获得全部分数。 The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.: 加入此域的邀请码。您只能使用字母和数字,并且不能长于 64 个字符。 The invitation code you provided is invalid.: 您提供的邀请码无效。 @@ -800,6 +802,7 @@ Unhide Selected: 取消隐藏选中 Unique: 唯一 Unknown field {0}.: 未知字段 {0}。 Unknown: 未知 +Unlock Topic: 解锁主题 Unordered List: 无序列表 Unsaved session expire seconds: 未保存的会话的过期秒数 Unstar Topic: 取消收藏 diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 0374718f9..def6ca832 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "3.4.24", + "version": "3.4.25", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", diff --git a/packages/hydrooj/src/error.ts b/packages/hydrooj/src/error.ts index 44d764584..1802cb1b7 100644 --- a/packages/hydrooj/src/error.ts +++ b/packages/hydrooj/src/error.ts @@ -98,6 +98,7 @@ export const DomainJoinForbiddenError = Err('DomainJoinForbiddenError', Forbidde export const DomainJoinAlreadyMemberError = Err('DomainJoinAlreadyMemberError', ForbiddenError, 'Failed to join the domain. You are already a member.'); export const InvalidJoinInvitationCodeError = Err('InvalidJoinInvitationCodeError', ForbiddenError, 'The invitation code you provided is invalid.'); export const CurrentPasswordError = Err('CurrentPasswordError', ForbiddenError, "Current password doesn't match."); +export const DiscussionLockedError = Err('DiscussionLockedError', ForbiddenError, 'The discussion is locked, you can not reply anymore.'); export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.'); export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.'); diff --git a/packages/hydrooj/src/handler/discussion.ts b/packages/hydrooj/src/handler/discussion.ts index 08d0a5783..133baff12 100644 --- a/packages/hydrooj/src/handler/discussion.ts +++ b/packages/hydrooj/src/handler/discussion.ts @@ -1,6 +1,9 @@ import { isSafeInteger } from 'lodash'; import { ObjectID } from 'mongodb'; -import { DiscussionNotFoundError, DocumentNotFoundError, PermissionError } from '../error'; +import { + DiscussionLockedError, DiscussionNotFoundError, DocumentNotFoundError, + PermissionError, +} from '../error'; import { DiscussionDoc, DiscussionReplyDoc, DiscussionTailReplyDoc } from '../interface'; import paginate from '../lib/paginate'; import { PERM, PRIV } from '../model/builtin'; @@ -207,6 +210,14 @@ class DiscussionDetailHandler extends DiscussionHandler { this.checkPriv(PRIV.PRIV_USER_PROFILE); } + @param('did', Types.ObjectID) + @param('lock', Types.Boolean) + async postSetLock(domainId: string, did: ObjectID, lock: boolean) { + if (!this.user.own(this.ddoc)) this.checkPerm(PERM.PERM_LOCK_DISCUSSION); + await discussion.edit(domainId, did, { lock }); + this.back(); + } + @param('type', Types.Range(['did', 'drid'])) @param('id', Types.ObjectID) @param('emoji', Types.Emoji) @@ -223,6 +234,7 @@ class DiscussionDetailHandler extends DiscussionHandler { @param('content', Types.Content) async postReply(domainId: string, did: ObjectID, content: string) { this.checkPerm(PERM.PERM_REPLY_DISCUSSION); + if (this.ddoc.lock) throw new DiscussionLockedError(domainId, did); 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); @@ -241,6 +253,7 @@ class DiscussionDetailHandler extends DiscussionHandler { @param('content', Types.Content) async postTailReply(domainId: string, drid: ObjectID, content: string) { this.checkPerm(PERM.PERM_REPLY_DISCUSSION); + if (this.ddoc.lock) throw new DiscussionLockedError(domainId, this.ddoc.docId); 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); @@ -355,7 +368,9 @@ class DiscussionEditHandler extends DiscussionHandler { if (!this.user.hasPerm(PERM.PERM_HIGHLIGHT_DISCUSSION)) highlight = this.ddoc.highlight; if (!this.user.hasPerm(PERM.PERM_PIN_DISCUSSION)) pin = this.ddoc.pin; await Promise.all([ - discussion.edit(domainId, did, title, content, highlight, pin), + discussion.edit(domainId, did, { + title, content, highlight, pin, + }), oplog.log(this, 'discussion.edit', this.ddoc), ]); this.response.body = { did }; diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 8571cbf0d..d64fe694b 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -435,6 +435,7 @@ declare module './model/discussion' { react: Record; sort: number; lastRCount: number; + lock?: boolean; } } diff --git a/packages/hydrooj/src/model/builtin.ts b/packages/hydrooj/src/model/builtin.ts index 8807bda44..a54039fe2 100644 --- a/packages/hydrooj/src/model/builtin.ts +++ b/packages/hydrooj/src/model/builtin.ts @@ -58,6 +58,7 @@ export const PERM = { PERM_DELETE_DISCUSSION_REPLY: 1n << 38n, PERM_DELETE_DISCUSSION_REPLY_SELF: 1n << 39n, PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 40n, + PERM_LOCK_DISCUSSION: 64n, // Contest PERM_VIEW_CONTEST: 1n << 41n, @@ -131,6 +132,7 @@ export const PERMS = [ Permission('perm_discussion', PERM.PERM_PIN_DISCUSSION, 'Pin discussions'), Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION, 'Edit discussions'), Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_SELF, 'Edit own discussions'), + Permission('perm_discussion', PERM.PERM_LOCK_DISCUSSION, 'Lock discussions'), Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION, 'Delete discussions'), Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_SELF, 'Delete own discussions'), Permission('perm_discussion', PERM.PERM_REPLY_DISCUSSION, 'Reply discussions'), diff --git a/packages/hydrooj/src/model/discussion.ts b/packages/hydrooj/src/model/discussion.ts index b616da22c..f30939123 100644 --- a/packages/hydrooj/src/model/discussion.ts +++ b/packages/hydrooj/src/model/discussion.ts @@ -57,6 +57,7 @@ export const PROJECTION_LIST: DiscussionDoc.Field[] = [ ]; export const PROJECTION_PUBLIC: DiscussionDoc.Field[] = [ ...PROJECTION_LIST, 'content', 'history', 'react', 'maintainer', + 'lock', ]; export const typeDisplay = { @@ -103,14 +104,8 @@ export async function get( return await document.get(domainId, document.TYPE_DISCUSSION, did, projection); } -export function edit( - domainId: string, did: ObjectID, - title: string, content: string, highlight: boolean, pin: boolean, -): Promise { - const payload = { - title, content, highlight, pin, - }; - return document.set(domainId, document.TYPE_DISCUSSION, did, payload); +export function edit(domainId: string, did: ObjectID, $set: Partial) { + return document.set(domainId, document.TYPE_DISCUSSION, did, $set); } export function inc( diff --git a/packages/hydrooj/src/model/message.ts b/packages/hydrooj/src/model/message.ts index 51684079f..26a8ce57b 100644 --- a/packages/hydrooj/src/model/message.ts +++ b/packages/hydrooj/src/model/message.ts @@ -26,7 +26,7 @@ class MessageModel { from, to, content, _id: res.insertedId, flag, }; if (from !== to) bus.broadcast('user/message', to, mdoc); - await user.inc(to, 'unreadMsg', 1); + if (flag & MessageModel.FLAG_UNREAD) await user.inc(to, 'unreadMsg', 1); return mdoc; } diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 056981b29..f873e0488 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.31.13", + "version": "4.31.14", "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 4d760ccba..8dd20b4cd 100644 --- a/packages/ui-default/templates/components/comments_discussion.html +++ b/packages/ui-default/templates/components/comments_discussion.html @@ -34,7 +34,8 @@ reply_edit_text = 'Update', comment_delete_op = 'delete_reply', reply_delete_op = 'delete_tail_reply', - reactions = {} + reactions = {}, + ddoc = {} ) %}
    {% if handler.user.hasPerm(comment_post_perm) %} @@ -44,6 +45,9 @@
    + {% if ddoc.lock %} + + {% else %}
    @@ -73,6 +77,7 @@
+ {% endif %} diff --git a/packages/ui-default/templates/discussion_detail.html b/packages/ui-default/templates/discussion_detail.html index e2ec4778d..8185ab2cd 100644 --- a/packages/ui-default/templates/discussion_detail.html +++ b/packages/ui-default/templates/discussion_detail.html @@ -45,6 +45,16 @@ {{ _('Unstar Topic') }} + {% endif %} + {% if handler.user.own(ddoc) or handler.user.hasPerm(perm.PERM_LOCK_DISCUSSION) %} +
  • + + {% if not ddoc.lock %}{% endif %} + +
  • {% endif %}
  • {{ _('Report') }} @@ -78,7 +88,8 @@ 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, - reactions = reactions + reactions = reactions, + ddoc = ddoc ) }} {{ paginator.render(page, pcount) }} {% if drcount == 0 %} @@ -97,9 +108,7 @@
    -

    - {{ user.render_inline(udoc, avatar=false, modbadge=false) }} -

    +

    {{ user.render_inline(udoc, avatar=false, modbadge=false) }}