feat: user-level instance mute (#7712)
* Update ja-JP.yml * Added settable config for muted instances * added psql query for removal of muted notes * Added filtering and trimming for instance mutes * cleaned up filtering of bad instance mutes and added a refresh at the end for the list on the client * Added notification & streaming timeline muting * Updated changelog * Added missing semicolon * Apply japanese string suggestions from robflop Co-authored-by: Robin B. <robflop98@outlook.com> * Changed Ja-JP instance mute title string to one suggested by sousuke Co-authored-by: sousuke0422 <sousuke20xx@gmail.com> * Update ja-JP instanceMuteDescription based on sousuke's suggestion Co-authored-by: sousuke0422 <sousuke20xx@gmail.com> * added notification mute * added notification and note children muting * Fixed a bug where local notifications were getting filtered on cold start * Fixed instance mute imports * Fixed not saving/loading instance mutes * removed en-US translations for instance mute * moved instance mute migration to js * changed settings index back to spaces * removed destructuring assignment from notification stream in instance mute check call Co-authored-by: tamaina <tamaina@hotmail.co.jp> * added .note accessor for checking note data instead of notification data * changed note to use Packed<'Note'> instead of any and removed usage of snake case Co-authored-by: tamaina <tamaina@hotmail.co.jp> * changed notification mute check to check specifically for notification host * changed to using single quotes * moved @click to the end for the linter * revert unnecessary changes * restored newlines * whitespace removal Co-authored-by: syuilo <syuilotan@yahoo.co.jp> Co-authored-by: Robin B. <robflop98@outlook.com> Co-authored-by: sousuke0422 <sousuke20xx@gmail.com> Co-authored-by: puffaboo <emilis@jigglypuff.club> Co-authored-by: tamaina <tamaina@hotmail.co.jp>
This commit is contained in:
		
							parent
							
								
									b9095995eb
								
							
						
					
					
						commit
						054417354c
					
				
					 20 changed files with 214 additions and 0 deletions
				
			
		|  | @ -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; | ||||
							
								
								
									
										15
									
								
								packages/backend/src/misc/is-instance-muted.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								packages/backend/src/misc/is-instance-muted.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
| } | ||||
|  | @ -189,6 +189,11 @@ export class UserProfile { | |||
| 	}) | ||||
| 	public mutedWords: string[][]; | ||||
| 
 | ||||
| 	@Column('jsonb', { | ||||
| 		default: [] | ||||
| 	}) | ||||
| 	public mutedInstances: string[]; | ||||
| 
 | ||||
| 	@Column('enum', { | ||||
| 		enum: notificationTypes, | ||||
| 		array: true, | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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()); | ||||
| } | ||||
|  | @ -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'); | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 != \'{}\''); | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 != \'{}\''); | ||||
|  |  | |||
|  | @ -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がブロックされているユーザーが関わるものだったら無視する
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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, { | ||||
|  |  | |||
|  | @ -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')); | ||||
|  |  | |||
							
								
								
									
										83
									
								
								packages/client/src/pages/settings/instance-mute.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								packages/client/src/pages/settings/instance-mute.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue