Compare commits
	
		
			7 Commits
		
	
	
		
			2025.3.1-b
			...
			multiple-r
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0ddd4bc545 | ||
| 
						 | 
					cb6a1c773e | ||
| 
						 | 
					9df887ba93 | ||
| 
						 | 
					a2769d0733 | ||
| 
						 | 
					036f90133c | ||
| 
						 | 
					f9bfff604d | ||
| 
						 | 
					fd0e840138 | 
							
								
								
									
										4
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -6686,6 +6686,10 @@ export interface Locale extends ILocale {
 | 
			
		||||
             * ノートのピン留めの最大数
 | 
			
		||||
             */
 | 
			
		||||
            "pinMax": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * 一つのノートに対する最大リアクション数
 | 
			
		||||
             */
 | 
			
		||||
            "reactionsPerNoteLimit": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * アンテナの作成可能数
 | 
			
		||||
             */
 | 
			
		||||
 
 | 
			
		||||
@@ -1728,6 +1728,7 @@ _role:
 | 
			
		||||
    alwaysMarkNsfw: "ファイルにNSFWを常に付与"
 | 
			
		||||
    canUpdateBioMedia: "アイコンとバナーの更新を許可"
 | 
			
		||||
    pinMax: "ノートのピン留めの最大数"
 | 
			
		||||
    reactionsPerNoteLimit: "一つのノートに対する最大リアクション数"
 | 
			
		||||
    antennaMax: "アンテナの作成可能数"
 | 
			
		||||
    wordMuteMax: "ワードミュートの最大文字数"
 | 
			
		||||
    webhookMax: "Webhookの作成可能数"
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		// Create reaction
 | 
			
		||||
		try {
 | 
			
		||||
			await this.noteReactionsRepository.insert(record);
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			if (isDuplicateKeyValueError(e)) {
 | 
			
		||||
				const exists = await this.noteReactionsRepository.findOneByOrFail({
 | 
			
		||||
					noteId: note.id,
 | 
			
		||||
					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 {
 | 
			
		||||
				throw e;
 | 
			
		||||
			}
 | 
			
		||||
@@ -286,11 +274,12 @@ export class ReactionService {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@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
 | 
			
		||||
		const exist = await this.noteReactionsRepository.findOneBy({
 | 
			
		||||
			noteId: note.id,
 | 
			
		||||
			userId: user.id,
 | 
			
		||||
			reaction: _reaction ?? FALLBACK,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (exist == null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,7 @@ export type RolePolicies = {
 | 
			
		||||
	alwaysMarkNsfw: boolean;
 | 
			
		||||
	canUpdateBioMedia: boolean;
 | 
			
		||||
	pinLimit: number;
 | 
			
		||||
	reactionsPerNoteLimit: number;
 | 
			
		||||
	antennaLimit: number;
 | 
			
		||||
	wordMuteLimit: number;
 | 
			
		||||
	webhookLimit: number;
 | 
			
		||||
@@ -78,6 +79,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
 | 
			
		||||
	alwaysMarkNsfw: false,
 | 
			
		||||
	canUpdateBioMedia: true,
 | 
			
		||||
	pinLimit: 5,
 | 
			
		||||
	reactionsPerNoteLimit: 1,
 | 
			
		||||
	antennaLimit: 5,
 | 
			
		||||
	wordMuteLimit: 200,
 | 
			
		||||
	webhookLimit: 3,
 | 
			
		||||
@@ -380,6 +382,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		||||
			alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
 | 
			
		||||
			canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
 | 
			
		||||
			pinLimit: calc('pinLimit', vs => Math.max(...vs)),
 | 
			
		||||
			reactionsPerNoteLimit: calc('reactionsPerNoteLimit', vs => Math.max(...vs)),
 | 
			
		||||
			antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
 | 
			
		||||
			wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
 | 
			
		||||
			webhookLimit: calc('webhookLimit', vs => Math.max(...vs)),
 | 
			
		||||
 
 | 
			
		||||
@@ -170,10 +170,10 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	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) {
 | 
			
		||||
			const reaction = _hint_.myReactions.get(note.id);
 | 
			
		||||
		if (_hint_?.myReactionsMap) {
 | 
			
		||||
			const reaction = _hint_.myReactionsMap.get(note.id);
 | 
			
		||||
			if (reaction) {
 | 
			
		||||
				return this.reactionService.convertLegacyReaction(reaction);
 | 
			
		||||
			} else {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,8 @@ import { MiUser } from './User.js';
 | 
			
		||||
import { MiNote } from './Note.js';
 | 
			
		||||
 | 
			
		||||
@Entity('note_reaction')
 | 
			
		||||
@Index(['userId', 'noteId'], { unique: true })
 | 
			
		||||
@Index(['userId', 'noteId'])
 | 
			
		||||
@Index(['userId', 'noteId', 'reaction'], { unique: true })
 | 
			
		||||
export class MiNoteReaction {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -236,6 +236,10 @@ export const packedRolePoliciesSchema = {
 | 
			
		||||
			type: 'integer',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		reactionsPerNoteLimit: {
 | 
			
		||||
			type: 'integer',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		antennaLimit: {
 | 
			
		||||
			type: 'integer',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -89,6 +89,7 @@ export const ROLE_POLICIES = [
 | 
			
		||||
	'alwaysMarkNsfw',
 | 
			
		||||
	'canUpdateBioMedia',
 | 
			
		||||
	'pinLimit',
 | 
			
		||||
	'reactionsPerNoteLimit',
 | 
			
		||||
	'antennaLimit',
 | 
			
		||||
	'wordMuteLimit',
 | 
			
		||||
	'webhookLimit',
 | 
			
		||||
 
 | 
			
		||||
@@ -417,6 +417,25 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
				</div>
 | 
			
		||||
			</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'])">
 | 
			
		||||
				<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
 | 
			
		||||
				<template #suffix>
 | 
			
		||||
 
 | 
			
		||||
@@ -149,6 +149,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
							</MkInput>
 | 
			
		||||
						</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'])">
 | 
			
		||||
							<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
 | 
			
		||||
							<template #suffix>{{ policies.antennaLimit }}</template>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user