mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core&ui: add discussion history (#450)
Co-authored-by: 无限UCW <45730483+wuxianucw@users.noreply.github.com> Co-authored-by: undefined <i@undefined.moe>
This commit is contained in:
14 files changed
+227
-45
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "4.2.12",
|
"version": "4.2.13",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "src/plugin-api",
|
"main": "src/plugin-api",
|
||||||
"module": "src/plugin-api",
|
"module": "src/plugin-api",
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
|
|||||||
this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
|
this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
|
||||||
if (!this.user.own(this.drdoc)) throw new PermissionError(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
|
if (!this.user.own(this.drdoc)) throw new PermissionError(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
discussion.editReply(domainId, drid, content),
|
discussion.editReply(domainId, drid, content, this.user._id, this.request.ip),
|
||||||
oplog.log(this, 'discussion.reply.edit', this.drdoc),
|
oplog.log(this, 'discussion.reply.edit', this.drdoc),
|
||||||
]);
|
]);
|
||||||
this.back();
|
this.back();
|
||||||
@@ -312,7 +312,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
|
|||||||
this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
|
this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
|
||||||
if (!this.user.own(this.drrdoc)) throw new PermissionError(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
|
if (!this.user.own(this.drrdoc)) throw new PermissionError(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
discussion.editTailReply(domainId, drid, drrid, content),
|
discussion.editTailReply(domainId, drid, drrid, content, this.user._id, this.request.ip),
|
||||||
oplog.log(this, 'discussion.tailReply.edit', this.drrdoc),
|
oplog.log(this, 'discussion.tailReply.edit', this.drrdoc),
|
||||||
]);
|
]);
|
||||||
this.back();
|
this.back();
|
||||||
@@ -354,11 +354,20 @@ class DiscussionDetailHandler extends DiscussionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DiscussionRawHandler extends DiscussionHandler {
|
class DiscussionRawHandler extends DiscussionHandler {
|
||||||
|
@param('did', Types.ObjectID, true)
|
||||||
@param('drid', Types.ObjectID, true)
|
@param('drid', Types.ObjectID, true)
|
||||||
@param('drrid', Types.ObjectID, true)
|
@param('drrid', Types.ObjectID, true)
|
||||||
async get(domainId: string, drid: ObjectID, drrid: ObjectID) {
|
@param('time', Types.UnsignedInt, true)
|
||||||
this.response.type = 'text/markdown';
|
@param('all', Types.Boolean, true)
|
||||||
this.response.body = drrid ? this.drrdoc.content : drid ? this.drdoc.content : this.ddoc.content;
|
async get(domainId: string, did: ObjectID, drid: ObjectID, drrid: ObjectID, ts: number, all = false) {
|
||||||
|
if (all) {
|
||||||
|
this.response.body.history = await discussion.getHistory(domainId, drrid || drid || did);
|
||||||
|
} else {
|
||||||
|
const [doc] = await discussion.getHistory(domainId, drrid || drid || did, ts ? { time: new Date(ts) } : {});
|
||||||
|
if (!doc && ts) throw new DiscussionNotFoundError(drrid || drid || did);
|
||||||
|
this.response.type = 'text/markdown';
|
||||||
|
this.response.body = doc ? doc.content : drrid ? this.drrdoc.content : drid ? this.drdoc.content : this.ddoc.content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +399,7 @@ class DiscussionEditHandler extends DiscussionHandler {
|
|||||||
if (!this.user.hasPerm(PERM.PERM_PIN_DISCUSSION)) pin = this.ddoc.pin;
|
if (!this.user.hasPerm(PERM.PERM_PIN_DISCUSSION)) pin = this.ddoc.pin;
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
discussion.edit(domainId, did, {
|
discussion.edit(domainId, did, {
|
||||||
title, content, highlight, pin,
|
title, highlight, pin, content, editor: this.user._id, edited: true,
|
||||||
}),
|
}),
|
||||||
oplog.log(this, 'discussion.edit', this.ddoc),
|
oplog.log(this, 'discussion.edit', this.ddoc),
|
||||||
]);
|
]);
|
||||||
@@ -427,7 +436,7 @@ export async function apply(ctx) {
|
|||||||
ctx.Route('discussion_main', '/discuss', DiscussionMainHandler);
|
ctx.Route('discussion_main', '/discuss', DiscussionMainHandler);
|
||||||
ctx.Route('discussion_detail', '/discuss/:did', DiscussionDetailHandler);
|
ctx.Route('discussion_detail', '/discuss/:did', DiscussionDetailHandler);
|
||||||
ctx.Route('discussion_edit', '/discuss/:did/edit', DiscussionEditHandler);
|
ctx.Route('discussion_edit', '/discuss/:did/edit', DiscussionEditHandler);
|
||||||
ctx.Route('discussion_detail', '/discuss/:did/raw', DiscussionRawHandler);
|
ctx.Route('discussion_raw', '/discuss/:did/raw', DiscussionRawHandler);
|
||||||
ctx.Route('discussion_reply_raw', '/discuss/:did/:drid/raw', DiscussionRawHandler);
|
ctx.Route('discussion_reply_raw', '/discuss/:did/:drid/raw', DiscussionRawHandler);
|
||||||
ctx.Route('discussion_tail_reply_raw', '/discuss/:did/:drid/:drrid/raw', DiscussionRawHandler);
|
ctx.Route('discussion_tail_reply_raw', '/discuss/:did/:drid/:drrid/raw', DiscussionRawHandler);
|
||||||
ctx.Route('discussion_node', '/discuss/:type/:name', DiscussionNodeHandler);
|
ctx.Route('discussion_node', '/discuss/:type/:name', DiscussionNodeHandler);
|
||||||
|
|||||||
@@ -405,11 +405,6 @@ export interface BlacklistDoc {
|
|||||||
expireAt: Date;
|
expireAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoryDoc {
|
|
||||||
content: string;
|
|
||||||
time: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discussion
|
// Discussion
|
||||||
export type { DiscussionDoc } from './model/discussion';
|
export type { DiscussionDoc } from './model/discussion';
|
||||||
declare module './model/discussion' {
|
declare module './model/discussion' {
|
||||||
@@ -426,7 +421,8 @@ declare module './model/discussion' {
|
|||||||
updateAt: Date;
|
updateAt: Date;
|
||||||
nReply: number;
|
nReply: number;
|
||||||
views: number;
|
views: number;
|
||||||
history: HistoryDoc[];
|
edited?: boolean;
|
||||||
|
editor?: number;
|
||||||
react: Record<string, number>;
|
react: Record<string, number>;
|
||||||
sort: number;
|
sort: number;
|
||||||
lastRCount: number;
|
lastRCount: number;
|
||||||
@@ -442,16 +438,18 @@ export interface DiscussionReplyDoc extends Document {
|
|||||||
ip: string;
|
ip: string;
|
||||||
content: string;
|
content: string;
|
||||||
reply: DiscussionTailReplyDoc[];
|
reply: DiscussionTailReplyDoc[];
|
||||||
history: HistoryDoc[];
|
edited?: boolean;
|
||||||
|
editor?: number;
|
||||||
react: Record<string, number>;
|
react: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DiscussionTailReplyDoc {
|
export interface DiscussionTailReplyDoc {
|
||||||
_id: ObjectID,
|
_id: ObjectID;
|
||||||
owner: number,
|
owner: number;
|
||||||
content: string,
|
content: string;
|
||||||
ip: string,
|
ip: string;
|
||||||
history: HistoryDoc[],
|
edited?: boolean;
|
||||||
|
editor?:number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BlogDoc {
|
export interface BlogDoc {
|
||||||
@@ -602,6 +600,17 @@ export interface OauthMap {
|
|||||||
uid: number;
|
uid: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DiscussionHistoryDoc {
|
||||||
|
title?: string;
|
||||||
|
content: string;
|
||||||
|
domainId: string;
|
||||||
|
docId: ObjectID;
|
||||||
|
/** Create time */
|
||||||
|
time: Date;
|
||||||
|
uid: number;
|
||||||
|
ip: string;
|
||||||
|
}
|
||||||
|
|
||||||
declare module './service/db' {
|
declare module './service/db' {
|
||||||
interface Collections {
|
interface Collections {
|
||||||
'blacklist': BlacklistDoc;
|
'blacklist': BlacklistDoc;
|
||||||
@@ -610,6 +619,7 @@ declare module './service/db' {
|
|||||||
'record': RecordDoc;
|
'record': RecordDoc;
|
||||||
'document': any;
|
'document': any;
|
||||||
'document.status': StatusDocBase;
|
'document.status': StatusDocBase;
|
||||||
|
'discussion.history': DiscussionHistoryDoc;
|
||||||
'user': Udoc;
|
'user': Udoc;
|
||||||
'user.preference': UserPreferenceDoc;
|
'user.preference': UserPreferenceDoc;
|
||||||
'vuser': VUdoc;
|
'vuser': VUdoc;
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import moment from 'moment';
|
|||||||
import { FilterQuery, ObjectID } from 'mongodb';
|
import { FilterQuery, ObjectID } from 'mongodb';
|
||||||
import { Context } from '../context';
|
import { Context } from '../context';
|
||||||
import { DiscussionNodeNotFoundError, DocumentNotFoundError } from '../error';
|
import { DiscussionNodeNotFoundError, DocumentNotFoundError } from '../error';
|
||||||
import { DiscussionReplyDoc, DiscussionTailReplyDoc, Document } from '../interface';
|
import {
|
||||||
|
DiscussionHistoryDoc, DiscussionReplyDoc, DiscussionTailReplyDoc, Document,
|
||||||
|
} from '../interface';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
|
import db from '../service/db';
|
||||||
import { NumberKeys } from '../typeutils';
|
import { NumberKeys } from '../typeutils';
|
||||||
import { buildProjection } from '../utils';
|
import { buildProjection } from '../utils';
|
||||||
import * as contest from './contest';
|
import * as contest from './contest';
|
||||||
@@ -22,9 +25,12 @@ export const PROJECTION_LIST: Field[] = [
|
|||||||
'parentId', 'parentType', 'title',
|
'parentId', 'parentType', 'title',
|
||||||
];
|
];
|
||||||
export const PROJECTION_PUBLIC: Field[] = [
|
export const PROJECTION_PUBLIC: Field[] = [
|
||||||
...PROJECTION_LIST, 'content', 'history', 'react', 'maintainer',
|
...PROJECTION_LIST, 'content', 'edited', 'react', 'maintainer',
|
||||||
'lock',
|
'lock',
|
||||||
];
|
];
|
||||||
|
export const HISTORY_PROJECTION_PUBLIC: (keyof DiscussionHistoryDoc)[] = [
|
||||||
|
'title', 'content', 'docId', 'uid', 'time',
|
||||||
|
];
|
||||||
|
|
||||||
export const typeDisplay = {
|
export const typeDisplay = {
|
||||||
[document.TYPE_PROBLEM]: 'problem',
|
[document.TYPE_PROBLEM]: 'problem',
|
||||||
@@ -34,15 +40,19 @@ export const typeDisplay = {
|
|||||||
[document.TYPE_HOMEWORK]: 'homework',
|
[document.TYPE_HOMEWORK]: 'homework',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const coll = db.collection('discussion.history');
|
||||||
|
|
||||||
export async function add(
|
export async function add(
|
||||||
domainId: string, parentType: number, parentId: ObjectID | number | string,
|
domainId: string, parentType: number, parentId: ObjectID | number | string,
|
||||||
owner: number, title: string, content: string,
|
owner: number, title: string, content: string,
|
||||||
ip: string | null = null, highlight: boolean, pin: boolean,
|
ip: string | null = null, highlight: boolean, pin: boolean,
|
||||||
): Promise<ObjectID> {
|
): Promise<ObjectID> {
|
||||||
|
const time = new Date();
|
||||||
const payload: Partial<DiscussionDoc> = {
|
const payload: Partial<DiscussionDoc> = {
|
||||||
domainId,
|
domainId,
|
||||||
content,
|
content,
|
||||||
owner,
|
owner,
|
||||||
|
editor: owner,
|
||||||
parentType,
|
parentType,
|
||||||
parentId,
|
parentId,
|
||||||
title,
|
title,
|
||||||
@@ -50,7 +60,7 @@ export async function add(
|
|||||||
nReply: 0,
|
nReply: 0,
|
||||||
highlight,
|
highlight,
|
||||||
pin,
|
pin,
|
||||||
updateAt: new Date(),
|
updateAt: time,
|
||||||
views: 0,
|
views: 0,
|
||||||
sort: 100,
|
sort: 100,
|
||||||
};
|
};
|
||||||
@@ -70,7 +80,10 @@ export async function get<T extends Field>(
|
|||||||
return await document.get(domainId, document.TYPE_DISCUSSION, did, projection);
|
return await document.get(domainId, document.TYPE_DISCUSSION, did, projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function edit(domainId: string, did: ObjectID, $set: Partial<DiscussionDoc>) {
|
export async function edit(domainId: string, did: ObjectID, $set: Partial<DiscussionDoc>) {
|
||||||
|
await coll.insertOne({
|
||||||
|
domainId, docId: did, content: $set.content, uid: $set.editor, ip: $set.ip, time: new Date(),
|
||||||
|
});
|
||||||
return document.set(domainId, document.TYPE_DISCUSSION, did, $set);
|
return document.set(domainId, document.TYPE_DISCUSSION, did, $set);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,13 +93,20 @@ export function inc(
|
|||||||
return document.inc(domainId, document.TYPE_DISCUSSION, did, key, value);
|
return document.inc(domainId, document.TYPE_DISCUSSION, did, key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function del(domainId: string, did: ObjectID): Promise<never> {
|
export async function del(domainId: string, did: ObjectID): Promise<void> {
|
||||||
return Promise.all([
|
const [ddoc, drdocs] = await Promise.all([
|
||||||
|
document.get(domainId, document.TYPE_DISCUSSION, did),
|
||||||
|
document.getMulti(domainId, document.TYPE_DISCUSSION_REPLY, {
|
||||||
|
parentType: document.TYPE_DISCUSSION, parentId: did,
|
||||||
|
}).project({ _id: 1, 'reply._id': 1 }).toArray(),
|
||||||
|
]) as any;
|
||||||
|
await Promise.all([
|
||||||
document.deleteOne(domainId, document.TYPE_DISCUSSION, did),
|
document.deleteOne(domainId, document.TYPE_DISCUSSION, did),
|
||||||
document.deleteMulti(domainId, document.TYPE_DISCUSSION_REPLY, {
|
document.deleteMulti(domainId, document.TYPE_DISCUSSION_REPLY, {
|
||||||
parentType: document.TYPE_DISCUSSION, parentId: did,
|
parentType: document.TYPE_DISCUSSION, parentId: did,
|
||||||
}),
|
}),
|
||||||
document.deleteMultiStatus(domainId, document.TYPE_DISCUSSION, { docId: did }),
|
document.deleteMultiStatus(domainId, document.TYPE_DISCUSSION, { docId: did }),
|
||||||
|
coll.deleteMany({ domainId, docId: { $in: [ddoc._id, ...(drdocs.reply?.map((i) => i._id) || [])] } }),
|
||||||
]) as any;
|
]) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,13 +124,17 @@ export async function addReply(
|
|||||||
domainId: string, did: ObjectID, owner: number,
|
domainId: string, did: ObjectID, owner: number,
|
||||||
content: string, ip: string,
|
content: string, ip: string,
|
||||||
): Promise<ObjectID> {
|
): Promise<ObjectID> {
|
||||||
|
const time = new Date();
|
||||||
const [drid] = await Promise.all([
|
const [drid] = await Promise.all([
|
||||||
document.add(
|
document.add(
|
||||||
domainId, content, owner, document.TYPE_DISCUSSION_REPLY,
|
domainId, content, owner, document.TYPE_DISCUSSION_REPLY,
|
||||||
null, document.TYPE_DISCUSSION, did, { ip },
|
null, document.TYPE_DISCUSSION, did, { ip, editor: owner },
|
||||||
),
|
),
|
||||||
document.incAndSet(domainId, document.TYPE_DISCUSSION, did, 'nReply', 1, { updateAt: new Date() }),
|
document.incAndSet(domainId, document.TYPE_DISCUSSION, did, 'nReply', 1, { updateAt: time }),
|
||||||
]);
|
]);
|
||||||
|
await coll.insertOne({
|
||||||
|
domainId, docId: drid, content, uid: owner, ip, time,
|
||||||
|
});
|
||||||
return drid;
|
return drid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,9 +143,12 @@ export function getReply(domainId: string, drid: ObjectID): Promise<DiscussionRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function editReply(
|
export async function editReply(
|
||||||
domainId: string, drid: ObjectID, content: string,
|
domainId: string, drid: ObjectID, content: string, uid: number, ip: string,
|
||||||
): Promise<DiscussionReplyDoc | null> {
|
): Promise<DiscussionReplyDoc | null> {
|
||||||
return document.set(domainId, document.TYPE_DISCUSSION_REPLY, drid, { content });
|
await coll.insertOne({
|
||||||
|
domainId, docId: drid, content, uid, ip, time: new Date(),
|
||||||
|
});
|
||||||
|
return document.set(domainId, document.TYPE_DISCUSSION_REPLY, drid, { content, edited: true, editor: uid });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function delReply(domainId: string, drid: ObjectID) {
|
export async function delReply(domainId: string, drid: ObjectID) {
|
||||||
@@ -130,6 +157,7 @@ export async function delReply(domainId: string, drid: ObjectID) {
|
|||||||
return await Promise.all([
|
return await Promise.all([
|
||||||
document.deleteOne(domainId, document.TYPE_DISCUSSION_REPLY, drid),
|
document.deleteOne(domainId, document.TYPE_DISCUSSION_REPLY, drid),
|
||||||
document.inc(domainId, document.TYPE_DISCUSSION, drdoc.parentId, 'nReply', -1),
|
document.inc(domainId, document.TYPE_DISCUSSION, drdoc.parentId, 'nReply', -1),
|
||||||
|
coll.deleteMany({ domainId, docId: { $in: [drid, ...(drdoc.reply?.map((i) => i._id) || [])] } }),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,14 +189,20 @@ export async function addTailReply(
|
|||||||
domainId: string, drid: ObjectID,
|
domainId: string, drid: ObjectID,
|
||||||
owner: number, content: string, ip: string,
|
owner: number, content: string, ip: string,
|
||||||
): Promise<[DiscussionReplyDoc, ObjectID]> {
|
): Promise<[DiscussionReplyDoc, ObjectID]> {
|
||||||
|
const time = new Date();
|
||||||
const [drdoc, subId] = await document.push(
|
const [drdoc, subId] = await document.push(
|
||||||
domainId, document.TYPE_DISCUSSION_REPLY, drid,
|
domainId, document.TYPE_DISCUSSION_REPLY, drid,
|
||||||
'reply', content, owner, { ip },
|
'reply', content, owner, { ip, editor: owner },
|
||||||
);
|
|
||||||
await document.set(
|
|
||||||
domainId, document.TYPE_DISCUSSION, drdoc.parentId,
|
|
||||||
{ updateAt: new Date() },
|
|
||||||
);
|
);
|
||||||
|
await Promise.all([
|
||||||
|
coll.insertOne({
|
||||||
|
domainId, docId: subId, content, uid: owner, ip, time: new Date(),
|
||||||
|
}),
|
||||||
|
document.set(
|
||||||
|
domainId, document.TYPE_DISCUSSION, drdoc.parentId,
|
||||||
|
{ updateAt: time },
|
||||||
|
),
|
||||||
|
]);
|
||||||
return [drdoc, subId];
|
return [drdoc, subId];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,14 +212,33 @@ export function getTailReply(
|
|||||||
return document.getSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid);
|
return document.getSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function editTailReply(
|
export async function editTailReply(
|
||||||
domainId: string, drid: ObjectID, drrid: ObjectID, content: string,
|
domainId: string, drid: ObjectID, drrid: ObjectID, content: string, uid: number, ip: string,
|
||||||
): Promise<DiscussionTailReplyDoc> {
|
): Promise<DiscussionTailReplyDoc> {
|
||||||
return document.setSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid, { content });
|
const [, drrdoc] = await Promise.all([
|
||||||
|
coll.insertOne({
|
||||||
|
domainId, docId: drrid, content, uid, time: new Date(), ip,
|
||||||
|
}),
|
||||||
|
document.setSub(domainId, document.TYPE_DISCUSSION_REPLY, drid,
|
||||||
|
'reply', drrid, { content, edited: true, editor: uid }),
|
||||||
|
]);
|
||||||
|
return drrdoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function delTailReply(domainId: string, drid: ObjectID, drrid: ObjectID) {
|
export async function delTailReply(domainId: string, drid: ObjectID, drrid: ObjectID) {
|
||||||
return document.deleteSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid);
|
return Promise.all([
|
||||||
|
document.deleteSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid),
|
||||||
|
coll.deleteMany({ domainId, docId: drrid }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHistory(
|
||||||
|
domainId: string, docId: ObjectID, query: FilterQuery<DiscussionHistoryDoc> = {},
|
||||||
|
projection = HISTORY_PROJECTION_PUBLIC,
|
||||||
|
) {
|
||||||
|
return coll.find({ domainId, docId, ...query })
|
||||||
|
.sort({ time: -1 }).project(buildProjection(projection))
|
||||||
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setStar(domainId: string, did: ObjectID, uid: number, star: boolean) {
|
export function setStar(domainId: string, did: ObjectID, uid: number, star: boolean) {
|
||||||
@@ -306,9 +359,11 @@ export function apply(ctx: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
global.Hydro.model.discussion = {
|
global.Hydro.model.discussion = {
|
||||||
|
coll,
|
||||||
typeDisplay,
|
typeDisplay,
|
||||||
PROJECTION_LIST,
|
PROJECTION_LIST,
|
||||||
PROJECTION_PUBLIC,
|
PROJECTION_PUBLIC,
|
||||||
|
HISTORY_PROJECTION_PUBLIC,
|
||||||
|
|
||||||
apply,
|
apply,
|
||||||
add,
|
add,
|
||||||
@@ -330,6 +385,7 @@ global.Hydro.model.discussion = {
|
|||||||
delTailReply,
|
delTailReply,
|
||||||
react,
|
react,
|
||||||
getReaction,
|
getReaction,
|
||||||
|
getHistory,
|
||||||
setStar,
|
setStar,
|
||||||
getStatus,
|
getStatus,
|
||||||
setStatus,
|
setStatus,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable object-curly-newline */
|
/* eslint-disable object-curly-newline */
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import {
|
import {
|
||||||
Cursor, FilterQuery, ObjectID, OnlyFieldsOfType, UpdateQuery,
|
Cursor, FilterQuery, ObjectID, OnlyFieldsOfType, PushOperator, UpdateQuery,
|
||||||
} from 'mongodb';
|
} from 'mongodb';
|
||||||
import { Context } from '../context';
|
import { Context } from '../context';
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +15,7 @@ import { ArrayKeys, MaybeArray, NumberKeys, Projection } from '../typeutils';
|
|||||||
import { buildProjection } from '../utils';
|
import { buildProjection } from '../utils';
|
||||||
|
|
||||||
type DocID = ObjectID | string | number;
|
type DocID = ObjectID | string | number;
|
||||||
|
type NormalArrayKeys<O, P = any> = Exclude<ArrayKeys<O, P>, Symbol>;
|
||||||
|
|
||||||
export const coll = db.collection('document');
|
export const coll = db.collection('document');
|
||||||
export const collStatus = db.collection('document.status');
|
export const collStatus = db.collection('document.status');
|
||||||
@@ -95,12 +96,18 @@ export async function get<K extends keyof DocType>(domainId: string, docType: K,
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function set<K extends keyof DocType>(
|
export async function set<K extends keyof DocType>(
|
||||||
domainId: string, docType: K, docId: DocType[K]['docId'], $set?: Partial<DocType[K]>, $unset?: OnlyFieldsOfType<DocType[K], any, true | '' | 1>,
|
domainId: string,
|
||||||
|
docType: K,
|
||||||
|
docId: DocType[K]['docId'],
|
||||||
|
$set?: Partial<DocType[K]>,
|
||||||
|
$unset?: OnlyFieldsOfType<DocType[K], any, true | '' | 1>,
|
||||||
|
$push?: PushOperator<DocType[K]>,
|
||||||
): Promise<DocType[K]> {
|
): Promise<DocType[K]> {
|
||||||
await bus.parallel('document/set', domainId, docType, docId, $set, $unset);
|
await bus.parallel('document/set', domainId, docType, docId, $set, $unset);
|
||||||
const update: UpdateQuery<DocType[K]> = {};
|
const update: UpdateQuery<DocType[K]> = {};
|
||||||
if ($set) update.$set = $set;
|
if ($set) update.$set = $set;
|
||||||
if ($unset) update.$unset = $unset;
|
if ($unset) update.$unset = $unset;
|
||||||
|
if ($push) update.$push = $push;
|
||||||
const res = await coll.findOneAndUpdate(
|
const res = await coll.findOneAndUpdate(
|
||||||
{ domainId, docType, docId },
|
{ domainId, docType, docId },
|
||||||
update,
|
update,
|
||||||
@@ -234,7 +241,7 @@ export async function getSub<T extends keyof DocType, K extends ArrayKeys<DocTyp
|
|||||||
return [doc, null];
|
return [doc, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setSub<T extends keyof DocType, K extends string & ArrayKeys<DocType[T]>>(
|
export async function setSub<T extends keyof DocType, K extends NormalArrayKeys<DocType[T]>>(
|
||||||
domainId: string, docType: T, docId: DocType[T]['docId'],
|
domainId: string, docType: T, docId: DocType[T]['docId'],
|
||||||
key: K, subId: DocType[T][K][0]['_id'], args: Partial<DocType[T][K][0]>,
|
key: K, subId: DocType[T][K][0]['_id'], args: Partial<DocType[T][K][0]>,
|
||||||
): Promise<DocType[T]> {
|
): Promise<DocType[T]> {
|
||||||
@@ -368,7 +375,7 @@ export async function incStatus<T extends keyof DocStatusType>(
|
|||||||
|
|
||||||
export async function revPushStatus<T extends keyof DocStatusType>(
|
export async function revPushStatus<T extends keyof DocStatusType>(
|
||||||
domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number,
|
domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number,
|
||||||
key: string & ArrayKeys<DocStatusType[T]>, value: any, id = '_id',
|
key: NormalArrayKeys<DocStatusType[T]>, value: any, id = '_id',
|
||||||
): Promise<DocStatusType[T]> {
|
): Promise<DocStatusType[T]> {
|
||||||
let res = await collStatus.findOneAndUpdate(
|
let res = await collStatus.findOneAndUpdate(
|
||||||
{ domainId, docType, docId, uid, [`${key}.${id}`]: value[id] },
|
{ domainId, docType, docId, uid, [`${key}.${id}`]: value[id] },
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const tasks = {
|
|||||||
content: r.content,
|
content: r.content,
|
||||||
owner: r.owner_uid,
|
owner: r.owner_uid,
|
||||||
ip: r.ip,
|
ip: r.ip,
|
||||||
history: [],
|
|
||||||
};
|
};
|
||||||
res.push(drrdoc);
|
res.push(drrdoc);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ $downvote-color = red
|
|||||||
float: left
|
float: left
|
||||||
margin-bottom: rem(5px)
|
margin-bottom: rem(5px)
|
||||||
|
|
||||||
|
.history
|
||||||
|
float: right
|
||||||
|
margin-left: rem(5px)
|
||||||
|
|
||||||
.dczcomments__operations
|
.dczcomments__operations
|
||||||
font-size: rem($font-size)
|
font-size: rem($font-size)
|
||||||
float: right
|
float: right
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Popover } from '@blueprintjs/core';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
|
||||||
|
import TimeAgo from 'timeago-react';
|
||||||
|
import { InfoDialog } from 'vj/components/dialog';
|
||||||
|
import { AutoloadPage } from 'vj/misc/Page';
|
||||||
|
import i18n from 'vj/utils/i18n';
|
||||||
|
import request from 'vj/utils/request';
|
||||||
|
import tpl from 'vj/utils/tpl';
|
||||||
|
|
||||||
|
async function historyDialog(payload, time, uid) {
|
||||||
|
const ts = new Date(time).getTime();
|
||||||
|
const rawHtml = await fetch(`${payload}?time=${ts}&render=1`).then((res) => res.text());
|
||||||
|
new InfoDialog({
|
||||||
|
$body: tpl`
|
||||||
|
<div class="typo">
|
||||||
|
<p><div data-user>${uid}</div> ${i18n('Edited at')} <span class="time relative" data-timestamp="${ts / 1000}">${time}</span></p>
|
||||||
|
${{ templateRaw: true, html: rawHtml }}
|
||||||
|
</div>`,
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
function History({ payload }) {
|
||||||
|
const [load, setLoad] = React.useState(false);
|
||||||
|
const { isLoading, isError, data } = useQuery(['history', payload], async () => {
|
||||||
|
const { history } = await request.get(`${payload}?all=1`);
|
||||||
|
return history;
|
||||||
|
}, { enabled: !!load });
|
||||||
|
return (
|
||||||
|
<Popover usePortal interactionKind="hover" position="bottom" onOpening={() => setLoad(true)}>
|
||||||
|
<span>{i18n('Edited')} <span className="icon icon-expand_more"></span></span>
|
||||||
|
<ol className="menu">
|
||||||
|
{(isLoading || isError) && (
|
||||||
|
<li className="menu__item">
|
||||||
|
<a className="menu__link">
|
||||||
|
{isLoading ? i18n('Loading...') : i18n('Loading failed.')}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{data?.map((item) => (
|
||||||
|
<li className="menu__item" key={item.time}>
|
||||||
|
<a className="menu__link" onClick={() => historyDialog(payload, item.time, item.uid)}>
|
||||||
|
{i18n('Edited at')}
|
||||||
|
{' '}
|
||||||
|
<TimeAgo datetime={item.time} locale={i18n('timeago_locale')} />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = new AutoloadPage('discussionHistoryPage', () => {
|
||||||
|
$('[data-discussion-history]').each((i, e) => {
|
||||||
|
const url = $(e).data('raw-url');
|
||||||
|
if (!url) return;
|
||||||
|
ReactDOM.createRoot(e).render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<History payload={url} />
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default page;
|
||||||
@@ -254,4 +254,10 @@ export function apply(ctx: Context) {
|
|||||||
ctx.on('system/setting', () => triggerHotUpdate());
|
ctx.on('system/setting', () => triggerHotUpdate());
|
||||||
ctx.on('app/watch/change', triggerHotUpdate);
|
ctx.on('app/watch/change', triggerHotUpdate);
|
||||||
ctx.on('app/watch/unlink', triggerHotUpdate);
|
ctx.on('app/watch/unlink', triggerHotUpdate);
|
||||||
|
ctx.on('handler/after/DiscussionRaw', async (that) => {
|
||||||
|
if (that.args.render && that.response.type === 'text/markdown') {
|
||||||
|
that.response.type = 'text/html';
|
||||||
|
that.response.body = await markdown.render(that.response.body);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -285,6 +285,8 @@ Download: 下载
|
|||||||
Downvote: 差评
|
Downvote: 差评
|
||||||
Duration (hours): 持续时间 (小时)
|
Duration (hours): 持续时间 (小时)
|
||||||
Duration: 持续时间
|
Duration: 持续时间
|
||||||
|
Edited: 已修改
|
||||||
|
Edited at: 修改于
|
||||||
Edit any contests: 修改任意的比赛
|
Edit any contests: 修改任意的比赛
|
||||||
Edit any homework: 修改任意的作业
|
Edit any homework: 修改任意的作业
|
||||||
Edit Contest: 编辑比赛
|
Edit Contest: 编辑比赛
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.42.9",
|
"version": "4.42.10",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
@@ -87,6 +87,7 @@
|
|||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-query": "^3.39.2",
|
||||||
"react-redux": "^8.0.4",
|
"react-redux": "^8.0.4",
|
||||||
"react-split-pane": "^0.1.92",
|
"react-split-pane": "^0.1.92",
|
||||||
"reconnecting-websocket": "^4.4.0",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
|
|||||||
@@ -94,6 +94,9 @@
|
|||||||
<div class="supplementary dczcomments__supplementary">
|
<div class="supplementary dczcomments__supplementary">
|
||||||
{{ user.render_inline(udoc, avatar=false) }}
|
{{ user.render_inline(udoc, avatar=false) }}
|
||||||
@ {{ datetimeSpan(doc['_id'])|safe }}
|
@ {{ datetimeSpan(doc['_id'])|safe }}
|
||||||
|
{% if doc.edited %}
|
||||||
|
<div class="history" data-discussion-history data-raw-url="{{ url('discussion_reply_raw', did=doc.parentId, drid=doc._id) }}"><a>{{ _('Edited') }}</a></div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dczcomments__operations nojs--hide">
|
<div class="dczcomments__operations nojs--hide">
|
||||||
{% if handler.user.hasPerm(reply_post_perm) %}
|
{% if handler.user.hasPerm(reply_post_perm) %}
|
||||||
@@ -132,6 +135,9 @@
|
|||||||
<div class="supplementary dczcomments__supplementary">
|
<div class="supplementary dczcomments__supplementary">
|
||||||
{{ user.render_inline(udoc, avatar=false) }}
|
{{ user.render_inline(udoc, avatar=false) }}
|
||||||
@ {{ datetimeSpan(rdoc._id)|safe }}
|
@ {{ datetimeSpan(rdoc._id)|safe }}
|
||||||
|
{% if doc.edited %}
|
||||||
|
<div class="history" data-discussion-history data-raw-url="{{ url('discussion_tail_reply_raw', did=doc.parentId, drid=doc._id, drrid=rdoc._id) }}"><a>{{ _('Edited') }}</a></div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dczcomments__operations nojs--hide">
|
<div class="dczcomments__operations nojs--hide">
|
||||||
{% if handler.user.hasPerm(reply_post_perm) %}
|
{% if handler.user.hasPerm(reply_post_perm) %}
|
||||||
|
|||||||
@@ -34,6 +34,9 @@
|
|||||||
<li><a href="{{ url('discussion_edit', did=ddoc.docId) }}">
|
<li><a href="{{ url('discussion_edit', did=ddoc.docId) }}">
|
||||||
<span class="icon icon-edit"></span> {{ _('Edit') }}
|
<span class="icon icon-edit"></span> {{ _('Edit') }}
|
||||||
</a></li>
|
</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if ddoc.edited %}
|
||||||
|
<li data-discussion-history data-raw-url="{{ url('discussion_raw', did=ddoc.docId) }}">{{ _('Edited') }}</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>{{ _('{0} views').format(ddoc.views) }}</li>
|
<li>{{ _('{0} views').format(ddoc.views) }}</li>
|
||||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||||
|
|||||||
@@ -84,6 +84,16 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if ui.getNodes('UserDropdown').length %}
|
||||||
|
<li class="menu__seperator"></li>
|
||||||
|
{%- for item in ui.getNodes('UserDropdown') -%}
|
||||||
|
<li class="menu__item nojs--hide">
|
||||||
|
<a href="{{ url(item.name) }}" class="menu__link">
|
||||||
|
<span class="icon icon-{{ item.args.icon }}"></span> {{ _(item.args.displayName or item.name) }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{%- endfor -%}
|
||||||
|
{% endif %}
|
||||||
<li class="menu__seperator"></li>
|
<li class="menu__seperator"></li>
|
||||||
{% if handler.user.hasPriv(PRIV.PRIV_EDIT_SYSTEM) %}
|
{% if handler.user.hasPriv(PRIV.PRIV_EDIT_SYSTEM) %}
|
||||||
<li class="menu__item nojs--hide">
|
<li class="menu__item nojs--hide">
|
||||||
|
|||||||
Reference in new issue
Block a user