Compare commits
7 commits
develop
...
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;
|
"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の作成可能数"
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -89,6 +89,7 @@ export const ROLE_POLICIES = [
|
||||||
'alwaysMarkNsfw',
|
'alwaysMarkNsfw',
|
||||||
'canUpdateBioMedia',
|
'canUpdateBioMedia',
|
||||||
'pinLimit',
|
'pinLimit',
|
||||||
|
'reactionsPerNoteLimit',
|
||||||
'antennaLimit',
|
'antennaLimit',
|
||||||
'wordMuteLimit',
|
'wordMuteLimit',
|
||||||
'webhookLimit',
|
'webhookLimit',
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue