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;
/**
*
*/
"reactionsPerNoteLimit": string;
/**
*
*/

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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