Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hard mute #12376

Merged
merged 12 commits into from
Nov 23, 2023
1 change: 1 addition & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ export interface Locale {
"smtpSecureInfo": string;
"testEmail": string;
"wordMute": string;
"hardWordMute": string;
"regexpError": string;
"regexpErrorDescription": string;
"instanceMute": string;
Expand Down
1 change: 1 addition & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使用時はオフにします。"
testEmail: "配信テスト"
wordMute: "ワードミュート"
hardWordMute: "ハードワードミュート"
regexpError: "正規表現エラー"
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
instanceMute: "サーバーミュート"
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/migration/1700383825690-hard-mute.js
Original file line number Diff line number Diff line change
@@ -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"`);
}
}
1 change: 1 addition & 0 deletions packages/backend/src/core/entities/UserEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion packages/backend/src/models/UserProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down
12 changes: 12 additions & 0 deletions packages/backend/src/models/json-schema/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
34 changes: 27 additions & 7 deletions packages/backend/src/server/api/endpoints/i/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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',
} },
Expand Down Expand Up @@ -234,28 +240,42 @@ 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;
Copy link
Sponsor Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここps.mutedWordsではなくmutedWordsを参照しなければならない気がします

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そうですね。修正します

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 {
new RE2(regexp[1], regexp[2]);
} 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) {
anatawa12 marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand Down
1 change: 1 addition & 0 deletions packages/backend/test/e2e/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 14 additions & 3 deletions packages/frontend/src/components/MkNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)">
Expand Down Expand Up @@ -183,6 +183,7 @@ const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
pinned?: boolean;
mock?: boolean;
withHardMute?: boolean;
}>(), {
mock: false,
});
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkNotes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkNotifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
18 changes: 17 additions & 1 deletion packages/frontend/src/pages/settings/mute-block.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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(() => []);
Expand Down
22 changes: 10 additions & 12 deletions packages/frontend/src/pages/settings/mute-block.word-mute.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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, () => {
Expand Down Expand Up @@ -85,9 +85,7 @@ async function save() {
return;
}

await os.api('i/update', {
mutedWords: parsed,
});
emit('save', parsed);

changed.value = false;
}
Expand Down
12 changes: 7 additions & 5 deletions packages/misskey-js/etc/misskey-js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion packages/misskey-js/src/api.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
3 changes: 2 additions & 1 deletion packages/misskey-js/src/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading