Compare commits

...

7 commits

Author SHA1 Message Date
syuilo 0ddd4bc545 Merge branch 'develop' into multiple-reactions 2024-08-18 13:22:22 +09:00
syuilo cb6a1c773e Update about-misskey.vue 2024-08-17 17:52:06 +09:00
syuilo 9df887ba93 Merge branch 'develop' into multiple-reactions 2024-08-17 17:23:02 +09:00
syuilo a2769d0733 wip 2024-07-17 17:13:01 +09:00
syuilo 036f90133c Merge branch 'develop' into multiple-reactions 2024-07-16 17:05:19 +09:00
syuilo f9bfff604d Merge branch 'develop' into multiple-reactions 2024-07-02 14:47:30 +09:00
syuilo fd0e840138 wip 2024-07-02 09:54:57 +09:00
11 changed files with 68 additions and 19 deletions

4
locales/index.d.ts vendored
View file

@ -6686,6 +6686,10 @@ export interface Locale extends ILocale {
* *
*/ */
"pinMax": string; "pinMax": string;
/**
*
*/
"reactionsPerNoteLimit": string;
/** /**
* *
*/ */

View file

@ -1728,6 +1728,7 @@ _role:
alwaysMarkNsfw: "ファイルにNSFWを常に付与" alwaysMarkNsfw: "ファイルにNSFWを常に付与"
canUpdateBioMedia: "アイコンとバナーの更新を許可" canUpdateBioMedia: "アイコンとバナーの更新を許可"
pinMax: "ノートのピン留めの最大数" pinMax: "ノートのピン留めの最大数"
reactionsPerNoteLimit: "一つのノートに対する最大リアクション数"
antennaMax: "アンテナの作成可能数" antennaMax: "アンテナの作成可能数"
wordMuteMax: "ワードミュートの最大文字数" wordMuteMax: "ワードミュートの最大文字数"
webhookMax: "Webhookの作成可能数" webhookMax: "Webhookの作成可能数"

View file

@ -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") ');
}
}

View file

@ -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,
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'); 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) {

View file

@ -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)),

View file

@ -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 {

View file

@ -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;

View file

@ -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,

View file

@ -89,6 +89,7 @@ export const ROLE_POLICIES = [
'alwaysMarkNsfw', 'alwaysMarkNsfw',
'canUpdateBioMedia', 'canUpdateBioMedia',
'pinLimit', 'pinLimit',
'reactionsPerNoteLimit',
'antennaLimit', 'antennaLimit',
'wordMuteLimit', 'wordMuteLimit',
'webhookLimit', 'webhookLimit',

View file

@ -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>

View file

@ -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>