Compare commits
	
		
			20 Commits
		
	
	
		
			2024.8.0-r
			...
			multiple-r
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0ddd4bc545 | ||
| 
						 | 
					3cd5f86510 | ||
| 
						 | 
					9b78ce8047 | ||
| 
						 | 
					1629c0e50d | ||
| 
						 | 
					427f4a2cda | ||
| 
						 | 
					ba9c5c37b8 | ||
| 
						 | 
					e790aa0548 | ||
| 
						 | 
					bf8c42eecd | ||
| 
						 | 
					129af06198 | ||
| 
						 | 
					83c04c55ad | ||
| 
						 | 
					0b98554319 | ||
| 
						 | 
					4e0d57000c | ||
| 
						 | 
					c0de57c08d | ||
| 
						 | 
					cb6a1c773e | ||
| 
						 | 
					9df887ba93 | ||
| 
						 | 
					75b0315ace | ||
| 
						 | 
					a2769d0733 | ||
| 
						 | 
					036f90133c | ||
| 
						 | 
					f9bfff604d | ||
| 
						 | 
					fd0e840138 | 
@@ -17,7 +17,7 @@
 | 
				
			|||||||
- Fix: 特定の条件下でノートの削除ボタンが出ないのを修正
 | 
					- Fix: 特定の条件下でノートの削除ボタンが出ないのを修正
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Server
 | 
					### Server
 | 
				
			||||||
- enhance: 照会時にURLがhtmlかつheadタグ内に`rel="alternate"`, `type="application/activity+json"`の`link`タグがある場合に追ってリンク先を照会できるように
 | 
					- Enhance: 照会時にURLがhtmlかつheadタグ内に`rel="alternate"`, `type="application/activity+json"`の`link`タグがある場合に追ってリンク先を照会できるように
 | 
				
			||||||
- Enhance: 凍結されたアカウントのフォローリクエストを表示しないように
 | 
					- Enhance: 凍結されたアカウントのフォローリクエストを表示しないように
 | 
				
			||||||
- Fix: WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 #14374
 | 
					- Fix: WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 #14374
 | 
				
			||||||
  - 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。
 | 
					  - 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2499,6 +2499,7 @@ _moderationLogTypes:
 | 
				
			|||||||
  createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
 | 
					  createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
 | 
				
			||||||
  updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
 | 
					  updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
 | 
				
			||||||
  deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
 | 
					  deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
 | 
				
			||||||
 | 
					  deleteFlash: "Delete Play"
 | 
				
			||||||
_fileViewer:
 | 
					_fileViewer:
 | 
				
			||||||
  title: "File details"
 | 
					  title: "File details"
 | 
				
			||||||
  type: "File type"
 | 
					  type: "File type"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -6686,6 +6686,10 @@ export interface Locale extends ILocale {
 | 
				
			|||||||
             * ノートのピン留めの最大数
 | 
					             * ノートのピン留めの最大数
 | 
				
			||||||
             */
 | 
					             */
 | 
				
			||||||
            "pinMax": string;
 | 
					            "pinMax": string;
 | 
				
			||||||
 | 
					            /**
 | 
				
			||||||
 | 
					             * 一つのノートに対する最大リアクション数
 | 
				
			||||||
 | 
					             */
 | 
				
			||||||
 | 
					            "reactionsPerNoteLimit": string;
 | 
				
			||||||
            /**
 | 
					            /**
 | 
				
			||||||
             * アンテナの作成可能数
 | 
					             * アンテナの作成可能数
 | 
				
			||||||
             */
 | 
					             */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1728,6 +1728,7 @@ _role:
 | 
				
			|||||||
    alwaysMarkNsfw: "ファイルにNSFWを常に付与"
 | 
					    alwaysMarkNsfw: "ファイルにNSFWを常に付与"
 | 
				
			||||||
    canUpdateBioMedia: "アイコンとバナーの更新を許可"
 | 
					    canUpdateBioMedia: "アイコンとバナーの更新を許可"
 | 
				
			||||||
    pinMax: "ノートのピン留めの最大数"
 | 
					    pinMax: "ノートのピン留めの最大数"
 | 
				
			||||||
 | 
					    reactionsPerNoteLimit: "一つのノートに対する最大リアクション数"
 | 
				
			||||||
    antennaMax: "アンテナの作成可能数"
 | 
					    antennaMax: "アンテナの作成可能数"
 | 
				
			||||||
    wordMuteMax: "ワードミュートの最大文字数"
 | 
					    wordMuteMax: "ワードミュートの最大文字数"
 | 
				
			||||||
    webhookMax: "Webhookの作成可能数"
 | 
					    webhookMax: "Webhookの作成可能数"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2316,6 +2316,7 @@ _pages:
 | 
				
			|||||||
  eyeCatchingImageSet: "设置封面图片"
 | 
					  eyeCatchingImageSet: "设置封面图片"
 | 
				
			||||||
  eyeCatchingImageRemove: "删除封面图片"
 | 
					  eyeCatchingImageRemove: "删除封面图片"
 | 
				
			||||||
  chooseBlock: "添加块"
 | 
					  chooseBlock: "添加块"
 | 
				
			||||||
 | 
					  enterSectionTitle: "输入会话标题"
 | 
				
			||||||
  selectType: "选择类型"
 | 
					  selectType: "选择类型"
 | 
				
			||||||
  contentBlocks: "内容"
 | 
					  contentBlocks: "内容"
 | 
				
			||||||
  inputBlocks: "输入"
 | 
					  inputBlocks: "输入"
 | 
				
			||||||
@@ -2499,6 +2500,10 @@ _moderationLogTypes:
 | 
				
			|||||||
  createAbuseReportNotificationRecipient: "新建了举报通知"
 | 
					  createAbuseReportNotificationRecipient: "新建了举报通知"
 | 
				
			||||||
  updateAbuseReportNotificationRecipient: "更新了举报通知"
 | 
					  updateAbuseReportNotificationRecipient: "更新了举报通知"
 | 
				
			||||||
  deleteAbuseReportNotificationRecipient: "删除了举报通知"
 | 
					  deleteAbuseReportNotificationRecipient: "删除了举报通知"
 | 
				
			||||||
 | 
					  deleteAccount: "删除了账户"
 | 
				
			||||||
 | 
					  deletePage: "删除了页面"
 | 
				
			||||||
 | 
					  deleteFlash: "删除了 Play"
 | 
				
			||||||
 | 
					  deleteGalleryPost: "删除了图库稿件"
 | 
				
			||||||
_fileViewer:
 | 
					_fileViewer:
 | 
				
			||||||
  title: "文件信息"
 | 
					  title: "文件信息"
 | 
				
			||||||
  type: "文件类型"
 | 
					  type: "文件类型"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2316,6 +2316,7 @@ _pages:
 | 
				
			|||||||
  eyeCatchingImageSet: "設定封面影像"
 | 
					  eyeCatchingImageSet: "設定封面影像"
 | 
				
			||||||
  eyeCatchingImageRemove: "刪除封面影像"
 | 
					  eyeCatchingImageRemove: "刪除封面影像"
 | 
				
			||||||
  chooseBlock: "新增方塊"
 | 
					  chooseBlock: "新增方塊"
 | 
				
			||||||
 | 
					  enterSectionTitle: "輸入區段的標題"
 | 
				
			||||||
  selectType: "選擇類型"
 | 
					  selectType: "選擇類型"
 | 
				
			||||||
  contentBlocks: "內容"
 | 
					  contentBlocks: "內容"
 | 
				
			||||||
  inputBlocks: "輸入"
 | 
					  inputBlocks: "輸入"
 | 
				
			||||||
@@ -2499,6 +2500,10 @@ _moderationLogTypes:
 | 
				
			|||||||
  createAbuseReportNotificationRecipient: "建立接收檢舉的通知對象"
 | 
					  createAbuseReportNotificationRecipient: "建立接收檢舉的通知對象"
 | 
				
			||||||
  updateAbuseReportNotificationRecipient: "更新接收檢舉的通知對象"
 | 
					  updateAbuseReportNotificationRecipient: "更新接收檢舉的通知對象"
 | 
				
			||||||
  deleteAbuseReportNotificationRecipient: "刪除接收檢舉的通知對象"
 | 
					  deleteAbuseReportNotificationRecipient: "刪除接收檢舉的通知對象"
 | 
				
			||||||
 | 
					  deleteAccount: "刪除帳戶"
 | 
				
			||||||
 | 
					  deletePage: "刪除頁面"
 | 
				
			||||||
 | 
					  deleteFlash: "刪除 Play"
 | 
				
			||||||
 | 
					  deleteGalleryPost: "刪除相簿的貼文"
 | 
				
			||||||
_fileViewer:
 | 
					_fileViewer:
 | 
				
			||||||
  title: "檔案詳細資訊"
 | 
					  title: "檔案詳細資訊"
 | 
				
			||||||
  type: "檔案類型 "
 | 
					  type: "檔案類型 "
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class MultipleReactions1721117896543 {
 | 
				
			||||||
 | 
						name = 'MultipleReactions1721117896543';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async up(queryRunner) {
 | 
				
			||||||
 | 
							await queryRunner.query('DROP INDEX "public"."IDX_ad0c221b25672daf2df320a817"');
 | 
				
			||||||
 | 
							await queryRunner.query('CREATE UNIQUE INDEX "IDX_a7751b74317122d11575bff31c" ON "note_reaction" ("userId", "noteId", "reaction") ');
 | 
				
			||||||
 | 
							await queryRunner.query('CREATE INDEX "IDX_ad0c221b25672daf2df320a817" ON "note_reaction" ("userId", "noteId") ');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async down(queryRunner) {
 | 
				
			||||||
 | 
							await queryRunner.query('DROP INDEX "public"."IDX_ad0c221b25672daf2df320a817"');
 | 
				
			||||||
 | 
							await queryRunner.query('DROP INDEX "public"."IDX_a7751b74317122d11575bff31c"');
 | 
				
			||||||
 | 
							await queryRunner.query('CREATE UNIQUE INDEX "IDX_ad0c221b25672daf2df320a817" ON "note_reaction" ("userId", "noteId") ');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -174,24 +174,12 @@ export class ReactionService {
 | 
				
			|||||||
			reaction,
 | 
								reaction,
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create reaction
 | 
					 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			await this.noteReactionsRepository.insert(record);
 | 
								await this.noteReactionsRepository.insert(record);
 | 
				
			||||||
		} catch (e) {
 | 
							} catch (e) {
 | 
				
			||||||
			if (isDuplicateKeyValueError(e)) {
 | 
								if (isDuplicateKeyValueError(e)) {
 | 
				
			||||||
				const exists = await this.noteReactionsRepository.findOneByOrFail({
 | 
									// 同じリアクションがすでにされていたらエラー
 | 
				
			||||||
					noteId: note.id,
 | 
									throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
 | 
				
			||||||
					userId: user.id,
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				if (exists.reaction !== reaction) {
 | 
					 | 
				
			||||||
					// 別のリアクションがすでにされていたら置き換える
 | 
					 | 
				
			||||||
					await this.delete(user, note);
 | 
					 | 
				
			||||||
					await this.noteReactionsRepository.insert(record);
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					// 同じリアクションがすでにされていたらエラー
 | 
					 | 
				
			||||||
					throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				throw e;
 | 
									throw e;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -286,11 +274,12 @@ export class ReactionService {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@bindThis
 | 
						@bindThis
 | 
				
			||||||
	public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote) {
 | 
						public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, _reaction?: string | null) {
 | 
				
			||||||
		// if already unreacted
 | 
							// if already unreacted
 | 
				
			||||||
		const exist = await this.noteReactionsRepository.findOneBy({
 | 
							const exist = await this.noteReactionsRepository.findOneBy({
 | 
				
			||||||
			noteId: note.id,
 | 
								noteId: note.id,
 | 
				
			||||||
			userId: user.id,
 | 
								userId: user.id,
 | 
				
			||||||
 | 
								reaction: _reaction ?? FALLBACK,
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (exist == null) {
 | 
							if (exist == null) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,6 +49,7 @@ export type RolePolicies = {
 | 
				
			|||||||
	alwaysMarkNsfw: boolean;
 | 
						alwaysMarkNsfw: boolean;
 | 
				
			||||||
	canUpdateBioMedia: boolean;
 | 
						canUpdateBioMedia: boolean;
 | 
				
			||||||
	pinLimit: number;
 | 
						pinLimit: number;
 | 
				
			||||||
 | 
						reactionsPerNoteLimit: number;
 | 
				
			||||||
	antennaLimit: number;
 | 
						antennaLimit: number;
 | 
				
			||||||
	wordMuteLimit: number;
 | 
						wordMuteLimit: number;
 | 
				
			||||||
	webhookLimit: number;
 | 
						webhookLimit: number;
 | 
				
			||||||
@@ -78,6 +79,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
 | 
				
			|||||||
	alwaysMarkNsfw: false,
 | 
						alwaysMarkNsfw: false,
 | 
				
			||||||
	canUpdateBioMedia: true,
 | 
						canUpdateBioMedia: true,
 | 
				
			||||||
	pinLimit: 5,
 | 
						pinLimit: 5,
 | 
				
			||||||
 | 
						reactionsPerNoteLimit: 1,
 | 
				
			||||||
	antennaLimit: 5,
 | 
						antennaLimit: 5,
 | 
				
			||||||
	wordMuteLimit: 200,
 | 
						wordMuteLimit: 200,
 | 
				
			||||||
	webhookLimit: 3,
 | 
						webhookLimit: 3,
 | 
				
			||||||
@@ -380,6 +382,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
				
			|||||||
			alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
 | 
								alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
 | 
				
			||||||
			canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
 | 
								canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
 | 
				
			||||||
			pinLimit: calc('pinLimit', vs => Math.max(...vs)),
 | 
								pinLimit: calc('pinLimit', vs => Math.max(...vs)),
 | 
				
			||||||
 | 
								reactionsPerNoteLimit: calc('reactionsPerNoteLimit', vs => Math.max(...vs)),
 | 
				
			||||||
			antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
 | 
								antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
 | 
				
			||||||
			wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
 | 
								wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
 | 
				
			||||||
			webhookLimit: calc('webhookLimit', vs => Math.max(...vs)),
 | 
								webhookLimit: calc('webhookLimit', vs => Math.max(...vs)),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -203,7 +203,9 @@ export class ApRequestService {
 | 
				
			|||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		//#region リクエスト先がhtmlかつactivity+jsonへのalternate linkタグがあるとき
 | 
							//#region リクエスト先がhtmlかつactivity+jsonへのalternate linkタグがあるとき
 | 
				
			||||||
		if (res.headers.get('Content-type')?.startsWith('text/html;') && _followAlternate === true) {
 | 
							const contentType = res.headers.get('content-type');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) {
 | 
				
			||||||
			const html = await res.text();
 | 
								const html = await res.text();
 | 
				
			||||||
			const window = new Window();
 | 
								const window = new Window();
 | 
				
			||||||
			const document = window.document;
 | 
								const document = window.document;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -170,10 +170,10 @@ export class NoteEntityService implements OnModuleInit {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	@bindThis
 | 
						@bindThis
 | 
				
			||||||
	public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: {
 | 
						public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: {
 | 
				
			||||||
		myReactions: Map<MiNote['id'], string | null>;
 | 
							myReactionsMap: Map<MiNote['id'], string | null>;
 | 
				
			||||||
	}) {
 | 
						}) {
 | 
				
			||||||
		if (_hint_?.myReactions) {
 | 
							if (_hint_?.myReactionsMap) {
 | 
				
			||||||
			const reaction = _hint_.myReactions.get(note.id);
 | 
								const reaction = _hint_.myReactionsMap.get(note.id);
 | 
				
			||||||
			if (reaction) {
 | 
								if (reaction) {
 | 
				
			||||||
				return this.reactionService.convertLegacyReaction(reaction);
 | 
									return this.reactionService.convertLegacyReaction(reaction);
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,8 @@ import { MiUser } from './User.js';
 | 
				
			|||||||
import { MiNote } from './Note.js';
 | 
					import { MiNote } from './Note.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity('note_reaction')
 | 
					@Entity('note_reaction')
 | 
				
			||||||
@Index(['userId', 'noteId'], { unique: true })
 | 
					@Index(['userId', 'noteId'])
 | 
				
			||||||
 | 
					@Index(['userId', 'noteId', 'reaction'], { unique: true })
 | 
				
			||||||
export class MiNoteReaction {
 | 
					export class MiNoteReaction {
 | 
				
			||||||
	@PrimaryColumn(id())
 | 
						@PrimaryColumn(id())
 | 
				
			||||||
	public id: string;
 | 
						public id: string;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -236,6 +236,10 @@ export const packedRolePoliciesSchema = {
 | 
				
			|||||||
			type: 'integer',
 | 
								type: 'integer',
 | 
				
			||||||
			optional: false, nullable: false,
 | 
								optional: false, nullable: false,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							reactionsPerNoteLimit: {
 | 
				
			||||||
 | 
								type: 'integer',
 | 
				
			||||||
 | 
								optional: false, nullable: false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		antennaLimit: {
 | 
							antennaLimit: {
 | 
				
			||||||
			type: 'integer',
 | 
								type: 'integer',
 | 
				
			||||||
			optional: false, nullable: false,
 | 
								optional: false, nullable: false,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<div :class="$style.preview">
 | 
					<div :class="$style.preview">
 | 
				
			||||||
	<div :class="$style.preview__content1">
 | 
						<div>
 | 
				
			||||||
		<MkInput v-model="text">
 | 
							<MkInput v-model="text">
 | 
				
			||||||
			<template #label>Text</template>
 | 
								<template #label>Text</template>
 | 
				
			||||||
		</MkInput>
 | 
							</MkInput>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
			<span :class="$style.bodyName">{{ role.name }}</span>
 | 
								<span :class="$style.bodyName">{{ role.name }}</span>
 | 
				
			||||||
			<template v-if="detailed">
 | 
								<template v-if="detailed">
 | 
				
			||||||
				<span v-if="role.target === 'manual'" :class="$style.bodyUsers">{{ role.usersCount }} users</span>
 | 
									<span v-if="role.target === 'manual'" :class="$style.bodyUsers">{{ role.usersCount }} users</span>
 | 
				
			||||||
				<span v-else-if="role.target === 'conditional'" :class="$style.bodyUsers">({{ i18n.ts._role.conditional }})</span>
 | 
									<span v-else-if="role.target === 'conditional'" :class="$style.bodyUsers">? users</span>
 | 
				
			||||||
			</template>
 | 
								</template>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div :class="$style.bodyDescription">{{ role.description }}</div>
 | 
							<div :class="$style.bodyDescription">{{ role.description }}</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -89,6 +89,7 @@ export const ROLE_POLICIES = [
 | 
				
			|||||||
	'alwaysMarkNsfw',
 | 
						'alwaysMarkNsfw',
 | 
				
			||||||
	'canUpdateBioMedia',
 | 
						'canUpdateBioMedia',
 | 
				
			||||||
	'pinLimit',
 | 
						'pinLimit',
 | 
				
			||||||
 | 
						'reactionsPerNoteLimit',
 | 
				
			||||||
	'antennaLimit',
 | 
						'antennaLimit',
 | 
				
			||||||
	'wordMuteLimit',
 | 
						'wordMuteLimit',
 | 
				
			||||||
	'webhookLimit',
 | 
						'webhookLimit',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,10 +72,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
							<img src="https://avatars.githubusercontent.com/u/4439005?v=4" :class="$style.contributorAvatar">
 | 
												<img src="https://avatars.githubusercontent.com/u/4439005?v=4" :class="$style.contributorAvatar">
 | 
				
			||||||
							<span :class="$style.contributorUsername">@syuilo</span>
 | 
												<span :class="$style.contributorUsername">@syuilo</span>
 | 
				
			||||||
						</a>
 | 
											</a>
 | 
				
			||||||
						<a href="https://github.com/tamaina" target="_blank" :class="$style.contributor">
 | 
					 | 
				
			||||||
							<img src="https://avatars.githubusercontent.com/u/7973572?v=4" :class="$style.contributorAvatar">
 | 
					 | 
				
			||||||
							<span :class="$style.contributorUsername">@tamaina</span>
 | 
					 | 
				
			||||||
						</a>
 | 
					 | 
				
			||||||
						<a href="https://github.com/acid-chicken" target="_blank" :class="$style.contributor">
 | 
											<a href="https://github.com/acid-chicken" target="_blank" :class="$style.contributor">
 | 
				
			||||||
							<img src="https://avatars.githubusercontent.com/u/20679825?v=4" :class="$style.contributorAvatar">
 | 
												<img src="https://avatars.githubusercontent.com/u/20679825?v=4" :class="$style.contributorAvatar">
 | 
				
			||||||
							<span :class="$style.contributorUsername">@acid-chicken</span>
 | 
												<span :class="$style.contributorUsername">@acid-chicken</span>
 | 
				
			||||||
@@ -267,6 +263,9 @@ const patronsWithIcon = [{
 | 
				
			|||||||
}, {
 | 
					}, {
 | 
				
			||||||
	name: 'Macop',
 | 
						name: 'Macop',
 | 
				
			||||||
	icon: 'https://assets.misskey-hub.net/patrons/ee052bf550014d36a643ce3dce595640.jpg',
 | 
						icon: 'https://assets.misskey-hub.net/patrons/ee052bf550014d36a643ce3dce595640.jpg',
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						name: 'なっかあ',
 | 
				
			||||||
 | 
						icon: 'https://assets.misskey-hub.net/patrons/c2f5f3e394e74a64912284a2f4ca710e.jpg',
 | 
				
			||||||
}];
 | 
					}];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const patrons = [
 | 
					const patrons = [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -417,6 +417,25 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</MkFolder>
 | 
								</MkFolder>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<MkFolder v-if="matchQuery([i18n.ts._role._options.reactionsPerNoteLimit, 'reactionsPerNoteLimit'])">
 | 
				
			||||||
 | 
									<template #label>{{ i18n.ts._role._options.reactionsPerNoteLimit }}</template>
 | 
				
			||||||
 | 
									<template #suffix>
 | 
				
			||||||
 | 
										<span v-if="role.policies.reactionsPerNoteLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
 | 
				
			||||||
 | 
										<span v-else>{{ role.policies.reactionsPerNoteLimit.value }}</span>
 | 
				
			||||||
 | 
										<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.reactionsPerNoteLimit)"></i></span>
 | 
				
			||||||
 | 
									</template>
 | 
				
			||||||
 | 
									<div class="_gaps">
 | 
				
			||||||
 | 
										<MkSwitch v-model="role.policies.reactionsPerNoteLimit.useDefault" :readonly="readonly">
 | 
				
			||||||
 | 
											<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 | 
				
			||||||
 | 
										</MkSwitch>
 | 
				
			||||||
 | 
										<MkInput v-model="role.policies.reactionsPerNoteLimit.value" :disabled="role.policies.reactionsPerNoteLimit.useDefault" type="number" :readonly="readonly">
 | 
				
			||||||
 | 
										</MkInput>
 | 
				
			||||||
 | 
										<MkRange v-model="role.policies.reactionsPerNoteLimit.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>
 | 
				
			||||||
 | 
								</MkFolder>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])">
 | 
								<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])">
 | 
				
			||||||
				<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
 | 
									<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
 | 
				
			||||||
				<template #suffix>
 | 
									<template #suffix>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -149,6 +149,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
							</MkInput>
 | 
												</MkInput>
 | 
				
			||||||
						</MkFolder>
 | 
											</MkFolder>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											<MkFolder v-if="matchQuery([i18n.ts._role._options.reactionsPerNoteLimit, 'reactionsPerNoteLimit'])">
 | 
				
			||||||
 | 
												<template #label>{{ i18n.ts._role._options.reactionsPerNoteLimit }}</template>
 | 
				
			||||||
 | 
												<template #suffix>{{ policies.reactionsPerNoteLimit }}</template>
 | 
				
			||||||
 | 
												<MkInput v-model="policies.reactionsPerNoteLimit" type="number">
 | 
				
			||||||
 | 
												</MkInput>
 | 
				
			||||||
 | 
											</MkFolder>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])">
 | 
											<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])">
 | 
				
			||||||
							<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
 | 
												<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
 | 
				
			||||||
							<template #suffix>{{ policies.antennaLimit }}</template>
 | 
												<template #suffix>{{ policies.antennaLimit }}</template>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -236,10 +236,12 @@ function reportAbuse() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	const pageUrl = `${url}/play/${flash.value.id}`;
 | 
						const pageUrl = `${url}/play/${flash.value.id}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
 | 
						const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
 | 
				
			||||||
		user: flash.value.user,
 | 
							user: flash.value.user,
 | 
				
			||||||
		initialComment: `Play: ${pageUrl}\n-----\n`,
 | 
							initialComment: `Play: ${pageUrl}\n-----\n`,
 | 
				
			||||||
	}, {}, 'closed');
 | 
						}, {
 | 
				
			||||||
 | 
							closed: () => dispose(),
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function showMenu(ev: MouseEvent) {
 | 
					function showMenu(ev: MouseEvent) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -160,10 +160,12 @@ function reportAbuse() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	const pageUrl = `${url}/gallery/${post.value.id}`;
 | 
						const pageUrl = `${url}/gallery/${post.value.id}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
 | 
						const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
 | 
				
			||||||
		user: post.value.user,
 | 
							user: post.value.user,
 | 
				
			||||||
		initialComment: `Post: ${pageUrl}\n-----\n`,
 | 
							initialComment: `Post: ${pageUrl}\n-----\n`,
 | 
				
			||||||
	}, {}, 'closed');
 | 
						}, {
 | 
				
			||||||
 | 
							closed: () => dispose(),
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function showMenu(ev: MouseEvent) {
 | 
					function showMenu(ev: MouseEvent) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -245,10 +245,12 @@ function reportAbuse() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	const pageUrl = `${url}/@${props.username}/pages/${props.pageName}`;
 | 
						const pageUrl = `${url}/@${props.username}/pages/${props.pageName}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
 | 
						const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
 | 
				
			||||||
		user: page.value.user,
 | 
							user: page.value.user,
 | 
				
			||||||
		initialComment: `Page: ${pageUrl}\n-----\n`,
 | 
							initialComment: `Page: ${pageUrl}\n-----\n`,
 | 
				
			||||||
	}, {}, 'closed');
 | 
						}, {
 | 
				
			||||||
 | 
							closed: () => dispose(),
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function showMenu(ev: MouseEvent) {
 | 
					function showMenu(ev: MouseEvent) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -240,7 +240,7 @@ function closeTutorial(): void {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function switchTlIfNeeded() {
 | 
					function switchTlIfNeeded() {
 | 
				
			||||||
	if (isBasicTimeline(src.value) && !availableBasicTimelines().includes(src.value)) {
 | 
						if (isBasicTimeline(src.value) && !isAvailableBasicTimeline(src.value)) {
 | 
				
			||||||
		src.value = availableBasicTimelines()[0];
 | 
							src.value = availableBasicTimelines()[0];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,9 +4,12 @@ import type {
 | 
				
			|||||||
	Ad,
 | 
						Ad,
 | 
				
			||||||
	Announcement,
 | 
						Announcement,
 | 
				
			||||||
	EmojiDetailed,
 | 
						EmojiDetailed,
 | 
				
			||||||
 | 
						Flash,
 | 
				
			||||||
 | 
						GalleryPost,
 | 
				
			||||||
	InviteCode,
 | 
						InviteCode,
 | 
				
			||||||
	MetaDetailed,
 | 
						MetaDetailed,
 | 
				
			||||||
	Note,
 | 
						Note,
 | 
				
			||||||
 | 
						Page,
 | 
				
			||||||
	Role,
 | 
						Role,
 | 
				
			||||||
	ReversiGameDetailed,
 | 
						ReversiGameDetailed,
 | 
				
			||||||
	SystemWebhook,
 | 
						SystemWebhook,
 | 
				
			||||||
@@ -405,18 +408,18 @@ export type ModerationLogPayloads = {
 | 
				
			|||||||
		pageId: string;
 | 
							pageId: string;
 | 
				
			||||||
		pageUserId: string;
 | 
							pageUserId: string;
 | 
				
			||||||
		pageUserUsername: string;
 | 
							pageUserUsername: string;
 | 
				
			||||||
		page: any;
 | 
							page: Page;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	deleteFlash: {
 | 
						deleteFlash: {
 | 
				
			||||||
		flashId: string;
 | 
							flashId: string;
 | 
				
			||||||
		flashUserId: string;
 | 
							flashUserId: string;
 | 
				
			||||||
		flashUserUsername: string;
 | 
							flashUserUsername: string;
 | 
				
			||||||
		flash: any;
 | 
							flash: Flash;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	deleteGalleryPost: {
 | 
						deleteGalleryPost: {
 | 
				
			||||||
		postId: string;
 | 
							postId: string;
 | 
				
			||||||
		postUserId: string;
 | 
							postUserId: string;
 | 
				
			||||||
		postUserUsername: string;
 | 
							postUserUsername: string;
 | 
				
			||||||
		post: any;
 | 
							post: GalleryPost;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user