mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core&ui: contest balloon notice (#1071)
This commit is contained in:
1 parent
6d2442c6a3
commit
053978d9d4
12 files changed
+135
-63
No files matched your search
@@ -28,6 +28,7 @@ __langname: 简体中文
|
|||||||
'Copy failed :(': '复制失败 :('
|
'Copy failed :(': '复制失败 :('
|
||||||
'Current dataset: {0}': '当前测试数据: {0}'
|
'Current dataset: {0}': '当前测试数据: {0}'
|
||||||
'Discussion: Terms Of Service': 讨论区服务条款
|
'Discussion: Terms Of Service': 讨论区服务条款
|
||||||
|
'First Blood Notice\n{0} solved problem {1} ({2})': '题目一血提示\n{0} 解决了题目 {1} ({2})'
|
||||||
'Format: category 1, sub category 1.1, sub category 1.2, ..., sub category 1.x, ..., category n, sub category n.1, sub category n.2, ..., sub category n.m, ...': 格式:分类1, 子分类1.1, 子分类1.2, ..., 子分类1.x, ..., 分类n, 子分类n.1, 子分类n.2, ..., 子分类n.m, ...
|
'Format: category 1, sub category 1.1, sub category 1.2, ..., sub category 1.x, ..., category n, sub category n.1, sub category n.2, ..., sub category n.m, ...': 格式:分类1, 子分类1.1, 子分类1.2, ..., 子分类1.x, ..., 分类n, 子分类n.1, 子分类n.2, ..., 子分类n.m, ...
|
||||||
'Join {0}': '加入 {0}'
|
'Join {0}': '加入 {0}'
|
||||||
'No': 否
|
'No': 否
|
||||||
|
|||||||
@@ -568,19 +568,22 @@ class HomeMessagesHandler extends Handler {
|
|||||||
const messages = await message.getByUser(this.user._id);
|
const messages = await message.getByUser(this.user._id);
|
||||||
const uids = new Set<number>([
|
const uids = new Set<number>([
|
||||||
...messages.map((mdoc) => mdoc.from),
|
...messages.map((mdoc) => mdoc.from),
|
||||||
...messages.map((mdoc) => mdoc.to),
|
...messages.flatMap((mdoc) => mdoc.to),
|
||||||
]);
|
]);
|
||||||
const udict = await user.getList('system', Array.from(uids));
|
const udict = await user.getList('system', Array.from(uids));
|
||||||
// TODO(twd2): improve here:
|
// TODO(twd2): improve here:
|
||||||
const parsed = {};
|
const parsed = {};
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
const target = m.from === this.user._id ? m.to : m.from;
|
const raw = m.from === this.user._id ? m.to : m.from;
|
||||||
parsed[target] ||= {
|
const targetArr = Array.isArray(raw) ? raw : [raw];
|
||||||
_id: target,
|
for (const target of targetArr) {
|
||||||
udoc: { ...udict[target], avatarUrl: avatar(udict[target].avatar) },
|
parsed[target] ||= {
|
||||||
messages: [],
|
_id: target,
|
||||||
};
|
udoc: { ...udict[target], avatarUrl: avatar(udict[target].avatar) },
|
||||||
parsed[target].messages.push(m);
|
messages: [],
|
||||||
|
};
|
||||||
|
parsed[target].messages.push(m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await user.setById(this.user._id, { unreadMsg: 0 });
|
await user.setById(this.user._id, { unreadMsg: 0 });
|
||||||
this.response.body = { messages: parsed };
|
this.response.body = { messages: parsed };
|
||||||
@@ -601,19 +604,10 @@ class HomeMessagesHandler extends Handler {
|
|||||||
@param('messageId', Types.ObjectId)
|
@param('messageId', Types.ObjectId)
|
||||||
async postDeleteMessage({ }, messageId: ObjectId) {
|
async postDeleteMessage({ }, messageId: ObjectId) {
|
||||||
const msg = await message.get(messageId);
|
const msg = await message.get(messageId);
|
||||||
if ([msg.from, msg.to].includes(this.user._id)) await message.del(messageId);
|
if (msg.from === this.user._id) await message.del(messageId);
|
||||||
else throw new PermissionError();
|
else throw new PermissionError();
|
||||||
this.back();
|
this.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@param('messageId', Types.ObjectId)
|
|
||||||
async postRead({ }, messageId: ObjectId) {
|
|
||||||
const msg = await message.get(messageId);
|
|
||||||
if ([msg.from, msg.to].includes(this.user._id)) {
|
|
||||||
await message.setFlag(messageId, message.FLAG_UNREAD);
|
|
||||||
} else throw new PermissionError();
|
|
||||||
this.back();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const inject = { geoip: { required: false }, oauth: {} };
|
export const inject = { geoip: { required: false }, oauth: {} };
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ class UserWebauthnHandler extends Handler {
|
|||||||
|
|
||||||
async post({ domainId, result, redirect }) {
|
async post({ domainId, result, redirect }) {
|
||||||
const challenge = this.session.challenge;
|
const challenge = this.session.challenge;
|
||||||
if (!challenge) throw new ForbiddenError();
|
if (!challenge) throw new ForbiddenError('no-challenge');
|
||||||
const tdoc = await token.get(challenge, token.TYPE_WEBAUTHN);
|
const tdoc = await token.get(challenge, token.TYPE_WEBAUTHN);
|
||||||
if (!tdoc) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_WEBAUTHN]);
|
if (!tdoc) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_WEBAUTHN]);
|
||||||
const udoc = await (tdoc.uid === 'login'
|
const udoc = await (tdoc.uid === 'login'
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ export interface DomainDoc extends Record<string, any> {
|
|||||||
// Message
|
// Message
|
||||||
export interface MessageDoc {
|
export interface MessageDoc {
|
||||||
from: number;
|
from: number;
|
||||||
to: number;
|
to: number | number[];
|
||||||
content: string;
|
content: string;
|
||||||
flag: number;
|
flag: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { sumBy } from 'lodash';
|
import { sumBy } from 'lodash';
|
||||||
import { Filter, ObjectId } from 'mongodb';
|
import { Filter, ObjectId } from 'mongodb';
|
||||||
import {
|
import {
|
||||||
Counter, formatSeconds, getAlphabeticId, Time,
|
Counter, formatSeconds, getAlphabeticId, sleep, Time,
|
||||||
} from '@hydrooj/utils/lib/utils';
|
} from '@hydrooj/utils/lib/utils';
|
||||||
|
import { Context } from '../context';
|
||||||
import {
|
import {
|
||||||
ContestAlreadyAttendedError, ContestNotFoundError,
|
ContestAlreadyAttendedError, ContestNotFoundError,
|
||||||
ContestScoreboardHiddenError, ValidationError,
|
ContestScoreboardHiddenError, ValidationError,
|
||||||
@@ -11,14 +12,17 @@ import {
|
|||||||
BaseUserDict, ContestPrintDoc, ContestRule, ContestRules, ProblemDict, RecordDoc,
|
BaseUserDict, ContestPrintDoc, ContestRule, ContestRules, ProblemDict, RecordDoc,
|
||||||
ScoreboardConfig, ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc,
|
ScoreboardConfig, ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc,
|
||||||
} from '../interface';
|
} from '../interface';
|
||||||
|
import avatar from '../lib/avatar';
|
||||||
import bus from '../service/bus';
|
import bus from '../service/bus';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
import type { Handler } from '../service/server';
|
import type { Handler } from '../service/server';
|
||||||
import { Optional } from '../typeutils';
|
import { Optional } from '../typeutils';
|
||||||
import { PERM, STATUS, STATUS_SHORT_TEXTS } from './builtin';
|
import { PERM, STATUS, STATUS_SHORT_TEXTS } from './builtin';
|
||||||
import * as document from './document';
|
import * as document from './document';
|
||||||
import problem from './problem';
|
import MessageModel from './message';
|
||||||
import user, { User } from './user';
|
import problem, { ProblemModel } from './problem';
|
||||||
|
import RecordModel from './record';
|
||||||
|
import UserModel, { User } from './user';
|
||||||
|
|
||||||
export enum PrintTaskStatus {
|
export enum PrintTaskStatus {
|
||||||
pending = 'pending',
|
pending = 'pending',
|
||||||
@@ -233,7 +237,7 @@ const acm = buildContestRule({
|
|||||||
async scoreboard(config, _, tdoc, pdict, cursor) {
|
async scoreboard(config, _, tdoc, pdict, cursor) {
|
||||||
const rankedTsdocs = await db.ranked(cursor, (a, b) => (a.score || 0) === (b.score || 0) && (a.time || 0) === (b.time || 0));
|
const rankedTsdocs = await db.ranked(cursor, (a, b) => (a.score || 0) === (b.score || 0) && (a.time || 0) === (b.time || 0));
|
||||||
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
||||||
const udict = await user.getListForRender(tdoc.domainId, uids, config.showDisplayName ? ['displayName'] : []);
|
const udict = await UserModel.getListForRender(tdoc.domainId, uids, config.showDisplayName ? ['displayName'] : []);
|
||||||
// Find first accept
|
// Find first accept
|
||||||
const first = {};
|
const first = {};
|
||||||
const data = await document.collStatus.aggregate([
|
const data = await document.collStatus.aggregate([
|
||||||
@@ -407,7 +411,7 @@ const oi = buildContestRule({
|
|||||||
async scoreboard(config, _, tdoc, pdict, cursor) {
|
async scoreboard(config, _, tdoc, pdict, cursor) {
|
||||||
const rankedTsdocs = await db.ranked(cursor, (a, b) => (a.score || 0) === (b.score || 0));
|
const rankedTsdocs = await db.ranked(cursor, (a, b) => (a.score || 0) === (b.score || 0));
|
||||||
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
||||||
const udict = await user.getListForRender(tdoc.domainId, uids, config.showDisplayName ? ['displayName'] : []);
|
const udict = await UserModel.getListForRender(tdoc.domainId, uids, config.showDisplayName ? ['displayName'] : []);
|
||||||
const psdict = {};
|
const psdict = {};
|
||||||
const first = {};
|
const first = {};
|
||||||
const useRelativeTime = !!tdoc.duration;
|
const useRelativeTime = !!tdoc.duration;
|
||||||
@@ -776,7 +780,7 @@ const homework = buildContestRule({
|
|||||||
async scoreboard(config, _, tdoc, pdict, cursor) {
|
async scoreboard(config, _, tdoc, pdict, cursor) {
|
||||||
const rankedTsdocs = await db.ranked(cursor, (a, b) => a.score === b.score);
|
const rankedTsdocs = await db.ranked(cursor, (a, b) => a.score === b.score);
|
||||||
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
|
||||||
const udict = await user.getListForRender(tdoc.domainId, uids, config.showDisplayName ? ['displayName'] : []);
|
const udict = await UserModel.getListForRender(tdoc.domainId, uids, config.showDisplayName ? ['displayName'] : []);
|
||||||
const columns = await this.scoreboardHeader(config, _, tdoc, pdict);
|
const columns = await this.scoreboardHeader(config, _, tdoc, pdict);
|
||||||
const rows: ScoreboardRow[] = [
|
const rows: ScoreboardRow[] = [
|
||||||
columns,
|
columns,
|
||||||
@@ -851,16 +855,30 @@ export async function getRelated(domainId: string, pid: number, rule?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function addBalloon(domainId: string, tid: ObjectId, uid: number, rid: ObjectId, pid: number) {
|
export async function addBalloon(domainId: string, tid: ObjectId, uid: number, rid: ObjectId, pid: number) {
|
||||||
const balloon = await collBalloon.findOne({
|
const balloon = await collBalloon.find({ domainId, tid, pid }).project({ uid: 1 }).toArray();
|
||||||
domainId, tid, pid, uid,
|
if (balloon.find((i) => i.uid === uid)) return null;
|
||||||
});
|
let isFirst = !balloon.length;
|
||||||
if (balloon) return null;
|
if (isFirst) {
|
||||||
const bdcount = await collBalloon.countDocuments({ domainId, tid, pid });
|
let pending: RecordDoc[] = [];
|
||||||
|
do {
|
||||||
|
if (pending.length) await sleep(500); // eslint-disable-line no-await-in-loop
|
||||||
|
pending = await RecordModel.getMulti(domainId, { // eslint-disable-line no-await-in-loop
|
||||||
|
pid, contest: tid, _id: { $lt: rid }, status: {
|
||||||
|
$in: [
|
||||||
|
STATUS.STATUS_WAITING, STATUS.STATUS_COMPILING,
|
||||||
|
STATUS.STATUS_JUDGING, STATUS.STATUS_FETCHED,
|
||||||
|
STATUS.STATUS_ACCEPTED,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).limit(1).toArray();
|
||||||
|
} while (pending.length && !pending.some((i) => i.status === STATUS.STATUS_ACCEPTED));
|
||||||
|
if (pending.some((i) => i.status === STATUS.STATUS_ACCEPTED)) isFirst = false;
|
||||||
|
}
|
||||||
const newBdoc = {
|
const newBdoc = {
|
||||||
_id: rid, domainId, tid, pid, uid, ...(!bdcount ? { first: true } : {}),
|
_id: rid, domainId, tid, pid, uid, ...(isFirst ? { first: true } : {}),
|
||||||
};
|
};
|
||||||
await collBalloon.insertOne(newBdoc);
|
await collBalloon.insertOne(newBdoc);
|
||||||
bus.broadcast('contest/balloon', domainId, tid, newBdoc);
|
bus.emit('contest/balloon', domainId, tid, newBdoc);
|
||||||
return rid;
|
return rid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1090,7 +1108,34 @@ export function getMultiPrintTask(domainId: string, tid: ObjectId, query = {}) {
|
|||||||
.sort({ _id: 1 });
|
.sort({ _id: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function apply(ctx: Context) {
|
||||||
|
ctx.on('contest/balloon', (domainId, tid, bdoc) => {
|
||||||
|
if (!bdoc.first) return;
|
||||||
|
(async () => {
|
||||||
|
const tsdocs = await getMultiStatus(domainId, { docId: tid, subscribe: 1 }).toArray();
|
||||||
|
const uids = Array.from<number>(new Set(tsdocs.map((tsdoc) => tsdoc.uid)));
|
||||||
|
const [team, tdoc, pdoc] = await Promise.all([
|
||||||
|
UserModel.getById(domainId, bdoc.uid),
|
||||||
|
get(domainId, tid),
|
||||||
|
ProblemModel.get(domainId, bdoc.pid),
|
||||||
|
]);
|
||||||
|
await MessageModel.send(1, uids, JSON.stringify({
|
||||||
|
message: 'First Blood Notice\n{0} solved problem {1} ({2})',
|
||||||
|
avatar: avatar(team.avatar),
|
||||||
|
params: [team.uname, getAlphabeticId(tdoc.pids.indexOf(bdoc.pid)), pdoc.title],
|
||||||
|
}), MessageModel.FLAG_I18N);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
await ctx.db.ensureIndexes(
|
||||||
|
collBalloon,
|
||||||
|
{ key: { domainId: 1, tid: 1, pid: 1, uid: 1 }, unique: true, name: 'basic' },
|
||||||
|
{ key: { domainId: 1, tid: 1, pid: 1 }, unique: true, name: 'first', partialFilterExpression: { first: true } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
global.Hydro.model.contest = {
|
global.Hydro.model.contest = {
|
||||||
|
apply,
|
||||||
|
|
||||||
RULES,
|
RULES,
|
||||||
PrintTaskStatus,
|
PrintTaskStatus,
|
||||||
buildContestRule,
|
buildContestRule,
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ class MessageModel {
|
|||||||
content: string, flag: number = MessageModel.FLAG_UNREAD,
|
content: string, flag: number = MessageModel.FLAG_UNREAD,
|
||||||
) {
|
) {
|
||||||
if (!Array.isArray(to)) to = [to];
|
if (!Array.isArray(to)) to = [to];
|
||||||
const base = { from, content, flag };
|
const base = { from, content, flag, to };
|
||||||
if (!to.length) return base;
|
if (!to.length) return base;
|
||||||
await MessageModel.coll.insertMany(to.map((t) => ({ ...base, to: t })));
|
await MessageModel.coll.insertOne(base);
|
||||||
bus.broadcast('user/message', to, base);
|
bus.broadcast('user/message', to, base);
|
||||||
if (flag & MessageModel.FLAG_UNREAD) await user.inc(to, 'unreadMsg', 1);
|
if (flag & MessageModel.FLAG_UNREAD) await user.inc(to, 'unreadMsg', 1);
|
||||||
return base;
|
return base;
|
||||||
@@ -52,14 +52,6 @@ class MessageModel {
|
|||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async setFlag(messageId: ObjectId, flag: number) {
|
|
||||||
return await MessageModel.coll.findOneAndUpdate(
|
|
||||||
{ _id: messageId },
|
|
||||||
{ $bit: { flag: { xor: flag } } },
|
|
||||||
{ returnDocument: 'after' },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async del(_id: ObjectId) {
|
static async del(_id: ObjectId) {
|
||||||
return await MessageModel.coll.deleteOne({ _id });
|
return await MessageModel.coll.deleteOne({ _id });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,11 @@ export class MongoService extends Service {
|
|||||||
index.background = true;
|
index.background = true;
|
||||||
if (!i) {
|
if (!i) {
|
||||||
logger.info('Indexing %s.%s with key %o', coll.collectionName, index.name, index.key);
|
logger.info('Indexing %s.%s with key %o', coll.collectionName, index.name, index.key);
|
||||||
await coll.createIndexes([index]);
|
try {
|
||||||
|
await coll.createIndexes([index]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Failed to index %s.%s with key %o: %s', coll.collectionName, index.name, index.key, e);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const isDifferent = () => {
|
const isDifferent = () => {
|
||||||
@@ -137,7 +141,11 @@ export class MongoService extends Service {
|
|||||||
}
|
}
|
||||||
logger.info('Re-Index %s.%s with key %o', coll.collectionName, index.name, index.key);
|
logger.info('Re-Index %s.%s with key %o', coll.collectionName, index.name, index.key);
|
||||||
await coll.dropIndex(i.name);
|
await coll.dropIndex(i.name);
|
||||||
await coll.createIndexes([index]);
|
try {
|
||||||
|
await coll.createIndexes([index]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Failed to re-index %s.%s with key %o: %s', coll.collectionName, index.name, index.key, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,22 @@ import { i18n, tpl } from 'vj/utils';
|
|||||||
import Sock from '../socket';
|
import Sock from '../socket';
|
||||||
|
|
||||||
let previous: VjNotification;
|
let previous: VjNotification;
|
||||||
const onmessage = (msg) => {
|
const processI18n = (msg) => {
|
||||||
console.log('Received message', msg);
|
|
||||||
if (msg.mdoc.flag & FLAG_I18N) {
|
if (msg.mdoc.flag & FLAG_I18N) {
|
||||||
try {
|
try {
|
||||||
msg.mdoc.content = JSON.parse(msg.mdoc.content);
|
msg.mdoc.content = JSON.parse(msg.mdoc.content);
|
||||||
if (msg.mdoc.content.url) msg.mdoc.url = msg.mdoc.content.url;
|
if (msg.mdoc.content.url) msg.mdoc.url = msg.mdoc.content.url;
|
||||||
|
if (msg.mdoc.content.avatar) msg.mdoc.avatar = msg.mdoc.content.avatar;
|
||||||
msg.mdoc.content = i18n(msg.mdoc.content.message, ...msg.mdoc.content.params);
|
msg.mdoc.content = i18n(msg.mdoc.content.message, ...msg.mdoc.content.params);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
msg.mdoc.content = i18n(msg.mdoc.content);
|
msg.mdoc.content = i18n(msg.mdoc.content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return msg;
|
||||||
|
};
|
||||||
|
const onmessage = (msg) => {
|
||||||
|
console.log('Received message', msg);
|
||||||
|
msg = processI18n(msg);
|
||||||
if (msg.mdoc.flag & FLAG_ALERT) {
|
if (msg.mdoc.flag & FLAG_ALERT) {
|
||||||
// Is alert
|
// Is alert
|
||||||
new InfoDialog({
|
new InfoDialog({
|
||||||
@@ -48,6 +53,7 @@ const onmessage = (msg) => {
|
|||||||
? {
|
? {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: msg.mdoc.flag & FLAG_RICHTEXT ? i18n('You received a system message, click here to view.') : msg.mdoc.content,
|
message: msg.mdoc.flag & FLAG_RICHTEXT ? i18n('You received a system message, click here to view.') : msg.mdoc.content,
|
||||||
|
...(msg.mdoc.avatar ? { avatar: msg.mdoc.avatar } : {}),
|
||||||
} : {
|
} : {
|
||||||
title: msg.udoc.uname,
|
title: msg.udoc.uname,
|
||||||
avatar: msg.udoc.avatarUrl,
|
avatar: msg.udoc.avatarUrl,
|
||||||
@@ -72,6 +78,8 @@ const initWorkerMode = (endpoint) => {
|
|||||||
const { payload, type } = message.data;
|
const { payload, type } = message.data;
|
||||||
if (type === 'message') {
|
if (type === 'message') {
|
||||||
if (onmessage(payload)) worker.port.postMessage({ type: 'ack', id: payload.mdoc._id });
|
if (onmessage(payload)) worker.port.postMessage({ type: 'ack', id: payload.mdoc._id });
|
||||||
|
} else if (type === 'i18n') {
|
||||||
|
worker.port.postMessage({ type: 'ack', id: `${payload.mdoc._id}-i18n`, payload: processI18n(payload) });
|
||||||
} else if (type === 'open-page') {
|
} else if (type === 'open-page') {
|
||||||
console.log('opening page');
|
console.log('opening page');
|
||||||
window.open('/home/messages');
|
window.open('/home/messages');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// <reference no-default-lib="true" />
|
/// <reference no-default-lib="true" />
|
||||||
/// <reference lib="webworker" />
|
/// <reference lib="webworker" />
|
||||||
import ReconnectingWebsocket from 'reconnecting-websocket';
|
import ReconnectingWebsocket from 'reconnecting-websocket';
|
||||||
import { FLAG_INFO } from 'vj/constant/message';
|
import { FLAG_I18N, FLAG_INFO } from 'vj/constant/message';
|
||||||
declare const self: SharedWorkerGlobalScope;
|
declare const self: SharedWorkerGlobalScope;
|
||||||
|
|
||||||
console.log('SharedWorker init');
|
console.log('SharedWorker init');
|
||||||
@@ -17,6 +17,7 @@ interface RequestInitSharedConnPayload {
|
|||||||
interface RequestAckPayload {
|
interface RequestAckPayload {
|
||||||
type: 'ack';
|
type: 'ack';
|
||||||
id: string;
|
id: string;
|
||||||
|
payload?: any;
|
||||||
}
|
}
|
||||||
interface RequestUnloadPayload {
|
interface RequestUnloadPayload {
|
||||||
type: 'unload';
|
type: 'unload';
|
||||||
@@ -28,28 +29,37 @@ function broadcastMsg(message: any) {
|
|||||||
for (const p of ports) p.postMessage(message);
|
for (const p of ports) p.postMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessage(payload: any) {
|
function onMessage(message: any) {
|
||||||
broadcastMsg({ type: 'message', payload });
|
broadcastMsg({ type: 'message', payload: message });
|
||||||
let acked = false;
|
let acked = false;
|
||||||
ack[payload.mdoc._id] = () => { acked = true; };
|
let payload = message;
|
||||||
|
ack[message.mdoc._id] = () => { acked = true; };
|
||||||
|
ack[`${message.mdoc._id}-i18n`] = (v) => { payload = v; };
|
||||||
|
if (message.mdoc.flag & FLAG_I18N) broadcastMsg({ type: 'i18n', payload: message });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
delete ack[payload.mdoc._id];
|
delete ack[message.mdoc._id];
|
||||||
|
delete ack[`${message.mdoc._id}-i18n`];
|
||||||
if (acked) return;
|
if (acked) return;
|
||||||
if (payload.mdoc.flag & FLAG_INFO) return;
|
if (message.mdoc.flag & FLAG_INFO) return;
|
||||||
if (Notification?.permission !== 'granted') {
|
if (Notification?.permission !== 'granted') {
|
||||||
console.log('Notification permission denied');
|
console.log('Notification permission denied');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log('Sending as system notification');
|
||||||
// eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
new Notification(
|
new Notification(
|
||||||
payload.udoc.uname || 'Hydro Notification',
|
payload.udoc._id === 1 ? payload.mdoc.content.split('\n')[0] : payload.udoc.uname || 'Hydro Notification',
|
||||||
{
|
payload.udoc._id === 1 ? {
|
||||||
|
tag: `notification-${payload.mdoc._id}`,
|
||||||
|
icon: payload.mdoc.avatar || '/android-chrome-192x192.png',
|
||||||
|
body: payload.mdoc.content.split('\n').slice(1).join('\n'),
|
||||||
|
} : {
|
||||||
tag: `message-${payload.mdoc._id}`,
|
tag: `message-${payload.mdoc._id}`,
|
||||||
icon: payload.udoc.avatarUrl || '/android-chrome-192x192.png',
|
icon: payload.udoc.avatarUrl || '/android-chrome-192x192.png',
|
||||||
body: payload.mdoc.content,
|
body: payload.mdoc.content,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, 5000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initConn(path: string, port: MessagePort, cookie: any) {
|
function initConn(path: string, port: MessagePort, cookie: any) {
|
||||||
@@ -92,7 +102,7 @@ self.onconnect = function (e) {
|
|||||||
|
|
||||||
port.addEventListener('message', (msg: { data: RequestPayload }) => {
|
port.addEventListener('message', (msg: { data: RequestPayload }) => {
|
||||||
if (msg.data.type === 'conn') initConn(msg.data.path, port, msg.data.cookie);
|
if (msg.data.type === 'conn') initConn(msg.data.path, port, msg.data.cookie);
|
||||||
if (msg.data.type === 'ack') ack[msg.data.id]?.();
|
if (msg.data.type === 'ack') ack[msg.data.id]?.(msg.data.payload);
|
||||||
if (msg.data.type === 'unload') ports.delete(port);
|
if (msg.data.type === 'unload') ports.delete(port);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,11 +38,12 @@ export default class Notification {
|
|||||||
if (avatar) this.type += ' avatar';
|
if (avatar) this.type += ' avatar';
|
||||||
if (title) this.type += ' title';
|
if (title) this.type += ' title';
|
||||||
this.action = action || (() => { });
|
this.action = action || (() => { });
|
||||||
this.$dom = $(tpl`<div class="notification ${type} hide"></div>`);
|
this.$dom = $(tpl`<div class="notification ${this.type} hide"></div>`);
|
||||||
if (avatar) $(tpl`<img width="64" height="64" class="avatar" src="${avatar}"></img>`).appendTo(this.$dom);
|
if (avatar) $(tpl`<img width="32" height="32" class="avatar" src="${avatar}"></img>`).appendTo(this.$dom);
|
||||||
|
const content = message.split('\n').map((line) => tpl`<p>${line}</p>`).join('');
|
||||||
if (title) {
|
if (title) {
|
||||||
$(tpl`<div class="notification-content"><h2>${title}</h2><p>${message}</p></div>`).appendTo(this.$dom);
|
$(tpl`<div class="notification-content"><h2>${title}</h2>${{ templateRaw: true, html: content }}</div>`).appendTo(this.$dom);
|
||||||
} else $(tpl`<p>${message}</p>`).appendTo(this.$dom);
|
} else $(`<div>${content}</div>`).appendTo(this.$dom);
|
||||||
this.$dom.on('click', this.handleClick.bind(this));
|
this.$dom.on('click', this.handleClick.bind(this));
|
||||||
this.$n = this.$dom
|
this.$n = this.$dom
|
||||||
.css('z-index', zIndexManager.getNext())
|
.css('z-index', zIndexManager.getNext())
|
||||||
|
|||||||
@@ -21,6 +21,15 @@
|
|||||||
opacity: 0
|
opacity: 0
|
||||||
transform: translateY(20px) scale(0.9)
|
transform: translateY(20px) scale(0.9)
|
||||||
|
|
||||||
|
&.avatar
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
.avatar
|
||||||
|
display: inline-block
|
||||||
|
vertical-align: top
|
||||||
|
margin-right: rem(10px)
|
||||||
|
|
||||||
&:before
|
&:before
|
||||||
display: inline-block
|
display: inline-block
|
||||||
font-family: $icon-font-name !important
|
font-family: $icon-font-name !important
|
||||||
@@ -30,6 +39,9 @@
|
|||||||
vertical-align: top
|
vertical-align: top
|
||||||
margin-top: rem((20px - $font-size-icon) / 2)
|
margin-top: rem((20px - $font-size-icon) / 2)
|
||||||
|
|
||||||
|
&.avatar:before
|
||||||
|
display: none !important
|
||||||
|
|
||||||
for key, value in $notification-color
|
for key, value in $notification-color
|
||||||
&.{key}
|
&.{key}
|
||||||
border-left: 5px solid value
|
border-left: 5px solid value
|
||||||
@@ -37,7 +49,7 @@
|
|||||||
color: value
|
color: value
|
||||||
|
|
||||||
for key, value in $notification-icon
|
for key, value in $notification-icon
|
||||||
&.{key}:before
|
&:not(.avatar).{key}:before
|
||||||
content: value
|
content: value
|
||||||
|
|
||||||
+mobile()
|
+mobile()
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ async function handleSetColor(tdoc) {
|
|||||||
Notification.error(`${e.message} ${e.params?.[0]}`);
|
Notification.error(`${e.message} ${e.params?.[0]}`);
|
||||||
}
|
}
|
||||||
Notification.info(i18n('Successfully updated.'));
|
Notification.info(i18n('Successfully updated.'));
|
||||||
pjax.request({ url: '', push: false });
|
if ($('[data-fragment-id="constest_balloon-tbody"]').length) pjax.request({ url: '', push: false });
|
||||||
|
else window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = new NamedPage('contest_balloon', () => {
|
const page = new NamedPage('contest_balloon', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user