Merge d46ee813bf into 6c27ab12eb
				
					
				
			This commit is contained in:
		
						commit
						3fc84f13ca
					
				|  | @ -5386,6 +5386,10 @@ export interface Locale extends ILocale { | |||
|      * 設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます) | ||||
|      */ | ||||
|     "settingsMigrating": string; | ||||
|     /** | ||||
|      * 読み取り専用 | ||||
|      */ | ||||
|     "readonly": string; | ||||
|     "_chat": { | ||||
|         /** | ||||
|          * まだメッセージはありません | ||||
|  | @ -5500,6 +5504,10 @@ export interface Locale extends ILocale { | |||
|          * このサーバー、またはこのアカウントでチャットは有効化されていません。 | ||||
|          */ | ||||
|         "chatNotAvailableForThisAccountOrServer": string; | ||||
|         /** | ||||
|          * このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。 | ||||
|          */ | ||||
|         "chatIsReadOnlyForThisAccountOrServer": string; | ||||
|         /** | ||||
|          * 相手のアカウントでチャット機能が使えない状態になっています。 | ||||
|          */ | ||||
|  | @ -7531,7 +7539,7 @@ export interface Locale extends ILocale { | |||
|             /** | ||||
|              * チャットを許可 | ||||
|              */ | ||||
|             "canChat": string; | ||||
|             "chatAvailability": string; | ||||
|         }; | ||||
|         "_condition": { | ||||
|             /** | ||||
|  |  | |||
|  | @ -1342,6 +1342,7 @@ bottom: "下" | |||
| top: "上" | ||||
| embed: "埋め込み" | ||||
| settingsMigrating: "設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)" | ||||
| readonly: "読み取り専用" | ||||
| 
 | ||||
| _chat: | ||||
|   noMessagesYet: "まだメッセージはありません" | ||||
|  | @ -1372,6 +1373,7 @@ _chat: | |||
|   muteThisRoom: "このルームをミュート" | ||||
|   deleteRoom: "ルームを削除" | ||||
|   chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。" | ||||
|   chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。" | ||||
|   chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。" | ||||
|   cannotChatWithTheUser: "このユーザーとのチャットを開始できません" | ||||
|   cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。" | ||||
|  | @ -1950,7 +1952,7 @@ _role: | |||
|     canImportFollowing: "フォローのインポートを許可" | ||||
|     canImportMuting: "ミュートのインポートを許可" | ||||
|     canImportUserLists: "リストのインポートを許可" | ||||
|     canChat: "チャットを許可" | ||||
|     chatAvailability: "チャットを許可" | ||||
|   _condition: | ||||
|     roleAssignedTo: "マニュアルロールにアサイン済み" | ||||
|     isLocal: "ローカルユーザー" | ||||
|  |  | |||
|  | @ -94,6 +94,40 @@ export class ChatService { | |||
| 	) { | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async getChatAvailability(userId: MiUser['id']): Promise<{ read: boolean; write: boolean; }> { | ||||
| 		const policies = await this.roleService.getUserPolicies(userId); | ||||
| 
 | ||||
| 		switch (policies.chatAvailability) { | ||||
| 			case 'available': | ||||
| 				return { | ||||
| 					read: true, | ||||
| 					write: true, | ||||
| 				}; | ||||
| 			case 'readonly': | ||||
| 				return { | ||||
| 					read: true, | ||||
| 					write: false, | ||||
| 				}; | ||||
| 			case 'unavailable': | ||||
| 				return { | ||||
| 					read: false, | ||||
| 					write: false, | ||||
| 				}; | ||||
| 			default: | ||||
| 				throw new Error('invalid chat availability (unreachable)'); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** getChatAvailabilityの糖衣。主にAPI呼び出し時に走らせて、権限的に問題ない場合はそのまま続行する */ | ||||
| 	@bindThis | ||||
| 	public async checkChatAvailability(userId: MiUser['id'], permission: 'read' | 'write') { | ||||
| 		const policy = await this.getChatAvailability(userId); | ||||
| 		if (policy[permission] === false) { | ||||
| 			throw new Error('ROLE_PERMISSION_DENIED'); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async createMessageToUser(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toUser: MiUser, params: { | ||||
| 		text?: string | null; | ||||
|  | @ -140,7 +174,7 @@ export class ChatService { | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (!(await this.roleService.getUserPolicies(toUser.id)).canChat) { | ||||
| 		if (!(await this.getChatAvailability(toUser.id)).write) { | ||||
| 			throw new Error('recipient is cannot chat (policy)'); | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -63,7 +63,7 @@ export type RolePolicies = { | |||
| 	canImportFollowing: boolean; | ||||
| 	canImportMuting: boolean; | ||||
| 	canImportUserLists: boolean; | ||||
| 	canChat: boolean; | ||||
| 	chatAvailability: 'available' | 'readonly' | 'unavailable'; | ||||
| }; | ||||
| 
 | ||||
| export const DEFAULT_POLICIES: RolePolicies = { | ||||
|  | @ -98,7 +98,7 @@ export const DEFAULT_POLICIES: RolePolicies = { | |||
| 	canImportFollowing: true, | ||||
| 	canImportMuting: true, | ||||
| 	canImportUserLists: true, | ||||
| 	canChat: true, | ||||
| 	chatAvailability: 'available', | ||||
| }; | ||||
| 
 | ||||
| @Injectable() | ||||
|  | @ -370,6 +370,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { | |||
| 			return aggregate(policies.map(policy => policy.useDefault ? basePolicies[name] : policy.value)); | ||||
| 		} | ||||
| 
 | ||||
| 		function aggregateChatAvailability(vs: RolePolicies['chatAvailability'][]) { | ||||
| 			if (vs.some(v => v === 'available')) return 'available'; | ||||
| 			if (vs.some(v => v === 'readonly')) return 'readonly'; | ||||
| 			return 'unavailable'; | ||||
| 		} | ||||
| 
 | ||||
| 		return { | ||||
| 			gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)), | ||||
| 			ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), | ||||
|  | @ -402,7 +408,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { | |||
| 			canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)), | ||||
| 			canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)), | ||||
| 			canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)), | ||||
| 			canChat: calc('canChat', vs => vs.some(v => v === true)), | ||||
| 			chatAvailability: calc('chatAvailability', aggregateChatAvailability), | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -557,7 +557,7 @@ export class UserEntityService implements OnModuleInit { | |||
| 				followersVisibility: profile!.followersVisibility, | ||||
| 				followingVisibility: profile!.followingVisibility, | ||||
| 				chatScope: user.chatScope, | ||||
| 				canChat: this.roleService.getUserPolicies(user.id).then(r => r.canChat), | ||||
| 				canChat: this.roleService.getUserPolicies(user.id).then(r => r.chatAvailability === 'available'), | ||||
| 				roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ | ||||
| 					id: role.id, | ||||
| 					name: role.name, | ||||
|  |  | |||
|  | @ -292,9 +292,10 @@ export const packedRolePoliciesSchema = { | |||
| 			type: 'boolean', | ||||
| 			optional: false, nullable: false, | ||||
| 		}, | ||||
| 		canChat: { | ||||
| 			type: 'boolean', | ||||
| 		chatAvailability: { | ||||
| 			type: 'string', | ||||
| 			optional: false, nullable: false, | ||||
| 			enum: ['available', 'readonly', 'unavailable'], | ||||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
|  |  | |||
|  | @ -46,6 +46,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'read'); | ||||
| 
 | ||||
| 			const history = ps.room ? await this.chatService.roomHistory(me.id, ps.limit) : await this.chatService.userHistory(me.id, ps.limit); | ||||
| 
 | ||||
| 			const packedMessages = await this.chatEntityService.packMessagesDetailed(history, me); | ||||
|  |  | |||
|  | @ -16,7 +16,6 @@ export const meta = { | |||
| 	tags: ['chat'], | ||||
| 
 | ||||
| 	requireCredential: true, | ||||
| 	requiredRolePolicy: 'canChat', | ||||
| 
 | ||||
| 	prohibitMoved: true, | ||||
| 
 | ||||
|  | @ -74,6 +73,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			const room = await this.chatService.findRoomById(ps.toRoomId); | ||||
| 			if (room == null) { | ||||
| 				throw new ApiError(meta.errors.noSuchRoom); | ||||
|  |  | |||
|  | @ -16,7 +16,6 @@ export const meta = { | |||
| 	tags: ['chat'], | ||||
| 
 | ||||
| 	requireCredential: true, | ||||
| 	requiredRolePolicy: 'canChat', | ||||
| 
 | ||||
| 	prohibitMoved: true, | ||||
| 
 | ||||
|  | @ -86,6 +85,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			let file = null; | ||||
| 			if (ps.fileId != null) { | ||||
| 				file = await this.driveFilesRepository.findOneBy({ | ||||
|  |  | |||
|  | @ -13,7 +13,6 @@ export const meta = { | |||
| 	tags: ['chat'], | ||||
| 
 | ||||
| 	requireCredential: true, | ||||
| 	requiredRolePolicy: 'canChat', | ||||
| 
 | ||||
| 	kind: 'write:chat', | ||||
| 
 | ||||
|  | @ -43,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			const message = await this.chatService.findMyMessageById(me.id, ps.messageId); | ||||
| 			if (message == null) { | ||||
| 				throw new ApiError(meta.errors.noSuchMessage); | ||||
|  |  | |||
|  | @ -13,7 +13,6 @@ export const meta = { | |||
| 	tags: ['chat'], | ||||
| 
 | ||||
| 	requireCredential: true, | ||||
| 	requiredRolePolicy: 'canChat', | ||||
| 
 | ||||
| 	kind: 'write:chat', | ||||
| 
 | ||||
|  | @ -44,6 +43,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			await this.chatService.react(ps.messageId, me.id, ps.reaction); | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -54,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'read'); | ||||
| 
 | ||||
| 			const room = await this.chatService.findRoomById(ps.roomId); | ||||
| 			if (room == null) { | ||||
| 				throw new ApiError(meta.errors.noSuchRoom); | ||||
|  |  | |||
|  | @ -54,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'read'); | ||||
| 
 | ||||
| 			if (ps.roomId != null) { | ||||
| 				const room = await this.chatService.findRoomById(ps.roomId); | ||||
| 				if (room == null) { | ||||
|  |  | |||
|  | @ -50,6 +50,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatEntityService: ChatEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'read'); | ||||
| 
 | ||||
| 			const message = await this.chatService.findMessageById(ps.messageId); | ||||
| 			if (message == null) { | ||||
| 				throw new ApiError(meta.errors.noSuchMessage); | ||||
|  |  | |||
|  | @ -13,7 +13,6 @@ export const meta = { | |||
| 	tags: ['chat'], | ||||
| 
 | ||||
| 	requireCredential: true, | ||||
| 	requiredRolePolicy: 'canChat', | ||||
| 
 | ||||
| 	kind: 'write:chat', | ||||
| 
 | ||||
|  | @ -44,6 +43,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			await this.chatService.unreact(ps.messageId, me.id, ps.reaction); | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -56,6 +56,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private getterService: GetterService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'read'); | ||||
| 
 | ||||
| 			const other = await this.getterService.getUser(ps.userId).catch(err => { | ||||
| 				if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); | ||||
| 				throw err; | ||||
|  |  | |||
|  | @ -15,7 +15,6 @@ export const meta = { | |||
| 	tags: ['chat'], | ||||
| 
 | ||||
| 	requireCredential: true, | ||||
| 	requiredRolePolicy: 'canChat', | ||||
| 
 | ||||
| 	prohibitMoved: true, | ||||
| 
 | ||||
|  | @ -52,6 +51,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatEntityService: ChatEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			const room = await this.chatService.createRoom(me, { | ||||
| 				name: ps.name, | ||||
| 				description: ps.description ?? '', | ||||
|  |  | |||
|  | @ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			const room = await this.chatService.findRoomById(ps.roomId); | ||||
| 			if (room == null) { | ||||
| 				throw new ApiError(meta.errors.noSuchRoom); | ||||
|  |  | |||
|  | @ -15,7 +15,6 @@ export const meta = { | |||
| 	tags: ['chat'], | ||||
| 
 | ||||
| 	requireCredential: true, | ||||
| 	requiredRolePolicy: 'canChat', | ||||
| 
 | ||||
| 	prohibitMoved: true, | ||||
| 
 | ||||
|  | @ -57,6 +56,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatEntityService: ChatEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			const room = await this.chatService.findMyRoomById(me.id, ps.roomId); | ||||
| 			if (room == null) { | ||||
| 				throw new ApiError(meta.errors.noSuchRoom); | ||||
|  |  | |||
|  | @ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			await this.chatService.ignoreRoomInvitation(me.id, ps.roomId); | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'read'); | ||||
| 
 | ||||
| 			const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId); | ||||
| 			return this.chatEntityService.packRoomInvitations(invitations, me); | ||||
| 		}); | ||||
|  |  | |||
|  | @ -55,6 +55,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatEntityService: ChatEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'read'); | ||||
| 
 | ||||
| 			const room = await this.chatService.findMyRoomById(me.id, ps.roomId); | ||||
| 			if (room == null) { | ||||
| 				throw new ApiError(meta.errors.noSuchRoom); | ||||
|  |  | |||
|  | @ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			await this.chatService.joinToRoom(me.id, ps.roomId); | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatEntityService: ChatEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'read'); | ||||
| 
 | ||||
| 			const memberships = await this.chatService.getMyMemberships(me.id, ps.limit, ps.sinceId, ps.untilId); | ||||
| 
 | ||||
| 			return this.chatEntityService.packRoomMemberships(memberships, me, { | ||||
|  |  | |||
|  | @ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			await this.chatService.leaveRoom(me.id, ps.roomId); | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -54,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatEntityService: ChatEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'read'); | ||||
| 
 | ||||
| 			const room = await this.chatService.findRoomById(ps.roomId); | ||||
| 			if (room == null) { | ||||
| 				throw new ApiError(meta.errors.noSuchRoom); | ||||
|  |  | |||
|  | @ -43,6 +43,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			await this.chatService.muteRoom(me.id, ps.roomId, ps.mute); | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatService: ChatService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'read'); | ||||
| 
 | ||||
| 			const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId); | ||||
| 			return this.chatEntityService.packRooms(rooms, me); | ||||
| 		}); | ||||
|  |  | |||
|  | @ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatEntityService: ChatEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'read'); | ||||
| 
 | ||||
| 			const room = await this.chatService.findRoomById(ps.roomId); | ||||
| 			if (room == null) { | ||||
| 				throw new ApiError(meta.errors.noSuchRoom); | ||||
|  |  | |||
|  | @ -49,6 +49,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private chatEntityService: ChatEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.chatService.checkChatAvailability(me.id, 'write'); | ||||
| 
 | ||||
| 			const room = await this.chatService.findMyRoomById(me.id, ps.roomId); | ||||
| 			if (room == null) { | ||||
| 				throw new ApiError(meta.errors.noSuchRoom); | ||||
|  |  | |||
|  | @ -108,7 +108,7 @@ export const ROLE_POLICIES = [ | |||
| 	'canImportFollowing', | ||||
| 	'canImportMuting', | ||||
| 	'canImportUserLists', | ||||
| 	'canChat', | ||||
| 	'chatAvailability', | ||||
| ] as const; | ||||
| 
 | ||||
| export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error.jpg'; | ||||
|  |  | |||
|  | @ -114,6 +114,7 @@ export const navbarItemDef = reactive({ | |||
| 		title: i18n.ts.chat, | ||||
| 		icon: 'ti ti-messages', | ||||
| 		to: '/chat', | ||||
| 		show: computed(() => $i != null && $i.policies.chatAvailability !== 'unavailable'), | ||||
| 		indicated: computed(() => $i != null && $i.hasUnreadChatMessages), | ||||
| 	}, | ||||
| 	achievements: { | ||||
|  |  | |||
|  | @ -165,21 +165,24 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 				</div> | ||||
| 			</MkFolder> | ||||
| 
 | ||||
| 			<MkFolder v-if="matchQuery([i18n.ts._role._options.canChat, 'canChat'])"> | ||||
| 				<template #label>{{ i18n.ts._role._options.canChat }}</template> | ||||
| 			<MkFolder v-if="matchQuery([i18n.ts._role._options.chatAvailability, 'chatAvailability'])"> | ||||
| 				<template #label>{{ i18n.ts._role._options.chatAvailability }}</template> | ||||
| 				<template #suffix> | ||||
| 					<span v-if="role.policies.canChat.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> | ||||
| 					<span v-else>{{ role.policies.canChat.value ? i18n.ts.yes : i18n.ts.no }}</span> | ||||
| 					<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canChat)"></i></span> | ||||
| 					<span v-if="role.policies.chatAvailability.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> | ||||
| 					<span v-else>{{ role.policies.chatAvailability.value === 'available' ? i18n.ts.yes : role.policies.chatAvailability.value === 'readonly' ? i18n.ts.readonly : i18n.ts.no }}</span> | ||||
| 					<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.chatAvailability)"></i></span> | ||||
| 				</template> | ||||
| 				<div class="_gaps"> | ||||
| 					<MkSwitch v-model="role.policies.canChat.useDefault" :readonly="readonly"> | ||||
| 					<MkSwitch v-model="role.policies.chatAvailability.useDefault" :readonly="readonly"> | ||||
| 						<template #label>{{ i18n.ts._role.useBaseValue }}</template> | ||||
| 					</MkSwitch> | ||||
| 					<MkSwitch v-model="role.policies.canChat.value" :disabled="role.policies.canChat.useDefault" :readonly="readonly"> | ||||
| 					<MkSelect v-model="role.policies.chatAvailability.value" :disabled="role.policies.chatAvailability.useDefault" :readonly="readonly"> | ||||
| 						<template #label>{{ i18n.ts.enable }}</template> | ||||
| 					</MkSwitch> | ||||
| 					<MkRange v-model="role.policies.canChat.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> | ||||
| 						<option value="available">{{ i18n.ts.enabled }}</option> | ||||
| 						<option value="readonly">{{ i18n.ts.readonly }}</option> | ||||
| 						<option value="unavailable">{{ i18n.ts.disabled }}</option> | ||||
| 					</MkSelect> | ||||
| 					<MkRange v-model="role.policies.chatAvailability.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> | ||||
| 						<template #label>{{ i18n.ts._role.priority }}</template> | ||||
| 					</MkRange> | ||||
| 				</div> | ||||
|  |  | |||
|  | @ -51,12 +51,15 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 							</MkSwitch> | ||||
| 						</MkFolder> | ||||
| 
 | ||||
| 						<MkFolder v-if="matchQuery([i18n.ts._role._options.canChat, 'canChat'])"> | ||||
| 							<template #label>{{ i18n.ts._role._options.canChat }}</template> | ||||
| 							<template #suffix>{{ policies.canChat ? i18n.ts.yes : i18n.ts.no }}</template> | ||||
| 							<MkSwitch v-model="policies.canChat"> | ||||
| 						<MkFolder v-if="matchQuery([i18n.ts._role._options.chatAvailability, 'chatAvailability'])"> | ||||
| 							<template #label>{{ i18n.ts._role._options.chatAvailability }}</template> | ||||
| 							<template #suffix>{{ policies.chatAvailability === 'available' ? i18n.ts.yes : policies.chatAvailability === 'readonly' ? i18n.ts.readonly : i18n.ts.no }}</template> | ||||
| 							<MkSelect v-model="policies.chatAvailability"> | ||||
| 								<template #label>{{ i18n.ts.enable }}</template> | ||||
| 							</MkSwitch> | ||||
| 								<option value="available">{{ i18n.ts.enabled }}</option> | ||||
| 								<option value="readonly">{{ i18n.ts.readonly }}</option> | ||||
| 								<option value="unavailable">{{ i18n.ts.disabled }}</option> | ||||
| 							</MkSelect> | ||||
| 						</MkFolder> | ||||
| 
 | ||||
| 						<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])"> | ||||
|  | @ -295,6 +298,7 @@ import MkInput from '@/components/MkInput.vue'; | |||
| import MkFolder from '@/components/MkFolder.vue'; | ||||
| import MkSwitch from '@/components/MkSwitch.vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkSelect from '@/components/MkSelect.vue'; | ||||
| import MkRange from '@/components/MkRange.vue'; | ||||
| import MkRolePreview from '@/components/MkRolePreview.vue'; | ||||
| import * as os from '@/os.js'; | ||||
|  |  | |||
|  | @ -85,7 +85,7 @@ const isMe = computed(() => props.message.fromUserId === $i.id); | |||
| const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []); | ||||
| 
 | ||||
| provide(DI.mfmEmojiReactCallback, (reaction) => { | ||||
| 	if (!$i.policies.canChat) return; | ||||
| 	if ($i.policies.chatAvailability !== 'available') return; | ||||
| 
 | ||||
| 	sound.playMisskeySfx('reaction'); | ||||
| 	misskeyApi('chat/messages/react', { | ||||
|  | @ -95,7 +95,7 @@ provide(DI.mfmEmojiReactCallback, (reaction) => { | |||
| }); | ||||
| 
 | ||||
| function react(ev: MouseEvent) { | ||||
| 	if (!$i.policies.canChat) return; | ||||
| 	if ($i.policies.chatAvailability !== 'available') return; | ||||
| 
 | ||||
| 	const targetEl = getHTMLElementOrNull(ev.currentTarget ?? ev.target); | ||||
| 	if (!targetEl) return; | ||||
|  | @ -110,7 +110,7 @@ function react(ev: MouseEvent) { | |||
| } | ||||
| 
 | ||||
| function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) { | ||||
| 	if (!$i.policies.canChat) return; | ||||
| 	if ($i.policies.chatAvailability !== 'available') return; | ||||
| 
 | ||||
| 	if (record.user.id === $i.id) { | ||||
| 		misskeyApi('chat/messages/unreact', { | ||||
|  | @ -138,7 +138,7 @@ function onContextmenu(ev: MouseEvent) { | |||
| function showMenu(ev: MouseEvent, contextmenu = false) { | ||||
| 	const menu: MenuItem[] = []; | ||||
| 
 | ||||
| 	if (!isMe.value && $i.policies.canChat) { | ||||
| 	if (!isMe.value && $i.policies.chatAvailability === 'available') { | ||||
| 		menu.push({ | ||||
| 			text: i18n.ts.reaction, | ||||
| 			icon: 'ti ti-mood-plus', | ||||
|  | @ -164,7 +164,7 @@ function showMenu(ev: MouseEvent, contextmenu = false) { | |||
| 		type: 'divider', | ||||
| 	}); | ||||
| 
 | ||||
| 	if (isMe.value && $i.policies.canChat) { | ||||
| 	if (isMe.value && $i.policies.chatAvailability === 'available') { | ||||
| 		menu.push({ | ||||
| 			text: i18n.ts.delete, | ||||
| 			icon: 'ti ti-trash', | ||||
|  |  | |||
|  | @ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 
 | ||||
| <template> | ||||
| <div class="_gaps"> | ||||
| 	<MkButton v-if="$i.policies.canChat" primary gradate rounded :class="$style.start" @click="start"><i class="ti ti-plus"></i> {{ i18n.ts.startChat }}</MkButton> | ||||
| 	<MkButton v-if="$i.policies.chatAvailability === 'available'" primary gradate rounded :class="$style.start" @click="start"><i class="ti ti-plus"></i> {{ i18n.ts.startChat }}</MkButton> | ||||
| 
 | ||||
| 	<MkInfo v-else>{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo> | ||||
| 	<MkInfo v-else>{{ $i.policies.chatAvailability === 'readonly' ? i18n.ts._chat.chatIsReadOnlyForThisAccountOrServer : i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo> | ||||
| 
 | ||||
| 	<MkAd :preferForms="['horizontal', 'horizontal-big']"/> | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,54 +6,56 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| <template> | ||||
| <PageWithHeader v-model:tab="tab" :reversed="tab === 'chat'" :tabs="headerTabs" :actions="headerActions"> | ||||
| 	<MkSpacer v-if="tab === 'chat'" :contentMax="700"> | ||||
| 		<div v-if="initializing"> | ||||
| 			<MkLoading/> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<div v-else-if="messages.length === 0"> | ||||
| 			<div class="_gaps" style="text-align: center;"> | ||||
| 				<div>{{ i18n.ts._chat.noMessagesYet }}</div> | ||||
| 				<template v-if="user"> | ||||
| 					<div v-if="user.chatScope === 'followers'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromFollowers }}</div> | ||||
| 					<div v-else-if="user.chatScope === 'following'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromFollowing }}</div> | ||||
| 					<div v-else-if="user.chatScope === 'mutual'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromMutualFollowing }}</div> | ||||
| 					<div v-else-if="user.chatScope === 'none'">{{ i18n.ts._chat.thisUserNotAllowedChatAnyone }}</div> | ||||
| 				</template> | ||||
| 				<template v-else-if="room"> | ||||
| 					<div>{{ i18n.ts._chat.inviteUserToChat }}</div> | ||||
| 				</template> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<div v-else ref="timelineEl" class="_gaps"> | ||||
| 			<div v-if="canFetchMore"> | ||||
| 				<MkButton :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">{{ i18n.ts.loadMore }}</MkButton> | ||||
| 		<div class="_gaps"> | ||||
| 			<div v-if="initializing"> | ||||
| 				<MkLoading/> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<TransitionGroup | ||||
| 				:enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''" | ||||
| 				:leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''" | ||||
| 				:enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''" | ||||
| 				:leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''" | ||||
| 				:moveClass="prefer.s.animation ? $style.transition_x_move : ''" | ||||
| 				tag="div" class="_gaps" | ||||
| 			> | ||||
| 				<template v-for="item in timeline.toReversed()" :key="item.id"> | ||||
| 					<XMessage v-if="item.type === 'item'" :message="item.data"/> | ||||
| 					<div v-else-if="item.type === 'date'" :class="$style.dateDivider"> | ||||
| 						<span><i class="ti ti-chevron-up"></i> {{ item.nextText }}</span> | ||||
| 						<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span> | ||||
| 						<span>{{ item.prevText }} <i class="ti ti-chevron-down"></i></span> | ||||
| 					</div> | ||||
| 				</template> | ||||
| 			</TransitionGroup> | ||||
| 		</div> | ||||
| 			<div v-else-if="messages.length === 0"> | ||||
| 				<div class="_gaps" style="text-align: center;"> | ||||
| 					<div>{{ i18n.ts._chat.noMessagesYet }}</div> | ||||
| 					<template v-if="user"> | ||||
| 						<div v-if="user.chatScope === 'followers'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromFollowers }}</div> | ||||
| 						<div v-else-if="user.chatScope === 'following'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromFollowing }}</div> | ||||
| 						<div v-else-if="user.chatScope === 'mutual'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromMutualFollowing }}</div> | ||||
| 						<div v-else-if="user.chatScope === 'none'">{{ i18n.ts._chat.thisUserNotAllowedChatAnyone }}</div> | ||||
| 					</template> | ||||
| 					<template v-else-if="room"> | ||||
| 						<div>{{ i18n.ts._chat.inviteUserToChat }}</div> | ||||
| 					</template> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 		<div v-if="user && (!user.canChat || user.host !== null)"> | ||||
| 			<MkInfo warn>{{ i18n.ts._chat.chatNotAvailableInOtherAccount }}</MkInfo> | ||||
| 		</div> | ||||
| 			<div v-else ref="timelineEl" class="_gaps"> | ||||
| 				<div v-if="canFetchMore"> | ||||
| 					<MkButton :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">{{ i18n.ts.loadMore }}</MkButton> | ||||
| 				</div> | ||||
| 
 | ||||
| 		<MkInfo v-if="!$i.policies.canChat" warn>{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo> | ||||
| 				<TransitionGroup | ||||
| 					:enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''" | ||||
| 					:leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''" | ||||
| 					:enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''" | ||||
| 					:leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''" | ||||
| 					:moveClass="prefer.s.animation ? $style.transition_x_move : ''" | ||||
| 					tag="div" class="_gaps" | ||||
| 				> | ||||
| 					<template v-for="item in timeline.toReversed()" :key="item.id"> | ||||
| 						<XMessage v-if="item.type === 'item'" :message="item.data"/> | ||||
| 						<div v-else-if="item.type === 'date'" :class="$style.dateDivider"> | ||||
| 							<span><i class="ti ti-chevron-up"></i> {{ item.nextText }}</span> | ||||
| 							<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span> | ||||
| 							<span>{{ item.prevText }} <i class="ti ti-chevron-down"></i></span> | ||||
| 						</div> | ||||
| 					</template> | ||||
| 				</TransitionGroup> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div v-if="user && (!user.canChat || user.host !== null)"> | ||||
| 				<MkInfo warn>{{ i18n.ts._chat.chatNotAvailableInOtherAccount }}</MkInfo> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<MkInfo v-if="$i.policies.chatAvailability !== 'available'" warn>{{ $i.policies.chatAvailability === 'readonly' ? i18n.ts._chat.chatIsReadOnlyForThisAccountOrServer : i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| 
 | ||||
| 	<MkSpacer v-else-if="tab === 'search'" :contentMax="700"> | ||||
|  |  | |||
|  | @ -379,44 +379,46 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 				</MkFolder> | ||||
| 			</SearchMarker> | ||||
| 
 | ||||
| 			<SearchMarker :keywords="['chat', 'messaging']"> | ||||
| 				<MkFolder> | ||||
| 					<template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template> | ||||
| 					<template #icon><SearchIcon><i class="ti ti-messages"></i></SearchIcon></template> | ||||
| 			<template v-if="$i.policies.chatAvailability !== 'unavailable'"> | ||||
| 				<SearchMarker :keywords="['chat', 'messaging']"> | ||||
| 					<MkFolder> | ||||
| 						<template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template> | ||||
| 						<template #icon><SearchIcon><i class="ti ti-messages"></i></SearchIcon></template> | ||||
| 
 | ||||
| 					<div class="_gaps_s"> | ||||
| 						<SearchMarker :keywords="['show', 'sender', 'name']"> | ||||
| 							<MkPreferenceContainer k="chat.showSenderName"> | ||||
| 								<MkSwitch v-model="chatShowSenderName"> | ||||
| 									<template #label><SearchLabel>{{ i18n.ts._settings._chat.showSenderName }}</SearchLabel></template> | ||||
| 								</MkSwitch> | ||||
| 							</MkPreferenceContainer> | ||||
| 						</SearchMarker> | ||||
| 						<div class="_gaps_s"> | ||||
| 							<SearchMarker :keywords="['show', 'sender', 'name']"> | ||||
| 								<MkPreferenceContainer k="chat.showSenderName"> | ||||
| 									<MkSwitch v-model="chatShowSenderName"> | ||||
| 										<template #label><SearchLabel>{{ i18n.ts._settings._chat.showSenderName }}</SearchLabel></template> | ||||
| 									</MkSwitch> | ||||
| 								</MkPreferenceContainer> | ||||
| 							</SearchMarker> | ||||
| 
 | ||||
| 						<SearchMarker :keywords="['send', 'enter', 'newline']"> | ||||
| 							<MkPreferenceContainer k="chat.sendOnEnter"> | ||||
| 								<MkSwitch v-model="chatSendOnEnter"> | ||||
| 									<template #label><SearchLabel>{{ i18n.ts._settings._chat.sendOnEnter }}</SearchLabel></template> | ||||
| 									<template #caption> | ||||
| 										<div class="_gaps_s"> | ||||
| 											<div> | ||||
| 												<b>{{ i18n.ts._settings.ifOn }}:</b> | ||||
| 												<div>{{ i18n.ts._chat.send }}: Enter</div> | ||||
| 												<div>{{ i18n.ts._chat.newline }}: Shift + Enter</div> | ||||
| 							<SearchMarker :keywords="['send', 'enter', 'newline']"> | ||||
| 								<MkPreferenceContainer k="chat.sendOnEnter"> | ||||
| 									<MkSwitch v-model="chatSendOnEnter"> | ||||
| 										<template #label><SearchLabel>{{ i18n.ts._settings._chat.sendOnEnter }}</SearchLabel></template> | ||||
| 										<template #caption> | ||||
| 											<div class="_gaps_s"> | ||||
| 												<div> | ||||
| 													<b>{{ i18n.ts._settings.ifOn }}:</b> | ||||
| 													<div>{{ i18n.ts._chat.send }}: Enter</div> | ||||
| 													<div>{{ i18n.ts._chat.newline }}: Shift + Enter</div> | ||||
| 												</div> | ||||
| 												<div> | ||||
| 													<b>{{ i18n.ts._settings.ifOff }}:</b> | ||||
| 													<div>{{ i18n.ts._chat.send }}: Ctrl + Enter</div> | ||||
| 													<div>{{ i18n.ts._chat.newline }}: Enter</div> | ||||
| 												</div> | ||||
| 											</div> | ||||
| 											<div> | ||||
| 												<b>{{ i18n.ts._settings.ifOff }}:</b> | ||||
| 												<div>{{ i18n.ts._chat.send }}: Ctrl + Enter</div> | ||||
| 												<div>{{ i18n.ts._chat.newline }}: Enter</div> | ||||
| 											</div> | ||||
| 										</div> | ||||
| 									</template> | ||||
| 								</MkSwitch> | ||||
| 							</MkPreferenceContainer> | ||||
| 						</SearchMarker> | ||||
| 					</div> | ||||
| 				</MkFolder> | ||||
| 			</SearchMarker> | ||||
| 										</template> | ||||
| 									</MkSwitch> | ||||
| 								</MkPreferenceContainer> | ||||
| 							</SearchMarker> | ||||
| 						</div> | ||||
| 					</MkFolder> | ||||
| 				</SearchMarker> | ||||
| 			</template> | ||||
| 
 | ||||
| 			<SearchMarker :keywords="['accessibility']"> | ||||
| 				<MkFolder> | ||||
|  | @ -732,6 +734,9 @@ import MkFeatureBanner from '@/components/MkFeatureBanner.vue'; | |||
| import { globalEvents } from '@/events.js'; | ||||
| import { claimAchievement } from '@/utility/achievements.js'; | ||||
| import { instance } from '@/instance.js'; | ||||
| import { ensureSignin } from '@/i.js'; | ||||
| 
 | ||||
| const $i = ensureSignin(); | ||||
| 
 | ||||
| const lang = ref(miLocalStorage.getItem('lang')); | ||||
| const dataSaver = ref(prefer.s.dataSaver); | ||||
|  |  | |||
|  | @ -78,19 +78,26 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 			</MkSwitch> | ||||
| 		</SearchMarker> | ||||
| 
 | ||||
| 		<FormSection> | ||||
| 			<SearchMarker :keywords="['chat']"> | ||||
| 				<MkSelect v-model="chatScope" @update:modelValue="save()"> | ||||
| 					<template #label><SearchLabel>{{ i18n.ts._chat.chatAllowedUsers }}</SearchLabel></template> | ||||
| 					<option value="everyone">{{ i18n.ts._chat._chatAllowedUsers.everyone }}</option> | ||||
| 					<option value="followers">{{ i18n.ts._chat._chatAllowedUsers.followers }}</option> | ||||
| 					<option value="following">{{ i18n.ts._chat._chatAllowedUsers.following }}</option> | ||||
| 					<option value="mutual">{{ i18n.ts._chat._chatAllowedUsers.mutual }}</option> | ||||
| 					<option value="none">{{ i18n.ts._chat._chatAllowedUsers.none }}</option> | ||||
| 					<template #caption>{{ i18n.ts._chat.chatAllowedUsers_note }}</template> | ||||
| 				</MkSelect> | ||||
| 			</SearchMarker> | ||||
| 		</FormSection> | ||||
| 		<SearchMarker :keywords="['chat']"> | ||||
| 			<FormSection> | ||||
| 				<template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template> | ||||
| 
 | ||||
| 				<div class="_gaps_m"> | ||||
| 					<MkInfo v-if="$i.policies.chatAvailability === 'unavailable'">{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo> | ||||
| 					<SearchMarker :keywords="['chat']"> | ||||
| 						<MkSelect v-model="chatScope" @update:modelValue="save()"> | ||||
| 							<template #label><SearchLabel>{{ i18n.ts._chat.chatAllowedUsers }}</SearchLabel></template> | ||||
| 							<option value="everyone">{{ i18n.ts._chat._chatAllowedUsers.everyone }}</option> | ||||
| 							<option value="followers">{{ i18n.ts._chat._chatAllowedUsers.followers }}</option> | ||||
| 							<option value="following">{{ i18n.ts._chat._chatAllowedUsers.following }}</option> | ||||
| 							<option value="mutual">{{ i18n.ts._chat._chatAllowedUsers.mutual }}</option> | ||||
| 							<option value="none">{{ i18n.ts._chat._chatAllowedUsers.none }}</option> | ||||
| 							<template #caption>{{ i18n.ts._chat.chatAllowedUsers_note }}</template> | ||||
| 						</MkSelect> | ||||
| 					</SearchMarker> | ||||
| 				</div> | ||||
| 			</FormSection> | ||||
| 		</SearchMarker> | ||||
| 
 | ||||
| 		<SearchMarker :keywords="['lockdown']"> | ||||
| 			<FormSection> | ||||
|  |  | |||
|  | @ -16,6 +16,10 @@ export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({ | |||
| 	errorComponent: MkError, | ||||
| }); | ||||
| 
 | ||||
| function chatPage(...args: Parameters<typeof page>) { | ||||
| 	return $i?.policies.chatAvailability !== 'unavailable' ? page(...args) : page(() => import('@/pages/not-found.vue')); | ||||
| } | ||||
| 
 | ||||
| export const ROUTE_DEF = [{ | ||||
| 	path: '/@:username/pages/:pageName(*)', | ||||
| 	component: page(() => import('@/pages/page.vue')), | ||||
|  | @ -42,19 +46,19 @@ export const ROUTE_DEF = [{ | |||
| 	component: page(() => import('@/pages/clip.vue')), | ||||
| }, { | ||||
| 	path: '/chat', | ||||
| 	component: page(() => import('@/pages/chat/home.vue')), | ||||
| 	component: chatPage(() => import('@/pages/chat/home.vue')), | ||||
| 	loginRequired: true, | ||||
| }, { | ||||
| 	path: '/chat/user/:userId', | ||||
| 	component: page(() => import('@/pages/chat/room.vue')), | ||||
| 	component: chatPage(() => import('@/pages/chat/room.vue')), | ||||
| 	loginRequired: true, | ||||
| }, { | ||||
| 	path: '/chat/room/:roomId', | ||||
| 	component: page(() => import('@/pages/chat/room.vue')), | ||||
| 	component: chatPage(() => import('@/pages/chat/room.vue')), | ||||
| 	loginRequired: true, | ||||
| }, { | ||||
| 	path: '/chat/messages/:messageId', | ||||
| 	component: page(() => import('@/pages/chat/message.vue')), | ||||
| 	component: chatPage(() => import('@/pages/chat/message.vue')), | ||||
| 	loginRequired: true, | ||||
| }, { | ||||
| 	path: '/instance-info/:host', | ||||
|  |  | |||
|  | @ -364,7 +364,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router | |||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
| 		if ($i.policies.canChat && user.canChat && user.host == null) { | ||||
| 		if ($i.policies.chatAvailability === 'available' && user.canChat && user.host == null) { | ||||
| 			menuItems.push({ | ||||
| 				type: 'link', | ||||
| 				icon: 'ti ti-messages', | ||||
|  |  | |||
|  | @ -5179,7 +5179,8 @@ export type components = { | |||
|       canImportFollowing: boolean; | ||||
|       canImportMuting: boolean; | ||||
|       canImportUserLists: boolean; | ||||
|       canChat: boolean; | ||||
|       /** @enum {string} */ | ||||
|       chatAvailability: 'available' | 'readonly' | 'unavailable'; | ||||
|     }; | ||||
|     ReversiGameLite: { | ||||
|       /** Format: id */ | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue