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
				
			
		|  | @ -234,6 +234,9 @@ | ||||||
| 
 | 
 | ||||||
| ## 12.89.1 (2021/08/24) | ## 12.89.1 (2021/08/24) | ||||||
| 
 | 
 | ||||||
|  | ### Features | ||||||
|  | - Added a user-level instance mute in user settings | ||||||
|  | 
 | ||||||
| ### Improvements | ### Improvements | ||||||
| - クライアントのデザインの調整 | - クライアントのデザインの調整 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -592,6 +592,7 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" | ||||||
| smtpSecureInfo: "STARTTLS使用時はオフにします。" | smtpSecureInfo: "STARTTLS使用時はオフにします。" | ||||||
| testEmail: "配信テスト" | testEmail: "配信テスト" | ||||||
| wordMute: "ワードミュート" | wordMute: "ワードミュート" | ||||||
|  | instanceMute: "インスタンスミュート" | ||||||
| userSaysSomething: "{name}が何かを言いました" | userSaysSomething: "{name}が何かを言いました" | ||||||
| makeActive: "アクティブにする" | makeActive: "アクティブにする" | ||||||
| display: "表示" | display: "表示" | ||||||
|  | @ -1021,6 +1022,12 @@ _wordMute: | ||||||
|   hard: "ハード" |   hard: "ハード" | ||||||
|   mutedNotes: "ミュートされたノート" |   mutedNotes: "ミュートされたノート" | ||||||
| 
 | 
 | ||||||
|  | _instanceMute: | ||||||
|  |   instanceMuteDescription: "ミュートしたインスタンスのユーザーからの返信を含めて、設定したインスタンスの全てのノートとRenoteをミュートします。" | ||||||
|  |   instanceMuteDescription2: "改行で区切って設定します" | ||||||
|  |   title: "設定したインスタンスのノートを隠します。" | ||||||
|  |   heading: "ミュートするインスタンス" | ||||||
|  | 
 | ||||||
| _theme: | _theme: | ||||||
|   explore: "テーマを探す" |   explore: "テーマを探す" | ||||||
|   install: "テーマのインストール" |   install: "テーマのインストール" | ||||||
|  |  | ||||||
|  | @ -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[][]; | 	public mutedWords: string[][]; | ||||||
| 
 | 
 | ||||||
|  | 	@Column('jsonb', { | ||||||
|  | 		default: [] | ||||||
|  | 	}) | ||||||
|  | 	public mutedInstances: string[]; | ||||||
|  | 
 | ||||||
| 	@Column('enum', { | 	@Column('enum', { | ||||||
| 		enum: notificationTypes, | 		enum: notificationTypes, | ||||||
| 		array: true, | 		array: true, | ||||||
|  |  | ||||||
|  | @ -288,6 +288,7 @@ export class UserRepository extends Repository<User> { | ||||||
| 				hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), | 				hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), | ||||||
| 				integrations: profile!.integrations, | 				integrations: profile!.integrations, | ||||||
| 				mutedWords: profile!.mutedWords, | 				mutedWords: profile!.mutedWords, | ||||||
|  | 				mutedInstances: profile!.mutedInstances, | ||||||
| 				mutingNotificationTypes: profile!.mutingNotificationTypes, | 				mutingNotificationTypes: profile!.mutingNotificationTypes, | ||||||
| 				emailNotificationTypes: profile!.emailNotificationTypes, | 				emailNotificationTypes: profile!.emailNotificationTypes, | ||||||
| 			} : {}), | 			} : {}), | ||||||
|  | @ -623,6 +624,10 @@ export const packedUserSchema = { | ||||||
| 			type: 'array' as const, | 			type: 'array' as const, | ||||||
| 			nullable: false as const, optional: true as const | 			nullable: false as const, optional: true as const | ||||||
| 		}, | 		}, | ||||||
|  | 		mutedInstances: { | ||||||
|  | 			type: 'array' as const, | ||||||
|  | 			nullable: false as const, optional: true as const | ||||||
|  | 		}, | ||||||
| 		mutingNotificationTypes: { | 		mutingNotificationTypes: { | ||||||
| 			type: 'array' as const, | 			type: 'array' as const, | ||||||
| 			nullable: false as const, optional: true 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 { readNotification } from '../../common/read-notification'; | ||||||
| import define from '../../define'; | import define from '../../define'; | ||||||
| import { makePaginationQuery } from '../../common/make-pagination-query'; | import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||||
|  | import { generateMutedInstanceNotificationQuery } from '../../common/generate-muted-instance-query'; | ||||||
| import { Notifications, Followings, Mutings, Users } from '@/models/index'; | import { Notifications, Followings, Mutings, Users } from '@/models/index'; | ||||||
| import { notificationTypes } from '@/types'; | import { notificationTypes } from '@/types'; | ||||||
| import read from '@/services/note/read'; | import read from '@/services/note/read'; | ||||||
|  | @ -101,6 +102,8 @@ export default define(meta, async (ps, user) => { | ||||||
| 	})); | 	})); | ||||||
| 	query.setParameters(mutingQuery.getParameters()); | 	query.setParameters(mutingQuery.getParameters()); | ||||||
| 
 | 
 | ||||||
|  | 	generateMutedInstanceNotificationQuery(query, user); | ||||||
|  | 
 | ||||||
| 	query.andWhere(new Brackets(qb => { qb | 	query.andWhere(new Brackets(qb => { qb | ||||||
| 		.where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`) | 		.where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`) | ||||||
| 		.orWhere('notification.notifierId IS NULL'); | 		.orWhere('notification.notifierId IS NULL'); | ||||||
|  |  | ||||||
|  | @ -116,6 +116,10 @@ export const meta = { | ||||||
| 			validator: $.optional.arr($.arr($.str)) | 			validator: $.optional.arr($.arr($.str)) | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		mutedInstances: { | ||||||
|  | 			validator: $.optional.arr($.str) | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		mutingNotificationTypes: { | 		mutingNotificationTypes: { | ||||||
| 			validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])) | 			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.mutedWords = ps.mutedWords; | ||||||
| 		profileUpdates.enableWordMute = ps.mutedWords.length > 0; | 		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 (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][]; | ||||||
| 	if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; | 	if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; | ||||||
| 	if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; | 	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 { Brackets } from 'typeorm'; | ||||||
| import { Notes } from '@/models/index'; | import { Notes } from '@/models/index'; | ||||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||||
|  | import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	tags: ['notes'], | 	tags: ['notes'], | ||||||
|  | @ -65,6 +66,7 @@ export default define(meta, async (ps, user) => { | ||||||
| 	generateVisibilityQuery(query, user); | 	generateVisibilityQuery(query, user); | ||||||
| 	if (user) generateMutedUserQuery(query, user); | 	if (user) generateMutedUserQuery(query, user); | ||||||
| 	if (user) generateBlockedUserQuery(query, user); | 	if (user) generateBlockedUserQuery(query, user); | ||||||
|  | 	if (user) generateMutedInstanceQuery(query, user); | ||||||
| 
 | 
 | ||||||
| 	const notes = await query.take(ps.limit!).getMany(); | 	const notes = await query.take(ps.limit!).getMany(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import { ApiError } from '../../error'; | ||||||
| import { makePaginationQuery } from '../../common/make-pagination-query'; | import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||||
| import { Notes } from '@/models/index'; | import { Notes } from '@/models/index'; | ||||||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||||
|  | import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query'; | ||||||
| import { activeUsersChart } from '@/services/chart/index'; | import { activeUsersChart } from '@/services/chart/index'; | ||||||
| import { generateRepliesQuery } from '../../common/generate-replies-query'; | import { generateRepliesQuery } from '../../common/generate-replies-query'; | ||||||
| import { generateMutedNoteQuery } from '../../common/generate-muted-note-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) generateMutedUserQuery(query, user); | ||||||
| 	if (user) generateMutedNoteQuery(query, user); | 	if (user) generateMutedNoteQuery(query, user); | ||||||
| 	if (user) generateBlockedUserQuery(query, user); | 	if (user) generateBlockedUserQuery(query, user); | ||||||
|  | 	if (user) generateMutedInstanceQuery(query, user); | ||||||
| 
 | 
 | ||||||
| 	if (ps.withFiles) { | 	if (ps.withFiles) { | ||||||
| 		query.andWhere('note.fileIds != \'{}\''); | 		query.andWhere('note.fileIds != \'{}\''); | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import { Followings, Notes } from '@/models/index'; | ||||||
| import { Brackets } from 'typeorm'; | import { Brackets } from 'typeorm'; | ||||||
| import { generateVisibilityQuery } from '../../common/generate-visibility-query'; | import { generateVisibilityQuery } from '../../common/generate-visibility-query'; | ||||||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||||
|  | import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query'; | ||||||
| import { activeUsersChart } from '@/services/chart/index'; | import { activeUsersChart } from '@/services/chart/index'; | ||||||
| import { generateRepliesQuery } from '../../common/generate-replies-query'; | import { generateRepliesQuery } from '../../common/generate-replies-query'; | ||||||
| import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; | import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; | ||||||
|  | @ -108,6 +109,7 @@ export default define(meta, async (ps, user) => { | ||||||
| 	generateRepliesQuery(query, user); | 	generateRepliesQuery(query, user); | ||||||
| 	generateVisibilityQuery(query, user); | 	generateVisibilityQuery(query, user); | ||||||
| 	generateMutedUserQuery(query, user); | 	generateMutedUserQuery(query, user); | ||||||
|  | 	generateMutedInstanceQuery(query, user); | ||||||
| 	generateMutedNoteQuery(query, user); | 	generateMutedNoteQuery(query, user); | ||||||
| 	generateBlockedUserQuery(query, user); | 	generateBlockedUserQuery(query, user); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||||
| import { Notes, Followings } from '@/models/index'; | import { Notes, Followings } from '@/models/index'; | ||||||
| import { generateVisibilityQuery } from '../../common/generate-visibility-query'; | import { generateVisibilityQuery } from '../../common/generate-visibility-query'; | ||||||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||||
|  | import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query'; | ||||||
| import { activeUsersChart } from '@/services/chart/index'; | import { activeUsersChart } from '@/services/chart/index'; | ||||||
| import { Brackets } from 'typeorm'; | import { Brackets } from 'typeorm'; | ||||||
| import { generateRepliesQuery } from '../../common/generate-replies-query'; | import { generateRepliesQuery } from '../../common/generate-replies-query'; | ||||||
|  | @ -100,6 +101,7 @@ export default define(meta, async (ps, user) => { | ||||||
| 	generateRepliesQuery(query, user); | 	generateRepliesQuery(query, user); | ||||||
| 	generateVisibilityQuery(query, user); | 	generateVisibilityQuery(query, user); | ||||||
| 	generateMutedUserQuery(query, user); | 	generateMutedUserQuery(query, user); | ||||||
|  | 	generateMutedInstanceQuery(query, user); | ||||||
| 	generateMutedNoteQuery(query, user); | 	generateMutedNoteQuery(query, user); | ||||||
| 	generateBlockedUserQuery(query, user); | 	generateBlockedUserQuery(query, user); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import { Notes } from '@/models/index'; | ||||||
| import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; | ||||||
| import { Brackets } from 'typeorm'; | import { Brackets } from 'typeorm'; | ||||||
| import { generateBlockedUserQuery } from '../../common/generate-block-query'; | import { generateBlockedUserQuery } from '../../common/generate-block-query'; | ||||||
|  | import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	tags: ['users', 'notes'], | 	tags: ['users', 'notes'], | ||||||
|  | @ -102,6 +103,7 @@ export default define(meta, async (ps, me) => { | ||||||
| 	generateVisibilityQuery(query, me); | 	generateVisibilityQuery(query, me); | ||||||
| 	if (me) generateMutedUserQuery(query, me, user); | 	if (me) generateMutedUserQuery(query, me, user); | ||||||
| 	if (me) generateBlockedUserQuery(query, me); | 	if (me) generateBlockedUserQuery(query, me); | ||||||
|  | 	if (me) generateMutedInstanceQuery(query, me); | ||||||
| 
 | 
 | ||||||
| 	if (ps.withFiles) { | 	if (ps.withFiles) { | ||||||
| 		query.andWhere('note.fileIds != \'{}\''); | 		query.andWhere('note.fileIds != \'{}\''); | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import { fetchMeta } from '@/misc/fetch-meta'; | ||||||
| import { Notes } from '@/models/index'; | import { Notes } from '@/models/index'; | ||||||
| import { checkWordMute } from '@/misc/check-word-mute'; | import { checkWordMute } from '@/misc/check-word-mute'; | ||||||
| import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | ||||||
|  | import { isInstanceMuted } from '@/misc/is-instance-muted'; | ||||||
| import { Packed } from '@/misc/schema'; | import { Packed } from '@/misc/schema'; | ||||||
| 
 | 
 | ||||||
| export default class extends Channel { | 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; | 			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がミュートしているユーザーが関わるものだったら無視する
 | 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | ||||||
| 		if (isMutedUserRelated(note, this.muting)) return; | 		if (isMutedUserRelated(note, this.muting)) return; | ||||||
| 		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
 | 		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
 | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import Channel from '../channel'; | ||||||
| import { Notes } from '@/models/index'; | import { Notes } from '@/models/index'; | ||||||
| import { checkWordMute } from '@/misc/check-word-mute'; | import { checkWordMute } from '@/misc/check-word-mute'; | ||||||
| import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | ||||||
|  | import { isInstanceMuted } from '@/misc/is-instance-muted'; | ||||||
| import { Packed } from '@/misc/schema'; | import { Packed } from '@/misc/schema'; | ||||||
| 
 | 
 | ||||||
| export default class extends Channel { | 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; | 			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)) { | 		if (['followers', 'specified'].includes(note.visibility)) { | ||||||
| 			note = await Notes.pack(note.id, this.user!, { | 			note = await Notes.pack(note.id, this.user!, { | ||||||
| 				detail: true | 				detail: true | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import { fetchMeta } from '@/misc/fetch-meta'; | ||||||
| import { Notes } from '@/models/index'; | import { Notes } from '@/models/index'; | ||||||
| import { checkWordMute } from '@/misc/check-word-mute'; | import { checkWordMute } from '@/misc/check-word-mute'; | ||||||
| import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | import { isBlockerUserRelated } from '@/misc/is-blocker-user-related'; | ||||||
|  | import { isInstanceMuted } from '@/misc/is-instance-muted'; | ||||||
| import { Packed } from '@/misc/schema'; | import { Packed } from '@/misc/schema'; | ||||||
| 
 | 
 | ||||||
| export default class extends Channel { | 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) { | 		if (note.reply) { | ||||||
| 			const reply = note.reply; | 			const reply = note.reply; | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import autobind from 'autobind-decorator'; | import autobind from 'autobind-decorator'; | ||||||
| import Channel from '../channel'; | import Channel from '../channel'; | ||||||
| import { Notes } from '@/models/index'; | import { Notes } from '@/models/index'; | ||||||
|  | import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted'; | ||||||
| 
 | 
 | ||||||
| export default class extends Channel { | export default class extends Channel { | ||||||
| 	public readonly chName = 'main'; | 	public readonly chName = 'main'; | ||||||
|  | @ -13,6 +14,8 @@ export default class extends Channel { | ||||||
| 		this.subscriber.on(`mainStream:${this.user!.id}`, async data => { | 		this.subscriber.on(`mainStream:${this.user!.id}`, async data => { | ||||||
| 			switch (data.type) { | 			switch (data.type) { | ||||||
| 				case 'notification': { | 				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.userId && this.muting.has(data.body.userId)) return; | ||||||
| 
 | 
 | ||||||
| 					if (data.body.note && data.body.note.isHidden) { | 					if (data.body.note && data.body.note.isHidden) { | ||||||
|  | @ -25,6 +28,8 @@ export default class extends Channel { | ||||||
| 					break; | 					break; | ||||||
| 				} | 				} | ||||||
| 				case 'mention': { | 				case 'mention': { | ||||||
|  | 					if (isInstanceMuted(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return; | ||||||
|  | 
 | ||||||
| 					if (this.muting.has(data.body.userId)) return; | 					if (this.muting.has(data.body.userId)) return; | ||||||
| 					if (data.body.isHidden) { | 					if (data.body.isHidden) { | ||||||
| 						const note = await Notes.pack(data.body.id, this.user, { | 						const note = await Notes.pack(data.body.id, this.user, { | ||||||
|  |  | ||||||
|  | @ -136,6 +136,11 @@ export default defineComponent({ | ||||||
| 				text: i18n.locale.importAndExport, | 				text: i18n.locale.importAndExport, | ||||||
| 				to: '/settings/import-export', | 				to: '/settings/import-export', | ||||||
| 				active: page.value === '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', | 				icon: 'fas fa-ban', | ||||||
| 				text: i18n.locale.muteAndBlock, | 				text: i18n.locale.muteAndBlock, | ||||||
|  | @ -190,6 +195,7 @@ export default defineComponent({ | ||||||
| 				case 'notifications': return defineAsyncComponent(() => import('./notifications.vue')); | 				case 'notifications': return defineAsyncComponent(() => import('./notifications.vue')); | ||||||
| 				case 'mute-block': return defineAsyncComponent(() => import('./mute-block.vue')); | 				case 'mute-block': return defineAsyncComponent(() => import('./mute-block.vue')); | ||||||
| 				case 'word-mute': return defineAsyncComponent(() => import('./word-mute.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 'integration': return defineAsyncComponent(() => import('./integration.vue')); | ||||||
| 				case 'security': return defineAsyncComponent(() => import('./security.vue')); | 				case 'security': return defineAsyncComponent(() => import('./security.vue')); | ||||||
| 				case '2fa': return defineAsyncComponent(() => import('./2fa.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