From 271854e345e5c653c3ec978131563bcbb11cead1 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 4 Mar 2022 16:26:10 +0900 Subject: [PATCH 01/32] enhance(chart): better federation pub/sub calculation --- .../src/services/chart/charts/federation.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/services/chart/charts/federation.ts b/packages/backend/src/services/chart/charts/federation.ts index 4fbd297dbf..97855a634b 100644 --- a/packages/backend/src/services/chart/charts/federation.ts +++ b/packages/backend/src/services/chart/charts/federation.ts @@ -1,6 +1,7 @@ import Chart, { KVs } from '../core.js'; -import { Followings } from '@/models/index.js'; +import { Followings, Instances } from '@/models/index.js'; import { name, schema } from './entities/federation.js'; +import { fetchMeta } from '@/misc/fetch-meta.js'; /** * フェデレーションに関するチャート @@ -17,6 +18,12 @@ export default class FederationChart extends Chart { } protected async tickMinor(): Promise>> { + const meta = await fetchMeta(); + + const suspendedInstancesQuery = Instances.createQueryBuilder('instance') + .select('instance.host') + .where('instance.isSuspended = true'); + const pubsubSubQuery = Followings.createQueryBuilder('f') .select('f.followerHost') .where('f.followerHost IS NOT NULL'); @@ -25,16 +32,22 @@ export default class FederationChart extends Chart { Followings.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') + .andWhere(`following.followeeHost NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) + .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), Followings.createQueryBuilder('following') .select('COUNT(DISTINCT following.followerHost)') .where('following.followerHost IS NOT NULL') + .andWhere(`following.followerHost NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) + .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), Followings.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') + .andWhere(`following.followeeHost NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) + .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) .setParameters(pubsubSubQuery.getParameters()) .getRawOne() From 5448ed643e67b10d19c234698886590f9c1ba9f1 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 4 Mar 2022 16:26:21 +0900 Subject: [PATCH 02/32] tweak client --- packages/client/src/components/chart.vue | 8 ++++---- packages/client/src/store.ts | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue index 3787c5f066..dc1ea2b2d8 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -274,7 +274,7 @@ export default defineComponent({ y: { position: 'left', stacked: props.stacked, - suggestedMax: 100, + suggestedMax: 50, grid: { color: gridColor, borderColor: 'rgb(0, 0, 0, 0)', @@ -390,17 +390,17 @@ export default defineComponent({ color: colors.red, }, { name: 'Pub & Sub', - type: 'area', + type: 'line', data: format(raw.pubsub), color: colors.lime, }, { name: 'Pub', - type: 'area', + type: 'line', data: format(raw.pub), color: colors.purple, }, { name: 'Sub', - type: 'area', + type: 'line', data: format(raw.sub), color: colors.orange, }], diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index e6a2f42906..b9800ec607 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -68,11 +68,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'deviceAccount', default: [ 'notifications', - 'messaging', + 'favorites', 'drive', 'followRequests', '-', - 'gallery', 'featured', 'explore', 'announcements', @@ -192,7 +191,7 @@ export const defaultStore = markRaw(new Storage('base', { }, reactionPickerHeight: { where: 'device', - default: 1 + default: 2 }, reactionPickerUseDrawerForMobile: { where: 'device', From 7f4551b769cfabb86ea715cfbf16ff1fa98b6aa7 Mon Sep 17 00:00:00 2001 From: Andreas Nedbal Date: Fri, 4 Mar 2022 09:13:10 +0100 Subject: [PATCH 03/32] Fix: Only allow admins to access admin views (#8361) * fix(client): only allow admins to access /admin/* * fix(client): Also allow moderators to access admin panel --- packages/client/src/router.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts index ec48b76fdf..839841f0fe 100644 --- a/packages/client/src/router.ts +++ b/packages/client/src/router.ts @@ -3,7 +3,7 @@ import { createRouter, createWebHistory } from 'vue-router'; import MkLoading from '@/pages/_loading_.vue'; import MkError from '@/pages/_error_.vue'; import MkTimeline from '@/pages/timeline.vue'; -import { $i } from './account'; +import { $i, iAmModerator } from './account'; import { ui } from '@/config'; const page = (path: string, ui?: string) => defineAsyncComponent({ @@ -67,8 +67,8 @@ const defaultRoutes = [ { path: '/my/antennas/:antennaId', component: page('my-antennas/edit'), props: true }, { path: '/my/clips', component: page('my-clips/index') }, { path: '/scratchpad', component: page('scratchpad') }, - { path: '/admin/:page(.*)?', component: page('admin/index'), props: route => ({ initialPage: route.params.page || null }) }, - { path: '/admin', component: page('admin/index') }, + { path: '/admin/:page(.*)?', component: iAmModerator ? page('admin/index') : page('not-found'), props: route => ({ initialPage: route.params.page || null }) }, + { path: '/admin', component: iAmModerator ? page('admin/index') : page('not-found') }, { path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) }, { path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) }, { path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) }, From 2939b760d346aa5cd4f59675a5ef2ee460b60b75 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 4 Mar 2022 18:04:39 +0900 Subject: [PATCH 04/32] set timeout for db query Resolve #8150 --- CHANGELOG.md | 2 ++ packages/backend/src/db/postgre.ts | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc8122339..88adeca87b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ You should also include the user name that made the change. - インスタンスデフォルトテーマを設定できるように @syuilo - プロフィールの追加情報を最大16まで保存できるように @syuilo - 連合チャートにPub&Subを追加 @syuilo +- デフォルトで10秒以上時間がかかるデータベースへのクエリは中断されるように @syuilo + - 設定ファイルの`db.extra`に`statement_timeout`を設定することでタイムアウト時間を変更できます ### Bugfixes - Client: リアクションピッカーの高さが低くなったまま戻らないことがあるのを修正 @syuilo diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index c1f7245bc7..066a3c6739 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -184,7 +184,7 @@ export function initDb(justBorrow = false, sync = false, forceRecreate = false) } catch (e) {} } - const log = process.env.NODE_ENV != 'production'; + const log = process.env.NODE_ENV !== 'production'; return createConnection({ type: 'postgres', @@ -193,7 +193,10 @@ export function initDb(justBorrow = false, sync = false, forceRecreate = false) username: config.db.user, password: config.db.pass, database: config.db.db, - extra: config.db.extra, + extra: { + statement_timeout: 1000 * 10, + ...config.db.extra, + }, synchronize: process.env.NODE_ENV === 'test' || sync, dropSchema: process.env.NODE_ENV === 'test' && !justBorrow, cache: !config.db.disableCache ? { From 82f9d5501bc83e428e8d03ee63e733a170440067 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 4 Mar 2022 18:18:37 +0900 Subject: [PATCH 05/32] fix query error --- packages/backend/src/services/chart/charts/federation.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/services/chart/charts/federation.ts b/packages/backend/src/services/chart/charts/federation.ts index 97855a634b..89670194de 100644 --- a/packages/backend/src/services/chart/charts/federation.ts +++ b/packages/backend/src/services/chart/charts/federation.ts @@ -32,21 +32,21 @@ export default class FederationChart extends Chart { Followings.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(`following.followeeHost NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : `following.followeeHost NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), Followings.createQueryBuilder('following') .select('COUNT(DISTINCT following.followerHost)') .where('following.followerHost IS NOT NULL') - .andWhere(`following.followerHost NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : `following.followerHost NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), Followings.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(`following.followeeHost NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) + .andWhere(meta.blockedHosts.length === 0 ? '1=1' : `following.followeeHost NOT IN (:...blocked)`, { blocked: meta.blockedHosts }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) .setParameters(pubsubSubQuery.getParameters()) From e68278f93e82ba396ea2b1fbe0c7e0231c640421 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 4 Mar 2022 20:23:53 +0900 Subject: [PATCH 06/32] =?UTF-8?q?feat:=20=E6=99=82=E9=99=90=E3=83=9F?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #7677 --- CHANGELOG.md | 1 + locales/ja-JP.yml | 6 +++ .../1646387162108-mute-expires-at.js | 13 ++++++ .../backend/src/models/entities/muting.ts | 7 +++ .../backend/src/models/repositories/muting.ts | 1 + packages/backend/src/models/schema/muting.ts | 5 +++ packages/backend/src/queue/index.ts | 5 +++ .../src/queue/processors/db/export-mute.ts | 3 +- .../system/check-expired-mutings.ts | 30 +++++++++++++ .../src/queue/processors/system/index.ts | 2 + .../src/server/api/endpoints/mute/create.ts | 6 +++ packages/client/src/scripts/get-user-menu.ts | 43 ++++++++++++++++--- 12 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 packages/backend/migration/1646387162108-mute-expires-at.js create mode 100644 packages/backend/src/queue/processors/system/check-expired-mutings.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 88adeca87b..0c1d49f2e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ You should also include the user name that made the change. ### Improvements - インスタンスデフォルトテーマを設定できるように @syuilo +- ミュートに期限を設定できるように @syuilo - プロフィールの追加情報を最大16まで保存できるように @syuilo - 連合チャートにPub&Subを追加 @syuilo - デフォルトで10秒以上時間がかかるデータベースへのクエリは中断されるように @syuilo diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 90dc14815e..8f48c2940c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -834,6 +834,12 @@ searchByGoogle: "ググる" instanceDefaultLightTheme: "インスタンスデフォルトのライトテーマ" instanceDefaultDarkTheme: "インスタンスデフォルトのダークテーマ" instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。" +mutePeriod: "ミュートする期限" +indefinitely: "無期限" +tenMinutes: "10分" +oneHour: "1時間" +oneDay: "1日" +oneWeek: "1週間" _emailUnavailable: used: "既に使用されています" diff --git a/packages/backend/migration/1646387162108-mute-expires-at.js b/packages/backend/migration/1646387162108-mute-expires-at.js new file mode 100644 index 0000000000..c8be8f3c54 --- /dev/null +++ b/packages/backend/migration/1646387162108-mute-expires-at.js @@ -0,0 +1,13 @@ +export class muteExpiresAt1646387162108 { + name = 'muteExpiresAt1646387162108' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "muting" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`CREATE INDEX "IDX_c1fd1c3dfb0627aa36c253fd14" ON "muting" ("expiresAt") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_c1fd1c3dfb0627aa36c253fd14"`); + await queryRunner.query(`ALTER TABLE "muting" DROP COLUMN "expiresAt"`); + } +} diff --git a/packages/backend/src/models/entities/muting.ts b/packages/backend/src/models/entities/muting.ts index 8cdd2af9d1..b3a7e7a671 100644 --- a/packages/backend/src/models/entities/muting.ts +++ b/packages/backend/src/models/entities/muting.ts @@ -14,6 +14,13 @@ export class Muting { }) public createdAt: Date; + @Index() + @Column('timestamp with time zone', { + nullable: true, + default: null, + }) + public expiresAt: Date | null; + @Index() @Column({ ...id(), diff --git a/packages/backend/src/models/repositories/muting.ts b/packages/backend/src/models/repositories/muting.ts index 6ffecc302a..643e0b68ee 100644 --- a/packages/backend/src/models/repositories/muting.ts +++ b/packages/backend/src/models/repositories/muting.ts @@ -16,6 +16,7 @@ export class MutingRepository extends Repository { return await awaitAll({ id: muting.id, createdAt: muting.createdAt.toISOString(), + expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null, muteeId: muting.muteeId, mutee: Users.pack(muting.muteeId, me, { detail: true, diff --git a/packages/backend/src/models/schema/muting.ts b/packages/backend/src/models/schema/muting.ts index d75a4fbfed..3ab99e17e7 100644 --- a/packages/backend/src/models/schema/muting.ts +++ b/packages/backend/src/models/schema/muting.ts @@ -12,6 +12,11 @@ export const packedMutingSchema = { optional: false, nullable: false, format: 'date-time', }, + expiresAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, muteeId: { type: 'string', optional: false, nullable: false, diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 94055e9c5a..1b1c21c4f0 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -273,6 +273,11 @@ export default function() { repeat: { cron: '0 0 * * *' }, }); + systemQueue.add('checkExpiredMutings', { + }, { + repeat: { cron: '*/5 * * * *' }, + }); + processSystemQueue(systemQueue); } diff --git a/packages/backend/src/queue/processors/db/export-mute.ts b/packages/backend/src/queue/processors/db/export-mute.ts index 8602e00bff..9fb144abb2 100644 --- a/packages/backend/src/queue/processors/db/export-mute.ts +++ b/packages/backend/src/queue/processors/db/export-mute.ts @@ -7,7 +7,7 @@ import { addFile } from '@/services/drive/add-file.js'; import { format as dateFormat } from 'date-fns'; import { getFullApAccount } from '@/misc/convert-host.js'; import { Users, Mutings } from '@/models/index.js'; -import { MoreThan } from 'typeorm'; +import { IsNull, MoreThan } from 'typeorm'; import { DbUserJobData } from '@/queue/types.js'; const logger = queueLogger.createSubLogger('export-mute'); @@ -40,6 +40,7 @@ export async function exportMute(job: Bull.Job, done: any): Promi const mutes = await Mutings.find({ where: { muterId: user.id, + expiresAt: IsNull(), ...(cursor ? { id: MoreThan(cursor) } : {}), }, take: 100, diff --git a/packages/backend/src/queue/processors/system/check-expired-mutings.ts b/packages/backend/src/queue/processors/system/check-expired-mutings.ts new file mode 100644 index 0000000000..621269e7e1 --- /dev/null +++ b/packages/backend/src/queue/processors/system/check-expired-mutings.ts @@ -0,0 +1,30 @@ +import Bull from 'bull'; +import { In } from 'typeorm'; +import { Mutings } from '@/models/index.js'; +import { queueLogger } from '../../logger.js'; +import { publishUserEvent } from '@/services/stream.js'; + +const logger = queueLogger.createSubLogger('check-expired-mutings'); + +export async function checkExpiredMutings(job: Bull.Job>, done: any): Promise { + logger.info(`Checking expired mutings...`); + + const expired = await Mutings.createQueryBuilder('muting') + .where('muting.expiresAt IS NOT NULL') + .andWhere('muting.expiresAt < :now', { now: new Date() }) + .innerJoinAndSelect('muting.mutee', 'mutee') + .getMany(); + + if (expired.length > 0) { + await Mutings.delete({ + id: In(expired.map(m => m.id)), + }); + + for (const m of expired) { + publishUserEvent(m.muterId, 'unmute', m.mutee!); + } + } + + logger.succ(`All expired mutings checked.`); + done(); +} diff --git a/packages/backend/src/queue/processors/system/index.ts b/packages/backend/src/queue/processors/system/index.ts index dca3249e82..f90f6efafd 100644 --- a/packages/backend/src/queue/processors/system/index.ts +++ b/packages/backend/src/queue/processors/system/index.ts @@ -2,11 +2,13 @@ import Bull from 'bull'; import { tickCharts } from './tick-charts.js'; import { resyncCharts } from './resync-charts.js'; import { cleanCharts } from './clean-charts.js'; +import { checkExpiredMutings } from './check-expired-mutings.js'; const jobs = { tickCharts, resyncCharts, cleanCharts, + checkExpiredMutings, } as Record> | Bull.ProcessPromiseFunction>>; export default function(dbQueue: Bull.Queue>) { diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index 0178aab143..dacee40d01 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -38,6 +38,7 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id' }, + expiresAt: { type: 'integer', nullable: true }, }, required: ['userId'], } as const; @@ -67,10 +68,15 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.alreadyMuting); } + if (ps.expiresAt && ps.expiresAt <= Date.now()) { + return; + } + // Create mute await Mutings.insert({ id: genId(), createdAt: new Date(), + expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, muterId: muter.id, muteeId: mutee.id, } as Muting); diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts index 6d1f25a942..192d14b83e 100644 --- a/packages/client/src/scripts/get-user-menu.ts +++ b/packages/client/src/scripts/get-user-menu.ts @@ -56,11 +56,44 @@ export function getUserMenu(user) { } async function toggleMute() { - os.apiWithDialog(user.isMuted ? 'mute/delete' : 'mute/create', { - userId: user.id - }).then(() => { - user.isMuted = !user.isMuted; - }); + if (user.isMuted) { + os.apiWithDialog('mute/delete', { + userId: user.id, + }).then(() => { + user.isMuted = false; + }); + } else { + const { canceled, result: period } = await os.select({ + title: i18n.ts.mutePeriod, + items: [{ + value: 'indefinitely', text: i18n.ts.indefinitely, + }, { + value: 'tenMinutes', text: i18n.ts.tenMinutes, + }, { + value: 'oneHour', text: i18n.ts.oneHour, + }, { + value: 'oneDay', text: i18n.ts.oneDay, + }, { + value: 'oneWeek', text: i18n.ts.oneWeek, + }], + default: 'indefinitely', + }); + if (canceled) return; + + const expiresAt = period === 'indefinitely' ? null + : period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10) + : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) + : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) + : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) + : null; + + os.apiWithDialog('mute/create', { + userId: user.id, + expiresAt, + }).then(() => { + user.isMuted = true; + }); + } } async function toggleBlock() { From 3e31d1fae9e662011a9ed12b8409890e21caec4f Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 5 Mar 2022 00:35:59 +0900 Subject: [PATCH 07/32] chore(client): hide error report setting Close #8327 --- packages/client/src/pages/settings/other.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/client/src/pages/settings/other.vue b/packages/client/src/pages/settings/other.vue index 6e48cb58a6..a9903acc7e 100644 --- a/packages/client/src/pages/settings/other.vue +++ b/packages/client/src/pages/settings/other.vue @@ -4,7 +4,9 @@ {{ $ts.showFeaturedNotesInTimeline }} + {{ $ts.accountInfo }} From 476dfb0f351c9b879de90fe3d1aef826ab03b82b Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 5 Mar 2022 01:22:57 +0900 Subject: [PATCH 08/32] =?UTF-8?q?fix(client):=20register=5Fnote=5Fview=5Fi?= =?UTF-8?q?nterruptor()=E3=81=8C=E5=8B=95=E3=81=8B=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #8318 --- CHANGELOG.md | 1 + packages/client/src/components/note.vue | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c1d49f2e5..dfb09b6061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ You should also include the user name that made the change. - Client: リアクションピッカーの高さが低くなったまま戻らないことがあるのを修正 @syuilo - Client: ユーザー名オートコンプリートが正しく動作しない問題を修正 @syuilo - Client: タッチ操作だとウィジェットの編集がしにくいのを修正 @xianonn +- Client: register_note_view_interruptor()が動かないのを修正 @syuilo ## 12.107.0 (2022/02/12) diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index 93286a28b2..3cd7a819d4 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -138,7 +138,18 @@ const props = defineProps<{ const inChannel = inject('inChannel', null); -const note = $ref(JSON.parse(JSON.stringify(props.note))); +let note = $ref(JSON.parse(JSON.stringify(props.note))); + +// plugin +if (noteViewInterruptors.length > 0) { + onMounted(async () => { + let result = JSON.parse(JSON.stringify(note)); + for (const interruptor of noteViewInterruptors) { + result = await interruptor.handler(result); + } + note = result; + }); +} const isRenote = ( note.renote != null && @@ -152,7 +163,7 @@ const menuButton = ref(); const renoteButton = ref>(); const renoteTime = ref(); const reactButton = ref(); -let appearNote = $ref(isRenote ? note.renote as misskey.entities.Note : note); +let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); const collapsed = ref(appearNote.cw == null && appearNote.text != null && ( From 5431b5124902deb577bd3bc08e637ba5a15ccdd1 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 5 Mar 2022 01:23:34 +0900 Subject: [PATCH 09/32] fix --- packages/client/src/components/note-detailed.vue | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue index 80233d608a..d30284ca5f 100644 --- a/packages/client/src/components/note-detailed.vue +++ b/packages/client/src/components/note-detailed.vue @@ -154,7 +154,18 @@ const props = defineProps<{ const inChannel = inject('inChannel', null); -const note = $ref(JSON.parse(JSON.stringify(props.note))); +let note = $ref(JSON.parse(JSON.stringify(props.note))); + +// plugin +if (noteViewInterruptors.length > 0) { + onMounted(async () => { + let result = JSON.parse(JSON.stringify(note)); + for (const interruptor of noteViewInterruptors) { + result = await interruptor.handler(result); + } + note = result; + }); +} const isRenote = ( note.renote != null && @@ -168,7 +179,7 @@ const menuButton = ref(); const renoteButton = ref>(); const renoteTime = ref(); const reactButton = ref(); -let appearNote = $ref(isRenote ? note.renote as misskey.entities.Note : note); +let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); const isDeleted = ref(false); From 446e4ce0c3193fe6ead9ec2969424816a2c33a28 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 5 Mar 2022 02:00:45 +0900 Subject: [PATCH 10/32] =?UTF-8?q?fix:=20iPhone=20X=E4=BB=A5=E9=99=8D(=3F)?= =?UTF-8?q?=E3=81=A7=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AE=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E3=81=8C=E5=85=A8=E3=81=A6=E8=A1=A8=E7=A4=BA=E3=81=97=E3=81=8D?= =?UTF-8?q?=E3=82=8C=E3=81=AA=E3=81=84=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20(#8375)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add safe-area-inset-bottom to spacer * fix * :v: * fix --- packages/client/src/components/notification-toast.vue | 2 +- packages/client/src/ui/universal.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/notification-toast.vue b/packages/client/src/components/notification-toast.vue index 4c5783e523..b808647bb4 100644 --- a/packages/client/src/components/notification-toast.vue +++ b/packages/client/src/components/notification-toast.vue @@ -53,7 +53,7 @@ onMounted(() => { } @media (max-width: 500px) { - bottom: 92px; + bottom: calc(env(safe-area-inset-bottom, 0px) + 92px); padding: 0 8px; } diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue index 000fec9b40..a5ff7a6255 100644 --- a/packages/client/src/ui/universal.vue +++ b/packages/client/src/ui/universal.vue @@ -265,7 +265,7 @@ const wallpaper = localStorage.getItem('wallpaper') != null; min-width: 0; > .spacer { - height: 82px; + height: calc(env(safe-area-inset-bottom, 0px) + 96px); @media (min-width: ($widgets-hide-threshold + 1px)) { display: none; From 8bb586c1fde8e90468d1ed22e9fcee2488248ac3 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 5 Mar 2022 02:01:38 +0900 Subject: [PATCH 11/32] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb09b6061..d647b98f23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ You should also include the user name that made the change. - Client: ユーザー名オートコンプリートが正しく動作しない問題を修正 @syuilo - Client: タッチ操作だとウィジェットの編集がしにくいのを修正 @xianonn - Client: register_note_view_interruptor()が動かないのを修正 @syuilo +- Client: iPhone X以降(?)でページの内容が全て表示しきれないのを修正 @tamaina ## 12.107.0 (2022/02/12) From 2442592ef1a51cad901cf5e7fe30cdc85d7568cc Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 6 Mar 2022 16:06:27 +0900 Subject: [PATCH 12/32] =?UTF-8?q?feat:=20=E3=82=A2=E3=83=B3=E3=82=B1?= =?UTF-8?q?=E3=83=BC=E3=83=88=E7=B5=82=E4=BA=86=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #4664 --- CHANGELOG.md | 1 + locales/ja-JP.yml | 2 ++ .../1646549089451-poll-ended-notification.js | 18 ++++++++++ .../src/models/entities/notification.ts | 3 +- .../src/models/repositories/notification.ts | 6 ++++ packages/backend/src/queue/index.ts | 4 ++- .../processors/ended-poll-notification.ts | 33 +++++++++++++++++++ packages/backend/src/queue/queues.ts | 3 +- packages/backend/src/queue/types.ts | 5 +++ packages/backend/src/services/note/create.ts | 10 ++++++ packages/backend/src/types.ts | 2 +- .../client/src/components/notification.vue | 19 +++++++++-- .../client/src/components/notifications.vue | 7 ++-- .../client/src/sw/compose-notification.ts | 5 +++ 14 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 packages/backend/migration/1646549089451-poll-ended-notification.js create mode 100644 packages/backend/src/queue/processors/ended-poll-notification.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d647b98f23..03bad049b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ You should also include the user name that made the change. ### Improvements - インスタンスデフォルトテーマを設定できるように @syuilo - ミュートに期限を設定できるように @syuilo +- アンケートが終了したときに通知が作成されるように @syuilo - プロフィールの追加情報を最大16まで保存できるように @syuilo - 連合チャートにPub&Subを追加 @syuilo - デフォルトで10秒以上時間がかかるデータベースへのクエリは中断されるように @syuilo diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8f48c2940c..13f9efe9b4 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1667,6 +1667,7 @@ _notification: youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" youWereInvitedToGroup: "グループに招待されました" + pollEnded: "アンケートの結果が出ました" _types: all: "すべて" @@ -1677,6 +1678,7 @@ _notification: quote: "引用" reaction: "リアクション" pollVote: "アンケートに投票された" + pollEnded: "アンケートが終了" receiveFollowRequest: "フォロー申請を受け取った" followRequestAccepted: "フォローが受理された" groupInvited: "グループに招待された" diff --git a/packages/backend/migration/1646549089451-poll-ended-notification.js b/packages/backend/migration/1646549089451-poll-ended-notification.js new file mode 100644 index 0000000000..38a38ce64d --- /dev/null +++ b/packages/backend/migration/1646549089451-poll-ended-notification.js @@ -0,0 +1,18 @@ + +export class pollEndedNotification1646549089451 { + name = 'pollEndedNotification1646549089451' + + async up(queryRunner) { + await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`); + await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`); + await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`); + } + + async down(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`); + await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`); + await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`); + await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`); + } +} diff --git a/packages/backend/src/models/entities/notification.ts b/packages/backend/src/models/entities/notification.ts index 4a4739b8c5..db3dba3632 100644 --- a/packages/backend/src/models/entities/notification.ts +++ b/packages/backend/src/models/entities/notification.ts @@ -59,7 +59,8 @@ export class Notification { * renote - (自分または自分がWatchしている)投稿がRenoteされた * quote - (自分または自分がWatchしている)投稿が引用Renoteされた * reaction - (自分または自分がWatchしている)投稿にリアクションされた - * pollVote - (自分または自分がWatchしている)投稿の投票に投票された + * pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された + * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された * groupInvited - グループに招待された diff --git a/packages/backend/src/models/repositories/notification.ts b/packages/backend/src/models/repositories/notification.ts index 441bb79261..8e72d8aabd 100644 --- a/packages/backend/src/models/repositories/notification.ts +++ b/packages/backend/src/models/repositories/notification.ts @@ -67,6 +67,12 @@ export class NotificationRepository extends Repository { }), choice: notification.choice, } : {}), + ...(notification.type === 'pollEnded' ? { + note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, { + detail: true, + _hint_: options._hintForEachNotes_, + }), + } : {}), ...(notification.type === 'groupInvited' ? { invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!), } : {}), diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 1b1c21c4f0..50bcccbb7f 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -8,10 +8,11 @@ import processInbox from './processors/inbox.js'; import processDb from './processors/db/index.js'; import processObjectStorage from './processors/object-storage/index.js'; import processSystemQueue from './processors/system/index.js'; +import { endedPollNotification } from './processors/ended-poll-notification.js'; import { queueLogger } from './logger.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { getJobInfo } from './get-job-info.js'; -import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues.js'; +import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue } from './queues.js'; import { ThinUser } from './types.js'; import { IActivity } from '@/remote/activitypub/type.js'; @@ -255,6 +256,7 @@ export default function() { deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver); inboxQueue.process(config.inboxJobConcurrency || 16, processInbox); + endedPollNotificationQueue.process(endedPollNotification); processDb(dbQueue); processObjectStorage(objectStorageQueue); diff --git a/packages/backend/src/queue/processors/ended-poll-notification.ts b/packages/backend/src/queue/processors/ended-poll-notification.ts new file mode 100644 index 0000000000..afac27921f --- /dev/null +++ b/packages/backend/src/queue/processors/ended-poll-notification.ts @@ -0,0 +1,33 @@ +import Bull from 'bull'; +import { In } from 'typeorm'; +import { Notes, Polls, PollVotes } from '@/models/index.js'; +import { queueLogger } from '../logger.js'; +import { EndedPollNotificationJobData } from '@/queue/types.js'; +import { createNotification } from '@/services/create-notification.js'; + +const logger = queueLogger.createSubLogger('ended-poll-notification'); + +export async function endedPollNotification(job: Bull.Job, done: any): Promise { + const note = await Notes.findOne(job.data.noteId); + if (note == null || !note.hasPoll) { + done(); + return; + } + + const votes = await PollVotes.createQueryBuilder('vote') + .select('vote.userId') + .where('vote.noteId = :noteId', { noteId: note.id }) + .innerJoinAndSelect('vote.user', 'user') + .andWhere('user.host IS NULL') + .getMany(); + + const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])]; + + for (const userId of userIds) { + createNotification(userId, 'pollEnded', { + noteId: note.id, + }); + } + + done(); +} diff --git a/packages/backend/src/queue/queues.ts b/packages/backend/src/queue/queues.ts index 6ac4ec69cf..02df587365 100644 --- a/packages/backend/src/queue/queues.ts +++ b/packages/backend/src/queue/queues.ts @@ -1,8 +1,9 @@ import config from '@/config/index.js'; import { initialize as initializeQueue } from './initialize.js'; -import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData } from './types.js'; +import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData } from './types.js'; export const systemQueue = initializeQueue>('system'); +export const endedPollNotificationQueue = initializeQueue('endedPollNotification'); export const deliverQueue = initializeQueue('deliver', config.deliverJobPerSec || 128); export const inboxQueue = initializeQueue('inbox', config.inboxJobPerSec || 16); export const dbQueue = initializeQueue('db'); diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index de5f5d1396..5191caea4c 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -1,4 +1,5 @@ import { DriveFile } from '@/models/entities/drive-file.js'; +import { Note } from '@/models/entities/note'; import { User } from '@/models/entities/user.js'; import { IActivity } from '@/remote/activitypub/type.js'; import httpSignature from 'http-signature'; @@ -41,6 +42,10 @@ export type ObjectStorageFileJobData = { key: string; }; +export type EndedPollNotificationJobData = { + noteId: Note['id']; +}; + export type ThinUser = { id: User['id']; }; diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index ed242a0b59..8c5f133628 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -34,6 +34,7 @@ import { deliverToRelays } from '../relay.js'; import { Channel } from '@/models/entities/channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { getAntennas } from '@/misc/antenna-cache.js'; +import { endedPollNotificationQueue } from '@/queue/queues.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -296,6 +297,15 @@ export default async (user: { id: User['id']; username: User['username']; host: incRenoteCount(data.renote); } + if (data.poll && data.poll.expiresAt) { + const delay = data.poll.expiresAt.getTime() - Date.now(); + endedPollNotificationQueue.add({ + noteId: note.id, + }, { + delay + }); + } + if (!silent) { if (Users.isLocalUser(user)) activeUsersChart.write(user); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 20f6f8bb88..573e2faf87 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -1,4 +1,4 @@ -export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const; +export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; diff --git a/packages/client/src/components/notification.vue b/packages/client/src/components/notification.vue index d855f81f8a..1a360f9905 100644 --- a/packages/client/src/components/notification.vue +++ b/packages/client/src/components/notification.vue @@ -1,7 +1,8 @@