summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authoranatawa12 <anatawa12@icloud.com>2023-11-23 18:56:20 +0900
committerGitHub <noreply@github.com>2023-11-23 18:56:20 +0900
commit864827f788cd1671a4db2ebc159c1c8ab031b7ad (patch)
tree689fa67a1f438cc3410446cd893397d91c2d6725 /packages
parent絵文字のオートコンプリート強化の対応 (#12365) (diff)
downloadsharkey-864827f788cd1671a4db2ebc159c1c8ab031b7ad.tar.gz
sharkey-864827f788cd1671a4db2ebc159c1c8ab031b7ad.tar.bz2
sharkey-864827f788cd1671a4db2ebc159c1c8ab031b7ad.zip
Hard mute (#12376)
* feat(backend,misskey-js): hard mute storage in backend * fix(backend,misskey-js): mute word record type * chore(frontend): generalize XWordMute * feat(frontend): configure hard mute * feat(frontend): hard mute notes on the timelines * lint(backend,frontend): fix lint failure * chore(misskey-js): update api.md * fix(backend): test failure * chore(frontend): check word mute for reply * chore: limit hard mute count
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/migration/1700383825690-hard-mute.js11
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts1
-rw-r--r--packages/backend/src/models/UserProfile.ts7
-rw-r--r--packages/backend/src/models/json-schema/user.ts12
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts34
-rw-r--r--packages/backend/test/e2e/users.ts1
-rw-r--r--packages/frontend/src/components/MkNote.vue17
-rw-r--r--packages/frontend/src/components/MkNotes.vue2
-rw-r--r--packages/frontend/src/components/MkNotifications.vue2
-rw-r--r--packages/frontend/src/pages/settings/mute-block.vue18
-rw-r--r--packages/frontend/src/pages/settings/mute-block.word-mute.vue22
-rw-r--r--packages/misskey-js/etc/misskey-js.api.md12
-rw-r--r--packages/misskey-js/src/api.types.ts3
-rw-r--r--packages/misskey-js/src/entities.ts3
14 files changed, 112 insertions, 33 deletions
diff --git a/packages/backend/migration/1700383825690-hard-mute.js b/packages/backend/migration/1700383825690-hard-mute.js
new file mode 100644
index 0000000000..afd3247f5c
--- /dev/null
+++ b/packages/backend/migration/1700383825690-hard-mute.js
@@ -0,0 +1,11 @@
+export class HardMute1700383825690 {
+ name = 'HardMute1700383825690'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`);
+ }
+}
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 17e7988176..917f4e06d0 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -473,6 +473,7 @@ export class UserEntityService implements OnModuleInit {
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
unreadNotificationsCount: notificationsInfo?.unreadCount,
mutedWords: profile!.mutedWords,
+ hardMutedWords: profile!.hardMutedWords,
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: [], // 後方互換性のため
notificationRecieveConfig: profile!.notificationRecieveConfig,
diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts
index d6d85c5609..8a43b60039 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -215,7 +215,12 @@ export class MiUserProfile {
@Column('jsonb', {
default: [],
})
- public mutedWords: string[][];
+ public mutedWords: (string[] | string)[];
+
+ @Column('jsonb', {
+ default: [],
+ })
+ public hardMutedWords: (string[] | string)[];
@Column('jsonb', {
default: [],
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index b0e18db01a..a2ec203e96 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -530,6 +530,18 @@ export const packedMeDetailedOnlySchema = {
},
},
},
+ hardMutedWords: {
+ type: 'array',
+ nullable: false, optional: false,
+ items: {
+ type: 'array',
+ nullable: false, optional: false,
+ items: {
+ type: 'string',
+ nullable: false, optional: false,
+ },
+ },
+ },
mutedInstances: {
type: 'array',
nullable: true, optional: false,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index b00aa87bee..8ba29c5658 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -123,6 +123,11 @@ export const meta = {
},
} as const;
+const muteWords = { type: 'array', items: { oneOf: [
+ { type: 'array', items: { type: 'string' } },
+ { type: 'string' }
+] } } as const;
+
export const paramDef = {
type: 'object',
properties: {
@@ -171,7 +176,8 @@ export const paramDef = {
autoSensitive: { type: 'boolean' },
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
- mutedWords: { type: 'array' },
+ mutedWords: muteWords,
+ hardMutedWords: muteWords,
mutedInstances: { type: 'array', items: {
type: 'string',
} },
@@ -234,16 +240,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
- if (ps.mutedWords !== undefined) {
+
+ function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
// TODO: ちゃんと数える
const length = JSON.stringify(ps.mutedWords).length;
- if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) {
+ if (length > limit) {
throw new ApiError(meta.errors.tooManyMutedWords);
}
+ }
+
+ function validateMuteWordRegex(mutedWords: (string[] | string)[]) {
+ for (const mutedWord of mutedWords) {
+ if (typeof mutedWord !== "string") continue;
- // validate regular expression syntax
- ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => {
- const regexp = x.match(/^\/(.+)\/(.*)$/);
+ const regexp = mutedWord.match(/^\/(.+)\/(.*)$/);
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
try {
@@ -251,11 +261,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} catch (err) {
throw new ApiError(meta.errors.invalidRegexp);
}
- });
+ }
+ }
+
+ if (ps.mutedWords !== undefined) {
+ checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
+ validateMuteWordRegex(ps.mutedWords);
profileUpdates.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
}
+ if (ps.hardMutedWords !== undefined) {
+ checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
+ validateMuteWordRegex(ps.hardMutedWords);
+ profileUpdates.hardMutedWords = ps.hardMutedWords;
+ }
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index 1867525cc8..2ce8fbc129 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -168,6 +168,7 @@ describe('ユーザー', () => {
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
unreadAnnouncements: user.unreadAnnouncements,
mutedWords: user.mutedWords,
+ hardMutedWords: user.hardMutedWords,
mutedInstances: user.mutedInstances,
mutingNotificationTypes: user.mutingNotificationTypes,
notificationRecieveConfig: user.notificationRecieveConfig,
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index e300ef88a5..6349df2e30 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
- v-if="!muted"
+ v-if="!hardMuted && !muted"
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
@@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</article>
</div>
-<div v-else :class="$style.muted" @click="muted = false">
+<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
@@ -183,6 +183,7 @@ const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
pinned?: boolean;
mock?: boolean;
+ withHardMute?: boolean;
}>(), {
mock: false,
});
@@ -239,13 +240,23 @@ const urls = $computed(() => parsed ? extractUrlFromMfm(parsed) : null);
const isLong = shouldCollapsed(appearNote, urls ?? []);
const collapsed = ref(appearNote.cw == null && isLong);
const isDeleted = ref(false);
-const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
+const muted = ref(checkMute(appearNote, $i?.mutedWords));
+const hardMuted = ref(props.withHardMute && checkMute(appearNote, $i?.hardMutedWords));
const translation = ref<any>(null);
const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
+function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
+ if (mutedWords == null) return false;
+
+ if (checkWordMute(note, $i, mutedWords)) return true;
+ if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
+ if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
+ return false;
+}
+
const keymap = {
'r': () => reply(true),
'e|a|plus': () => react(true),
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 89fd504dcc..7af31074db 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:ad="true"
:class="$style.notes"
>
- <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
+ <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
</MkDateSeparatedList>
</div>
</template>
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 0c817bd64c..7b072fa492 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notifications }">
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
- <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
+ <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList>
</template>
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index c6cbd424ec..4883ca0df4 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -9,7 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.wordMute }}</template>
- <XWordMute/>
+ <XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/>
+ </MkFolder>
+
+ <MkFolder>
+ <template #icon><i class="ti ti-message-off"></i></template>
+ <template #label>{{ i18n.ts.hardWordMute }}</template>
+
+ <XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/>
</MkFolder>
<MkFolder>
@@ -129,6 +136,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import * as os from '@/os.js';
import { infoImageUrl } from '@/instance.js';
+import { $i } from '@/account.js';
import MkFolder from '@/components/MkFolder.vue';
const renoteMutingPagination = {
@@ -207,6 +215,14 @@ async function toggleBlockItem(item) {
}
}
+async function saveMutedWords(mutedWords: (string | string[])[]) {
+ await os.api('i/update', { mutedWords });
+}
+
+async function saveHardMutedWords(hardMutedWords: (string | string[])[]) {
+ await os.api('i/update', { hardMutedWords });
+}
+
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
diff --git a/packages/frontend/src/pages/settings/mute-block.word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
index 25a836ea55..7328967c51 100644
--- a/packages/frontend/src/pages/settings/mute-block.word-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
@@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, watch } from 'vue';
import MkTextarea from '@/components/MkTextarea.vue';
-import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import MkTab from '@/components/MkTab.vue';
import * as os from '@/os.js';
-import number from '@/filters/number.js';
-import { defaultStore } from '@/store.js';
-import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+
+const props = defineProps<{
+ muted: (string[] | string)[];
+}>();
+
+const emit = defineEmits<{
+ (ev: 'save', value: (string[] | string)[]): void;
+}>();
const render = (mutedWords) => mutedWords.map(x => {
if (Array.isArray(x)) {
@@ -37,8 +38,7 @@ const render = (mutedWords) => mutedWords.map(x => {
}
}).join('\n');
-const tab = ref('soft');
-const mutedWords = ref(render($i!.mutedWords));
+const mutedWords = ref(render(props.muted));
const changed = ref(false);
watch(mutedWords, () => {
@@ -85,9 +85,7 @@ async function save() {
return;
}
- await os.api('i/update', {
- mutedWords: parsed,
- });
+ emit('save', parsed);
changed.value = false;
}
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 63c3cb71a5..dc93c4be3b 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -1565,7 +1565,8 @@ export type Endpoints = {
injectFeaturedNote?: boolean;
receiveAnnouncementEmail?: boolean;
alwaysMarkNsfw?: boolean;
- mutedWords?: string[][];
+ mutedWords?: (string[] | string)[];
+ hardMutedWords?: (string[] | string)[];
notificationRecieveConfig?: any;
emailNotificationTypes?: string[];
alsoKnownAs?: string[];
@@ -2516,7 +2517,8 @@ type MeDetailed = UserDetailed & {
integrations: Record<string, any>;
isDeleted: boolean;
isExplorable: boolean;
- mutedWords: string[][];
+ mutedWords: (string[] | string)[];
+ hardMutedWords: (string[] | string)[];
notificationRecieveConfig: {
[notificationType in typeof notificationTypes_2[number]]?: {
type: 'all';
@@ -3053,9 +3055,9 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
//
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
// src/api.types.ts:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
-// src/api.types.ts:634:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
-// src/entities.ts:116:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
-// src/entities.ts:627:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
+// src/api.types.ts:635:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
+// src/entities.ts:117:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
+// src/entities.ts:628:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
index 1a75b7cf57..ba333231e5 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -432,7 +432,8 @@ export type Endpoints = {
injectFeaturedNote?: boolean;
receiveAnnouncementEmail?: boolean;
alwaysMarkNsfw?: boolean;
- mutedWords?: string[][];
+ mutedWords?: (string[] | string)[];
+ hardMutedWords?: (string[] | string)[];
notificationRecieveConfig?: any;
emailNotificationTypes?: string[];
alsoKnownAs?: string[];
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index a51315b13b..2ddcf93f5d 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -112,7 +112,8 @@ export type MeDetailed = UserDetailed & {
integrations: Record<string, any>;
isDeleted: boolean;
isExplorable: boolean;
- mutedWords: string[][];
+ mutedWords: (string[] | string)[];
+ hardMutedWords: (string[] | string)[];
notificationRecieveConfig: {
[notificationType in typeof notificationTypes[number]]?: {
type: 'all';