diff --git a/CHANGELOG.md b/CHANGELOG.md index a46f9c7c8962e01462a127d185a5a4c6c8bdd283..12a73a4c7cee43c206bd433e7338102e8339409c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -234,6 +234,9 @@ ## 12.89.1 (2021/08/24) +### Features +- Added a user-level instance mute in user settings + ### Improvements - クライアントã®ãƒ‡ã‚¶ã‚¤ãƒ³ã®èª¿æ•´ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d5c009bbcfe30ae8abe4a9dd453949c9efdff51b..2a8d0bd9e88d24012db24f2900bac5f98f3a2393 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -592,6 +592,7 @@ smtpSecure: "SMTP 接続ã«æš—黙的ãªSSL/TLSを使用ã™ã‚‹" smtpSecureInfo: "STARTTLS使用時ã¯ã‚ªãƒ•ã«ã—ã¾ã™ã€‚" testEmail: "é…信テスト" wordMute: "ワードミュート" +instanceMute: "インスタンスミュート" userSaysSomething: "{name}ãŒä½•ã‹ã‚’言ã„ã¾ã—ãŸ" makeActive: "アクティブã«ã™ã‚‹" display: "表示" @@ -1021,6 +1022,12 @@ _wordMute: hard: "ãƒãƒ¼ãƒ‰" mutedNotes: "ミュートã•ã‚ŒãŸãƒŽãƒ¼ãƒˆ" +_instanceMute: + instanceMuteDescription: "ミュートã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã‚‰ã®è¿”ä¿¡ã‚’å«ã‚ã¦ã€è¨å®šã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®å…¨ã¦ã®ãƒŽãƒ¼ãƒˆã¨Renoteをミュートã—ã¾ã™ã€‚" + instanceMuteDescription2: "改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™" + title: "è¨å®šã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ãƒŽãƒ¼ãƒˆã‚’éš ã—ã¾ã™ã€‚" + heading: "ミュートã™ã‚‹ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹" + _theme: explore: "テーマを探ã™" install: "テーマã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«" diff --git a/packages/backend/migration/1629968054000_userInstanceBlocks.js b/packages/backend/migration/1629968054000_userInstanceBlocks.js new file mode 100644 index 0000000000000000000000000000000000000000..5703ff0b00e0daf511bb7c1fabf84b94f2d6c06b --- /dev/null +++ b/packages/backend/migration/1629968054000_userInstanceBlocks.js @@ -0,0 +1,15 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +class userInstanceBlocks1629968054000 { + constructor() { + this.name = 'userInstanceBlocks1629968054000'; + } + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutedInstances" jsonb NOT NULL DEFAULT '[]'`); + await queryRunner.query(`COMMENT ON COLUMN "user_profile"."mutedInstances" IS 'List of instances muted by the user.'`); + } + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutedInstances"`); + } +} +exports.userInstanceBlocks1629968054000 = userInstanceBlocks1629968054000; diff --git a/packages/backend/src/misc/is-instance-muted.ts b/packages/backend/src/misc/is-instance-muted.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e1785b51706ab7e1aae4c5e8af9f8e907f91e71 --- /dev/null +++ b/packages/backend/src/misc/is-instance-muted.ts @@ -0,0 +1,15 @@ +import { Packed } from "./schema"; + +export function isInstanceMuted(note: Packed<'Note'>, mutedInstances: Set<string>): boolean { + if (mutedInstances.has(note?.user?.host ?? '')) return true; + if (mutedInstances.has(note?.reply?.user?.host ?? '')) return true; + if (mutedInstances.has(note?.renote?.user?.host ?? '')) return true; + + return false; +} + +export function isUserFromMutedInstance(notif: Packed<'Notification'>, mutedInstances: Set<string>): boolean { + if (mutedInstances.has(notif?.user?.host ?? '')) return true; + + return false; +} diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts index 8a8cacfd52fcc7ae07e83036af7dc3bc9952b7ad..60e16820f4de5a6a74de6c5fb2520eb2af1e8af7 100644 --- a/packages/backend/src/models/entities/user-profile.ts +++ b/packages/backend/src/models/entities/user-profile.ts @@ -189,6 +189,11 @@ export class UserProfile { }) public mutedWords: string[][]; + @Column('jsonb', { + default: [] + }) + public mutedInstances: string[]; + @Column('enum', { enum: notificationTypes, array: true, diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 81468d6de2f4ea4b6dc5d1c14916f29ed5a1ed9f..2f6c150d394ed8f3806c1d4a8c02e11e4904d14d 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -288,6 +288,7 @@ export class UserRepository extends Repository<User> { hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), integrations: profile!.integrations, mutedWords: profile!.mutedWords, + mutedInstances: profile!.mutedInstances, mutingNotificationTypes: profile!.mutingNotificationTypes, emailNotificationTypes: profile!.emailNotificationTypes, } : {}), @@ -623,6 +624,10 @@ export const packedUserSchema = { type: 'array' as const, nullable: false as const, optional: true as const }, + mutedInstances: { + type: 'array' as const, + nullable: false as const, optional: true as const + }, mutingNotificationTypes: { type: 'array' as const, nullable: false as const, optional: true as const diff --git a/packages/backend/src/server/api/common/generate-muted-instance-query.ts b/packages/backend/src/server/api/common/generate-muted-instance-query.ts new file mode 100644 index 0000000000000000000000000000000000000000..dbc9fc98f1e42acdb5f1f61128ed0a5fb2e55b52 --- /dev/null +++ b/packages/backend/src/server/api/common/generate-muted-instance-query.ts @@ -0,0 +1,40 @@ +import { User } from '@/models/entities/user'; +import { id } from '@/models/id'; +import { UserProfiles } from '@/models/index'; +import { SelectQueryBuilder, Brackets } from 'typeorm'; + +function createMutesQuery(id: string) { + return UserProfiles.createQueryBuilder('user_profile') + .select('user_profile.mutedInstances') + .where('user_profile.userId = :muterId', { muterId: id }); +} + +export function generateMutedInstanceQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) { + const mutingQuery = createMutesQuery(me.id); + + q + .andWhere(new Brackets(qb => { qb + .andWhere('note.userHost IS NULL') + .orWhere(`NOT((${ mutingQuery.getQuery() })::jsonb ? note.userHost)`); + })) + .andWhere(new Brackets(qb => { qb + .where(`note.replyUserHost IS NULL`) + .orWhere(`NOT ((${ mutingQuery.getQuery() })::jsonb ? note.replyUserHost)`); + })) + .andWhere(new Brackets(qb => { qb + .where(`note.renoteUserHost IS NULL`) + .orWhere(`NOT ((${ mutingQuery.getQuery() })::jsonb ? note.renoteUserHost)`); + })); + q.setParameters(mutingQuery.getParameters()); +} + +export function generateMutedInstanceNotificationQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) { + const mutingQuery = createMutesQuery(me.id); + + q.andWhere(new Brackets(qb => { qb + .andWhere('notifier.host IS NULL') + .orWhere(`NOT (( ${mutingQuery.getQuery()} )::jsonb ? notifier.host)`); + })); + + q.setParameters(mutingQuery.getParameters()); +} diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 56668d03b75093fe3a5a31737f9666a75f485d25..a85637d8a032ace883b9f5b76729e967c6d7a812 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -3,6 +3,7 @@ import { ID } from '@/misc/cafy-id'; import { readNotification } from '../../common/read-notification'; import define from '../../define'; import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateMutedInstanceNotificationQuery } from '../../common/generate-muted-instance-query'; import { Notifications, Followings, Mutings, Users } from '@/models/index'; import { notificationTypes } from '@/types'; import read from '@/services/note/read'; @@ -101,6 +102,8 @@ export default define(meta, async (ps, user) => { })); query.setParameters(mutingQuery.getParameters()); + generateMutedInstanceNotificationQuery(query, user); + query.andWhere(new Brackets(qb => { qb .where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`) .orWhere('notification.notifierId IS NULL'); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index d0f201ab606d84913914a174ff15f2d0eca4f51f..4c5a4da622471aeb8c1fba43fcce8a06c3cfa753 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -116,6 +116,10 @@ export const meta = { validator: $.optional.arr($.arr($.str)) }, + mutedInstances: { + validator: $.optional.arr($.str) + }, + mutingNotificationTypes: { validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])) }, @@ -185,6 +189,7 @@ export default define(meta, async (ps, _user, token) => { profileUpdates.mutedWords = ps.mutedWords; profileUpdates.enableWordMute = ps.mutedWords.length > 0; } + if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][]; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 68881fda9e1b56ad58fd0e11b292b87901ec2480..49e5a2f84d5b7151366d9ff0f1079297135fe115 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -7,6 +7,7 @@ import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; import { Brackets } from 'typeorm'; import { Notes } from '@/models/index'; import { generateBlockedUserQuery } from '../../common/generate-block-query'; +import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query'; export const meta = { tags: ['notes'], @@ -65,6 +66,7 @@ export default define(meta, async (ps, user) => { generateVisibilityQuery(query, user); if (user) generateMutedUserQuery(query, user); if (user) generateBlockedUserQuery(query, user); + if (user) generateMutedInstanceQuery(query, user); const notes = await query.take(ps.limit!).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 5902c0415cbfcfdca7598073bcbd12685a640796..7c80153b42c08bf514b6aee489df645b512f7fba 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -6,6 +6,7 @@ import { ApiError } from '../../error'; import { makePaginationQuery } from '../../common/make-pagination-query'; import { Notes } from '@/models/index'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; +import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query'; import { activeUsersChart } from '@/services/chart/index'; import { generateRepliesQuery } from '../../common/generate-replies-query'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; @@ -83,6 +84,7 @@ export default define(meta, async (ps, user) => { if (user) generateMutedUserQuery(query, user); if (user) generateMutedNoteQuery(query, user); if (user) generateBlockedUserQuery(query, user); + if (user) generateMutedInstanceQuery(query, user); if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 47f08f208b148b1b804ead53b7a12d358b19a51e..22babb5d0c28e3239b4019178033d697a85c2857 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -8,6 +8,7 @@ import { Followings, Notes } from '@/models/index'; import { Brackets } from 'typeorm'; import { generateVisibilityQuery } from '../../common/generate-visibility-query'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; +import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query'; import { activeUsersChart } from '@/services/chart/index'; import { generateRepliesQuery } from '../../common/generate-replies-query'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; @@ -108,6 +109,7 @@ export default define(meta, async (ps, user) => { generateRepliesQuery(query, user); generateVisibilityQuery(query, user); generateMutedUserQuery(query, user); + generateMutedInstanceQuery(query, user); generateMutedNoteQuery(query, user); generateBlockedUserQuery(query, user); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 1bd0e57d34880585fb2edd0e4e4716004fca6d81..7a69b1590d679553d136ea22fb49d3b49215e757 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -5,6 +5,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query'; import { Notes, Followings } from '@/models/index'; import { generateVisibilityQuery } from '../../common/generate-visibility-query'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; +import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query'; import { activeUsersChart } from '@/services/chart/index'; import { Brackets } from 'typeorm'; import { generateRepliesQuery } from '../../common/generate-replies-query'; @@ -100,6 +101,7 @@ export default define(meta, async (ps, user) => { generateRepliesQuery(query, user); generateVisibilityQuery(query, user); generateMutedUserQuery(query, user); + generateMutedInstanceQuery(query, user); generateMutedNoteQuery(query, user); generateBlockedUserQuery(query, user); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 0afbad9d04661a9033768a14e71cd46ff10d5cb7..e461672534a94d827300eec5e6861d9c7507c87a 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -9,6 +9,7 @@ import { Notes } from '@/models/index'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; import { Brackets } from 'typeorm'; import { generateBlockedUserQuery } from '../../common/generate-block-query'; +import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query'; export const meta = { tags: ['users', 'notes'], @@ -102,6 +103,7 @@ export default define(meta, async (ps, me) => { generateVisibilityQuery(query, me); if (me) generateMutedUserQuery(query, me, user); if (me) generateBlockedUserQuery(query, me); + if (me) generateMutedInstanceQuery(query, me); if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index f5983ab472322e60bcd165625b822424c7e2fe96..3c37b16dd7205faa5d359605e48c251a83ada2a5 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -5,6 +5,7 @@ import { fetchMeta } from '@/misc/fetch-meta'; import { Notes } from '@/models/index'; import { checkWordMute } from '@/misc/check-word-mute'; import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; +import { isInstanceMuted } from '@/misc/is-instance-muted'; import { Packed } from '@/misc/schema'; export default class extends Channel { @@ -48,6 +49,9 @@ export default class extends Channel { if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; } + // Ignore notes from instances the user has muted + if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return; + // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ if (isMutedUserRelated(note, this.muting)) return; // æµã‚Œã¦ããŸNoteãŒãƒ–ãƒãƒƒã‚¯ã•ã‚Œã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 52e9aec250edecdd7c60c923e4121426dd58af1b..24fb3bd40b5c39961290de5fdff5777ea1071cf4 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -4,6 +4,7 @@ import Channel from '../channel'; import { Notes } from '@/models/index'; import { checkWordMute } from '@/misc/check-word-mute'; import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; +import { isInstanceMuted } from '@/misc/is-instance-muted'; import { Packed } from '@/misc/schema'; export default class extends Channel { @@ -26,6 +27,9 @@ export default class extends Channel { if ((this.user!.id !== note.userId) && !this.following.has(note.userId)) return; } + // Ignore notes from instances the user has muted + if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return; + if (['followers', 'specified'].includes(note.visibility)) { note = await Notes.pack(note.id, this.user!, { detail: true diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 51f95fc0cd97efd1e18dfff31dd9d0fc19de4bf7..615cc4540faa274aa71f471dbb7adbf119880e61 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -5,6 +5,7 @@ import { fetchMeta } from '@/misc/fetch-meta'; import { Notes } from '@/models/index'; import { checkWordMute } from '@/misc/check-word-mute'; import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; +import { isInstanceMuted } from '@/misc/is-instance-muted'; import { Packed } from '@/misc/schema'; export default class extends Channel { @@ -57,6 +58,9 @@ export default class extends Channel { } } + // Ignore notes from instances the user has muted + if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return; + // 関係ãªã„返信ã¯é™¤å¤– if (note.reply) { const reply = note.reply; diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 131ac3047230f35ec99fea4a38fd8dd51b05a315..925263aef01dd092ec24a01df2886741ba4c56ef 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -1,6 +1,7 @@ import autobind from 'autobind-decorator'; import Channel from '../channel'; import { Notes } from '@/models/index'; +import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted'; export default class extends Channel { public readonly chName = 'main'; @@ -13,6 +14,8 @@ export default class extends Channel { this.subscriber.on(`mainStream:${this.user!.id}`, async data => { switch (data.type) { case 'notification': { + // Ignore notifications from instances the user has muted + if (isUserFromMutedInstance(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return; if (data.body.userId && this.muting.has(data.body.userId)) return; if (data.body.note && data.body.note.isHidden) { @@ -25,6 +28,8 @@ export default class extends Channel { break; } case 'mention': { + if (isInstanceMuted(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return; + if (this.muting.has(data.body.userId)) return; if (data.body.isHidden) { const note = await Notes.pack(data.body.id, this.user, { diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue index bfac1be77d83dd4c8398eb562635cc435737c8df..2e26870d8b251e80953b26c47b736eaa8cede047 100644 --- a/packages/client/src/pages/settings/index.vue +++ b/packages/client/src/pages/settings/index.vue @@ -136,6 +136,11 @@ export default defineComponent({ text: i18n.locale.importAndExport, to: '/settings/import-export', active: page.value === 'import-export', + }, { + icon: 'fas fa-volume-mute', + text: i18n.locale.instanceMute, + to: '/settings/instance-mute', + active: page.value === 'instance-mute', }, { icon: 'fas fa-ban', text: i18n.locale.muteAndBlock, @@ -190,6 +195,7 @@ export default defineComponent({ case 'notifications': return defineAsyncComponent(() => import('./notifications.vue')); case 'mute-block': return defineAsyncComponent(() => import('./mute-block.vue')); case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue')); + case 'instance-mute': return defineAsyncComponent(() => import('./instance-mute.vue')); case 'integration': return defineAsyncComponent(() => import('./integration.vue')); case 'security': return defineAsyncComponent(() => import('./security.vue')); case '2fa': return defineAsyncComponent(() => import('./2fa.vue')); diff --git a/packages/client/src/pages/settings/instance-mute.vue b/packages/client/src/pages/settings/instance-mute.vue new file mode 100644 index 0000000000000000000000000000000000000000..813d2a04419534a56109ca004d018ce9a31b454d --- /dev/null +++ b/packages/client/src/pages/settings/instance-mute.vue @@ -0,0 +1,83 @@ +<template> + <div> + <FormBase> + <div class="_formItem"> + <FormInfo>{{ $ts._instanceMute.title}}</FormInfo> + <FormTextarea v-model="instanceMutes"> + <span>{{$ts._instanceMute.heading}}</span> + <template #desc>{{ $ts._instanceMute.instanceMuteDescription}}<br>{{$ts._instanceMute.instanceMuteDescription2}}</template> + </FormTextarea> + </div> + <FormButton primary inline :disabled="!changed" @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> + </FormBase> + </div> +</template> + +<script> +import { defineComponent } from 'vue' +import FormBase from '@/components/debobigego/base.vue'; +import FormTextarea from '@/components/debobigego/textarea.vue'; +import FormInfo from '@/components/debobigego/info.vue'; +import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; +import FormButton from '@/components/debobigego/button.vue'; +import * as os from '@/os'; +import number from '@/filters/number'; +import * as symbols from '@/symbols'; + +export default defineComponent({ + components: { + FormBase, + FormButton, + FormTextarea, + FormKeyValueView, + FormInfo, + }, + + emits: ['info'], + + data() { + return { + [symbols.PAGE_INFO]: { + title: this.$ts.instanceMute, + icon: 'fas fa-volume-mute' + }, + tab: 'soft', + instanceMutes: '', + changed: false, + } + }, + + watch: { + instanceMutes: { + handler() { + this.changed = true; + }, + deep: true + }, + }, + + mounted() { + this.$emit('info', this[symbols.PAGE_INFO]); + }, + + + async created() { + this.instanceMutes = this.$i.mutedInstances.join('\n'); + }, + + methods: { + async save() { + let mutes = this.instanceMutes.trim().split('\n').map(el => el.trim()).filter(el => el); + await os.api('i/update', { + mutedInstances: mutes, + }); + this.changed = false; + + // Refresh filtered list to signal to the user how they've been saved + this.instanceMutes = mutes.join('\n'); + }, + + number //? + } +}) +</script>