mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-08-18 07:28:43 +00:00
Compare commits
30 Commits
develop
...
sp-reaction
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e2a65f1de | |||
| f590682326 | |||
| 88ad7ddc3d | |||
| e321369c32 | |||
| b7f7962565 | |||
| 9060c4cb8a | |||
| 115d955768 | |||
| 20cfbad90f | |||
| b590d52874 | |||
| b356edb810 | |||
| 5109a106f7 | |||
| e5d7084634 | |||
| 9acc9b0f5e | |||
| 060ac9aef4 | |||
| a1791ae189 | |||
| 2c93ecacf6 | |||
| 3405db42ea | |||
| 4f2ab4e6e2 | |||
| 67071de164 | |||
| 9d091a5245 | |||
| 1beaee367c | |||
| 9838686437 | |||
| a567bef08a | |||
| 464256c6b5 | |||
| f08d6e46df | |||
| d53b2f532a | |||
| 40f6acd720 | |||
| 9dc4762fe0 | |||
| f3d26722bb | |||
| 5eb873ff91 |
@@ -1422,6 +1422,8 @@ prepend: "先頭に追加"
|
||||
urlPreviewSensitiveList: "サムネイルの表示を制限するURL"
|
||||
urlPreviewSensitiveListDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、サムネイルが表示されなくなります。"
|
||||
pixelatedZoom: "ピクセルアート拡大モード"
|
||||
spReaction: "スペシャルリアクション"
|
||||
noRemainingUsesForSpReaction: "スペシャルリアクションの残り使用可能回数がありません"
|
||||
|
||||
_imageEditing:
|
||||
_vars:
|
||||
@@ -1792,6 +1794,12 @@ _serverSettings:
|
||||
entrancePageStyle: "エントランスページのスタイル"
|
||||
showTimelineForVisitor: "タイムラインを表示する"
|
||||
showActivitiesForVisitor: "アクティビティを表示する"
|
||||
features: "機能"
|
||||
|
||||
_spReactions:
|
||||
enable: "スペシャルリアクションを有効にする"
|
||||
description1: "通常のリアクションより目立つ「スペシャルリアクション」をノートに送れる機能です。"
|
||||
description2: "有効にする場合、ロールポリシーで、毎月送ることのできる最大数を設定してください。"
|
||||
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "全て公開"
|
||||
@@ -2150,6 +2158,7 @@ _role:
|
||||
noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数"
|
||||
scheduledNoteLimit: "予約投稿の同時作成可能数"
|
||||
watermarkAvailable: "ウォーターマーク機能の使用可否"
|
||||
spReactionsMonthlyLimit: "スペシャルリアクションの月間送信上限"
|
||||
_condition:
|
||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||
isLocal: "ローカルユーザー"
|
||||
@@ -2916,6 +2925,7 @@ _notification:
|
||||
renote: "リノート"
|
||||
quote: "引用"
|
||||
reaction: "リアクション"
|
||||
spReaction: "スペシャルリアクション"
|
||||
pollEnded: "アンケートが終了"
|
||||
scheduledNotePosted: "予約投稿が成功した"
|
||||
scheduledNotePostFailed: "予約投稿が失敗した"
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@11.11.0",
|
||||
"packageManager": "pnpm@11.22.0",
|
||||
"workspaces": [
|
||||
"packages/backend",
|
||||
"packages/frontend-shared",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class SpReactions1769664628306 {
|
||||
name = 'SpReactions1769664628306'
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "note_sp_reaction" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "reaction" character varying(260) NOT NULL, "noteUserId" character varying(32) NOT NULL, CONSTRAINT "PK_11fd5ecdd9bb91517edfcf890d9" PRIMARY KEY ("id")); COMMENT ON COLUMN "note_sp_reaction"."noteUserId" IS '[Denormalized]'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3463a48b09fa41e1826ebd9f58" ON "note_sp_reaction" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_bfe4caa46cc0526bc2932d6dbe" ON "note_sp_reaction" ("noteId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_8be6eb3f4edc9940a3f8142669" ON "note_sp_reaction" ("noteUserId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b5f210b20bd987fe8584c85d33" ON "note_sp_reaction" ("userId", "noteId") `);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableSpReaction" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "note_sp_reaction" ADD CONSTRAINT "FK_3463a48b09fa41e1826ebd9f585" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "note_sp_reaction" ADD CONSTRAINT "FK_bfe4caa46cc0526bc2932d6dbed" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note_sp_reaction" DROP CONSTRAINT "FK_bfe4caa46cc0526bc2932d6dbed"`);
|
||||
await queryRunner.query(`ALTER TABLE "note_sp_reaction" DROP CONSTRAINT "FK_3463a48b09fa41e1826ebd9f585"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableSpReaction"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_b5f210b20bd987fe8584c85d33"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_8be6eb3f4edc9940a3f8142669"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_bfe4caa46cc0526bc2932d6dbe"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_3463a48b09fa41e1826ebd9f58"`);
|
||||
await queryRunner.query(`DROP TABLE "note_sp_reaction"`);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta } from '@/models/_.js';
|
||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta, MiNoteSpReaction, NoteSpReactionsRepository } from '@/models/_.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
@@ -73,6 +74,9 @@ export class ReactionService {
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -82,6 +86,9 @@ export class ReactionService {
|
||||
@Inject(DI.noteReactionsRepository)
|
||||
private noteReactionsRepository: NoteReactionsRepository,
|
||||
|
||||
@Inject(DI.noteSpReactionsRepository)
|
||||
private noteSpReactionsRepository: NoteSpReactionsRepository,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
@@ -337,6 +344,135 @@ export class ReactionService {
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createSp(user: { id: MiUser['id']; isBot: MiUser['isBot'] }, note: MiNote, reaction: string) {
|
||||
if (!this.meta.enableSpReaction) {
|
||||
throw new IdentifiableError('52c432ea-b166-491c-a73b-5dd703221b20');
|
||||
}
|
||||
|
||||
if (note.userId === user.id) {
|
||||
throw new IdentifiableError('afa694bf-6661-4d72-b8f7-bfb86a7545a1');
|
||||
}
|
||||
|
||||
if (note.userHost !== null) {
|
||||
throw new IdentifiableError('b59abda2-0d81-49e3-8148-80cf35ac4402');
|
||||
}
|
||||
|
||||
if (note.reactionAcceptance === 'likeOnly') {
|
||||
throw new IdentifiableError('1b168811-a1aa-470a-9b61-7fcf807cf9c1');
|
||||
}
|
||||
|
||||
if (!await this.noteEntityService.isVisibleForMe(note, user.id)) {
|
||||
throw new IdentifiableError('3ce0e3bc-7d48-4e87-a902-578c6ffd369e', 'Note not accessible for you.');
|
||||
}
|
||||
|
||||
// monthly limit
|
||||
const policies = await this.roleService.getUserPolicies(user.id);
|
||||
if (policies.spReactionsMonthlyLimit === 0) {
|
||||
throw new IdentifiableError('e371be02-9478-4133-90ef-8401ee38e474');
|
||||
}
|
||||
const month = new Date().getUTCMonth() + 1;
|
||||
const monthlySpReactionsCountMapKey = `monthlySpReactionsCountMap:${user.id}:${month}`;
|
||||
const count = await this.redisClient.get(monthlySpReactionsCountMapKey);
|
||||
if (count != null && parseInt(count, 10) >= policies.spReactionsMonthlyLimit) {
|
||||
throw new IdentifiableError('82e1a10c-52a8-4ccb-8ff7-3678bff68444');
|
||||
}
|
||||
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||
if (blocked) {
|
||||
throw new IdentifiableError('388ee683-8720-4aea-9ac8-b8c92d260815');
|
||||
}
|
||||
|
||||
const custom = reaction.match(isCustomEmojiRegexp);
|
||||
if (custom) {
|
||||
const name = custom[1];
|
||||
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
|
||||
|
||||
if (emoji == null) {
|
||||
throw new IdentifiableError('47c098e2-d0b6-4197-8d00-5a68bbb156be');
|
||||
}
|
||||
|
||||
// センシティブ
|
||||
if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) {
|
||||
throw new IdentifiableError('7fc2efbd-2652-4a60-975b-6eb65f60c7b3');
|
||||
}
|
||||
|
||||
if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 && !(await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) {
|
||||
// リアクションとして使う権限がない
|
||||
throw new IdentifiableError('63288e20-4251-4c62-a9d5-9da4e0bdd41e');
|
||||
}
|
||||
|
||||
reaction = `:${name}:`;
|
||||
} else {
|
||||
reaction = this.normalize(reaction);
|
||||
}
|
||||
|
||||
const record: MiNoteSpReaction = {
|
||||
id: this.idService.gen(),
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
reaction,
|
||||
noteUserId: note.userId,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.noteSpReactionsRepository.insert(record);
|
||||
} catch (e) {
|
||||
if (isDuplicateKeyValueError(e)) {
|
||||
throw new IdentifiableError('c9e8b0d0-d532-4453-8cc1-5cf8e95ba764');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Increment reactions count
|
||||
const spReaction = `sp:${reaction}`;
|
||||
if (this.meta.enableReactionsBuffering) {
|
||||
await this.reactionsBufferingService.create(note.id, user.id, spReaction, note.reactionAndUserPairCache);
|
||||
} else {
|
||||
const sql = `jsonb_set("reactions", '{${spReaction}}', (COALESCE("reactions"->>'${spReaction}', '0')::int + 1)::text::jsonb)`;
|
||||
await this.notesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
reactions: () => sql,
|
||||
...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
|
||||
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${spReaction}')`,
|
||||
} : {}),
|
||||
})
|
||||
.where('id = :id', { id: note.id })
|
||||
.execute();
|
||||
}
|
||||
|
||||
// increment monthly reactions count
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.incr(monthlySpReactionsCountMapKey);
|
||||
redisPipeline.expireat(monthlySpReactionsCountMapKey,
|
||||
(Date.now() / 1000) + (60 * 60 * 24 * 40), // TTLは最低でも一か月存続しさえすれば厳密でなくていい
|
||||
'NX',
|
||||
);
|
||||
redisPipeline.exec();
|
||||
|
||||
// 3日以内に投稿されたノートの場合ハイライト用ランキング更新
|
||||
if (
|
||||
(Date.now() - this.idService.parse(note.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3
|
||||
) {
|
||||
if (note.channelId != null) {
|
||||
if (note.replyId == null) {
|
||||
this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1);
|
||||
}
|
||||
} else {
|
||||
if (note.visibility === 'public' && note.replyId == null) {
|
||||
this.featuredService.updateGlobalNotesRanking(note.id, 1);
|
||||
this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.notificationService.createNotification(note.userId, 'spReaction', {
|
||||
noteId: note.id,
|
||||
reaction: reaction,
|
||||
}, user.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* - 文字列タイプのレガシーな形式のリアクションを現在の形式に変換する
|
||||
* - ローカルのリアクションのホストを `@.` にする(`decodeReaction()`の効果)
|
||||
|
||||
@@ -73,6 +73,7 @@ export type RolePolicies = {
|
||||
noteDraftLimit: number;
|
||||
scheduledNoteLimit: number;
|
||||
watermarkAvailable: boolean;
|
||||
spReactionsMonthlyLimit: number;
|
||||
};
|
||||
|
||||
export const DEFAULT_POLICIES: RolePolicies = {
|
||||
@@ -121,6 +122,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
noteDraftLimit: 10,
|
||||
scheduledNoteLimit: 1,
|
||||
watermarkAvailable: true,
|
||||
spReactionsMonthlyLimit: 0,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -29,6 +29,7 @@ const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set([
|
||||
'renote:grouped',
|
||||
'quote',
|
||||
'reaction',
|
||||
'spReaction',
|
||||
'reaction:grouped',
|
||||
'pollEnded',
|
||||
'scheduledNotePosted',
|
||||
|
||||
@@ -24,6 +24,7 @@ export const DI = {
|
||||
noteFavoritesRepository: Symbol('noteFavoritesRepository'),
|
||||
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
|
||||
noteReactionsRepository: Symbol('noteReactionsRepository'),
|
||||
noteSpReactionsRepository: Symbol('noteSpReactionsRepository'),
|
||||
pollsRepository: Symbol('pollsRepository'),
|
||||
pollVotesRepository: Symbol('pollVotesRepository'),
|
||||
userProfilesRepository: Symbol('userProfilesRepository'),
|
||||
|
||||
@@ -747,6 +747,11 @@ export class MiMeta {
|
||||
})
|
||||
public showRoleBadgesOfRemoteUsers: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableSpReaction: boolean;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: { },
|
||||
})
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiNote } from './Note.js';
|
||||
|
||||
@Entity('note_sp_reaction')
|
||||
@Index(['userId', 'noteId'], { unique: true })
|
||||
export class MiNoteSpReaction {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(() => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user?: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public noteId: MiNote['id'];
|
||||
|
||||
@ManyToOne(() => MiNote, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public note?: MiNote | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 260,
|
||||
})
|
||||
public reaction: string;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: '[Denormalized]',
|
||||
})
|
||||
public noteUserId: MiUser['id'];
|
||||
//#endregion
|
||||
}
|
||||
@@ -55,6 +55,13 @@ export type MiNotification = {
|
||||
notifierId: MiUser['id'];
|
||||
noteId: MiNote['id'];
|
||||
reaction: string;
|
||||
} | {
|
||||
type: 'spReaction';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
notifierId: MiUser['id'];
|
||||
noteId: MiNote['id'];
|
||||
reaction: string;
|
||||
} | {
|
||||
type: 'pollEnded';
|
||||
id: string;
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
MiNote,
|
||||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteSpReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiNoteDraft,
|
||||
MiPage,
|
||||
@@ -142,6 +143,12 @@ const $noteReactionsRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $noteSpReactionsRepository: Provider = {
|
||||
provide: DI.noteSpReactionsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiNoteSpReaction).extend(miRepository as MiRepository<MiNoteSpReaction>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $noteDraftsRepository: Provider = {
|
||||
provide: DI.noteDraftsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiNoteDraft).extend(miRepository as MiRepository<MiNoteDraft>),
|
||||
@@ -556,6 +563,7 @@ const $reversiGamesRepository: Provider = {
|
||||
$noteFavoritesRepository,
|
||||
$noteThreadMutingsRepository,
|
||||
$noteReactionsRepository,
|
||||
$noteSpReactionsRepository,
|
||||
$noteDraftsRepository,
|
||||
$pollsRepository,
|
||||
$pollVotesRepository,
|
||||
@@ -634,6 +642,7 @@ const $reversiGamesRepository: Provider = {
|
||||
$noteFavoritesRepository,
|
||||
$noteThreadMutingsRepository,
|
||||
$noteReactionsRepository,
|
||||
$noteSpReactionsRepository,
|
||||
$noteDraftsRepository,
|
||||
$pollsRepository,
|
||||
$pollVotesRepository,
|
||||
|
||||
@@ -23,7 +23,7 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiChannel } from '@/models/Channel.js';
|
||||
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
|
||||
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
|
||||
import { MiChannelMuting } from "@/models/ChannelMuting.js";
|
||||
import { MiChannelMuting } from '@/models/ChannelMuting.js';
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiChatMessage } from '@/models/ChatMessage.js';
|
||||
import { MiChatRoom } from '@/models/ChatRoom.js';
|
||||
@@ -50,6 +50,7 @@ import { MiNote } from '@/models/Note.js';
|
||||
import { MiNoteDraft } from '@/models/NoteDraft.js';
|
||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||
import { MiNoteSpReaction } from '@/models/NoteSpReaction.js';
|
||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
||||
import { MiPage } from '@/models/Page.js';
|
||||
import { MiPageLike } from '@/models/PageLike.js';
|
||||
@@ -131,6 +132,7 @@ export {
|
||||
MiNoteDraft,
|
||||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteSpReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiPage,
|
||||
MiPageLike,
|
||||
@@ -211,6 +213,7 @@ export type NotesRepository = Repository<MiNote> & MiRepository<MiNote>;
|
||||
export type NoteDraftsRepository = Repository<MiNoteDraft> & MiRepository<MiNoteDraft>;
|
||||
export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>;
|
||||
export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>;
|
||||
export type NoteSpReactionsRepository = Repository<MiNoteSpReaction> & MiRepository<MiNoteSpReaction>;
|
||||
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>;
|
||||
export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>;
|
||||
export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>;
|
||||
|
||||
@@ -182,6 +182,35 @@ export const packedNotificationSchema = {
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...baseSchema.properties,
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['spReaction'],
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
note: {
|
||||
type: 'object',
|
||||
ref: 'Note',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
reaction: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
@@ -329,6 +329,10 @@ export const packedRolePoliciesSchema = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
spReactionsMonthlyLimit: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -608,6 +608,7 @@ export const packedMeDetailedOnlySchema = {
|
||||
renote: { optional: true, ...notificationRecieveConfig },
|
||||
quote: { optional: true, ...notificationRecieveConfig },
|
||||
reaction: { optional: true, ...notificationRecieveConfig },
|
||||
spReaction: { optional: true, ...notificationRecieveConfig },
|
||||
pollEnded: { optional: true, ...notificationRecieveConfig },
|
||||
scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
|
||||
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
|
||||
|
||||
@@ -44,6 +44,7 @@ import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||
import { MiNoteSpReaction } from '@/models/NoteSpReaction.js';
|
||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
||||
import { MiNoteDraft } from '@/models/NoteDraft.js';
|
||||
import { MiPage } from '@/models/Page.js';
|
||||
@@ -204,6 +205,7 @@ export const entities = [
|
||||
MiNote,
|
||||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteSpReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiNoteDraft,
|
||||
MiPage,
|
||||
|
||||
@@ -335,6 +335,8 @@ export * as 'notes/polls/vote' from './endpoints/notes/polls/vote.js';
|
||||
export * as 'notes/reactions' from './endpoints/notes/reactions.js';
|
||||
export * as 'notes/reactions/create' from './endpoints/notes/reactions/create.js';
|
||||
export * as 'notes/reactions/delete' from './endpoints/notes/reactions/delete.js';
|
||||
export * as 'notes/sp-reactions' from './endpoints/notes/sp-reactions.js';
|
||||
export * as 'notes/sp-reactions/create' from './endpoints/notes/sp-reactions/create.js';
|
||||
export * as 'notes/renotes' from './endpoints/notes/renotes.js';
|
||||
export * as 'notes/replies' from './endpoints/notes/replies.js';
|
||||
export * as 'notes/search' from './endpoints/notes/search.js';
|
||||
|
||||
@@ -620,6 +620,10 @@ export const meta = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableSpReaction: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -781,6 +785,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes,
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes,
|
||||
showRoleBadgesOfRemoteUsers: instance.showRoleBadgesOfRemoteUsers,
|
||||
enableSpReaction: instance.enableSpReaction,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,6 +228,7 @@ export const paramDef = {
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' },
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' },
|
||||
showRoleBadgesOfRemoteUsers: { type: 'boolean' },
|
||||
enableSpReaction: { type: 'boolean' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
@@ -792,6 +793,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
set.showRoleBadgesOfRemoteUsers = ps.showRoleBadgesOfRemoteUsers;
|
||||
}
|
||||
|
||||
if (ps.enableSpReaction !== undefined) {
|
||||
set.enableSpReaction = ps.enableSpReaction;
|
||||
}
|
||||
|
||||
const before = await this.metaService.fetch(true);
|
||||
|
||||
await this.metaService.update(set);
|
||||
|
||||
@@ -208,6 +208,7 @@ export const paramDef = {
|
||||
renote: notificationRecieveConfig,
|
||||
quote: notificationRecieveConfig,
|
||||
reaction: notificationRecieveConfig,
|
||||
spReaction: notificationRecieveConfig,
|
||||
pollEnded: notificationRecieveConfig,
|
||||
scheduledNotePosted: notificationRecieveConfig,
|
||||
scheduledNotePostFailed: notificationRecieveConfig,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import type { NoteSpReactionsRepository } from '@/models/_.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'reactions'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
allowGet: true,
|
||||
cacheSec: 60,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'NoteReaction',
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: '447a14db-4245-4aa9-ba11-66e9ca290ec3',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', format: 'misskey:id' },
|
||||
type: { type: 'string', nullable: true },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
},
|
||||
required: ['noteId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.noteSpReactionsRepository)
|
||||
private noteSpReactionsRepository: NoteSpReactionsRepository,
|
||||
|
||||
private noteReactionEntityService: NoteReactionEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.noteSpReactionsRepository.createQueryBuilder('reaction'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('reaction.noteId = :noteId', { noteId: ps.noteId })
|
||||
.leftJoinAndSelect('reaction.user', 'user')
|
||||
.leftJoinAndSelect('reaction.note', 'note');
|
||||
|
||||
if (ps.type) {
|
||||
// ローカルリアクションはホスト名が . とされているが
|
||||
// DB 上ではそうではないので、必要に応じて変換
|
||||
const suffix = '@.:';
|
||||
const type = ps.type.endsWith(suffix) ? ps.type.slice(0, ps.type.length - suffix.length) + ':' : ps.type;
|
||||
query.andWhere('reaction.reaction = :type', { type });
|
||||
}
|
||||
|
||||
const reactions = await query.limit(ps.limit).getMany();
|
||||
|
||||
// noteSpReactionEntityServiceを使って使うべきだけど面倒なので流用
|
||||
return await this.noteReactionEntityService.packMany(reactions, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ReactionService } from '@/core/ReactionService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['sp-reactions', 'notes'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:reactions',
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: '92fac1fb-7645-4f25-93a9-84e4a70d7583',
|
||||
},
|
||||
|
||||
alreadyReacted: {
|
||||
message: 'You are already reacting to that note.',
|
||||
code: 'ALREADY_REACTED',
|
||||
id: '913b962a-8c22-4efa-b266-4437eea27074',
|
||||
},
|
||||
|
||||
youHaveBeenBlocked: {
|
||||
message: 'You cannot react this note because you have been blocked by this user.',
|
||||
code: 'YOU_HAVE_BEEN_BLOCKED',
|
||||
id: '5087cf55-51c0-4e21-b7a4-9a1a90291a85',
|
||||
},
|
||||
|
||||
cannotReactToRenote: {
|
||||
message: 'You cannot react to Renote.',
|
||||
code: 'CANNOT_REACT_TO_RENOTE',
|
||||
id: '938bb658-7d42-4361-b5cc-3eae0e0b8b9b',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', format: 'misskey:id' },
|
||||
reaction: { type: 'string' },
|
||||
},
|
||||
required: ['noteId', 'reaction'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private getterService: GetterService,
|
||||
private reactionService: ReactionService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const note = await this.getterService.getNote(ps.noteId).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
await this.reactionService.createSp(me, note, ps.reaction).catch(err => {
|
||||
if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
|
||||
if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
if (err.id === '12c35529-3c79-4327-b1cc-e2cf63a71925') throw new ApiError(meta.errors.cannotReactToRenote);
|
||||
throw err;
|
||||
});
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
* renote - 投稿がRenoteされた
|
||||
* quote - 投稿が引用Renoteされた
|
||||
* reaction - 投稿にリアクションされた
|
||||
* spReaction - スペシャルリアクションされた
|
||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
||||
* scheduledNotePosted - 予約したノートが投稿された
|
||||
* scheduledNotePostFailed - 予約したノートの投稿に失敗した
|
||||
@@ -33,6 +34,7 @@ export const notificationTypes = [
|
||||
'renote',
|
||||
'quote',
|
||||
'reaction',
|
||||
'spReaction',
|
||||
'pollEnded',
|
||||
'scheduledNotePosted',
|
||||
'scheduledNotePostFailed',
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"@storybook/types": "8.6.18",
|
||||
"@storybook/vue3": "10.5.0",
|
||||
"@storybook/vue3-vite": "10.5.0",
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
"@tabler/icons-webfont": "3.46.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
"@types/estree": "1.0.9",
|
||||
|
||||
@@ -197,8 +197,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, ref, useTemplateRef, provide, computed } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { Ref } from 'vue';
|
||||
import type { Keymap } from '@/utility/hotkey.js';
|
||||
import { useNote } from '@/composables/use-note.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@@ -208,7 +209,6 @@ import { isEnabledUrlPreview } from '@/utility/url-preview.js';
|
||||
import { focusPrev, focusNext } from '@/utility/focus.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { DI } from '@/di.js';
|
||||
import type { Keymap } from '@/utility/hotkey.js';
|
||||
|
||||
// コンポーネント外部の依存関係
|
||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||
@@ -288,7 +288,7 @@ const {
|
||||
menuButton,
|
||||
renoteButton,
|
||||
renoteTime,
|
||||
reactButton,
|
||||
reactButton: reactButton as Ref<HTMLElement>,
|
||||
clipButton,
|
||||
}, {
|
||||
inTimeline,
|
||||
|
||||
@@ -241,6 +241,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { inject, provide, ref, useTemplateRef, markRaw, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { Ref } from 'vue';
|
||||
import type { Keymap } from '@/utility/hotkey.js';
|
||||
import { useNote } from '@/composables/use-note.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@@ -251,7 +253,6 @@ import { Paginator } from '@/utility/paginator.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { DI } from '@/di.js';
|
||||
import type { Keymap } from '@/utility/hotkey.js';
|
||||
|
||||
// コンポーネント外部の依存関係
|
||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||
@@ -320,7 +321,7 @@ const {
|
||||
menuButton,
|
||||
renoteButton,
|
||||
renoteTime,
|
||||
reactButton,
|
||||
reactButton: reactButton as Ref<HTMLElement>,
|
||||
clipButton,
|
||||
}, {
|
||||
inChannel,
|
||||
|
||||
@@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
|
||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||
<i v-else-if="notification.type === 'spReaction'" class="ti ti-octahedron-plus"></i>
|
||||
<i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-send"></i>
|
||||
<i v-else-if="notification.type === 'scheduledNotePostFailed'" class="ti ti-alert-triangle"></i>
|
||||
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
||||
@@ -74,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span v-else-if="notification.type === 'createToken'">{{ i18n.ts._notification.createToken }}</span>
|
||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||
<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
|
||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'spReaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||
@@ -82,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||
</header>
|
||||
<div>
|
||||
<MkA v-if="notification.type === 'reaction' || notification.type === 'reaction:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<MkA v-if="notification.type === 'reaction' || notification.type === 'reaction:grouped' || notification.type === 'spReaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
|
||||
@@ -8,11 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
ref="buttonEl"
|
||||
v-ripple="canToggle"
|
||||
class="_button"
|
||||
:class="[$style.root, { [$style.reacted]: myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large' }]"
|
||||
:class="[$style.root, { [$style.reacted]: myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large', [$style.sp]: reaction.startsWith('sp:') }]"
|
||||
@click="toggleReaction()"
|
||||
@contextmenu.prevent.stop="menu"
|
||||
>
|
||||
<MkReactionIcon style="pointer-events: none;" :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
||||
<MkReactionIcon style="pointer-events: none;" :class="[$style.icon, { [$style.limitWidth]: prefer.s.limitWidthOfReaction }]" :reaction="unwrapSp(reaction)" :emojiUrl="reactionEmojis[unwrapSp(reaction).substring(1, unwrapSp(reaction).length - 1)]"/>
|
||||
<span :class="$style.count">{{ count }}</span>
|
||||
</button>
|
||||
</template>
|
||||
@@ -32,7 +32,6 @@ import { $i } from '@/i.js';
|
||||
import MkReactionEffect from '@/components/MkReactionEffect.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
|
||||
import { customEmojisMap } from '@/custom-emojis.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { DI } from '@/di.js';
|
||||
@@ -58,6 +57,13 @@ const emit = defineEmits<{
|
||||
|
||||
const buttonEl = useTemplateRef('buttonEl');
|
||||
|
||||
function unwrapSp(reaction: string): string {
|
||||
if (reaction.startsWith('sp:')) {
|
||||
return reaction.substring(3);
|
||||
}
|
||||
return reaction;
|
||||
}
|
||||
|
||||
const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, ''));
|
||||
|
||||
const canToggle = computed(() => {
|
||||
@@ -243,9 +249,9 @@ if (!mock) {
|
||||
useTooltip(buttonEl, async (showing) => {
|
||||
if (buttonEl.value == null) return;
|
||||
|
||||
const reactions = await misskeyApiGet('notes/reactions', {
|
||||
const reactions = await misskeyApiGet('notes/sp-reactions', {
|
||||
noteId: props.noteId,
|
||||
type: props.reaction,
|
||||
type: unwrapSp(props.reaction),
|
||||
limit: 10,
|
||||
_cacheKey_: props.count,
|
||||
});
|
||||
@@ -254,7 +260,7 @@ if (!mock) {
|
||||
|
||||
const { dispose } = os.popup(XDetails, {
|
||||
showing,
|
||||
reaction: props.reaction,
|
||||
reaction: unwrapSp(props.reaction),
|
||||
users,
|
||||
count: props.count,
|
||||
anchorElement: buttonEl.value,
|
||||
@@ -275,6 +281,21 @@ if (!mock) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.sp {
|
||||
background: linear-gradient(319deg, #B67B03 0%, #DAAF08 45%, #FEE9A0 70%, #DAAF08 85%, #B67B03 90% 100%) !important;
|
||||
border: solid 1px #B67B03 !important;
|
||||
box-shadow: 1px 1px 2px #fff inset !important;
|
||||
|
||||
> .count {
|
||||
color: #fff !important;
|
||||
text-shadow: 0 0 3px #000 !important;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
filter: drop-shadow(0 0 3px #fff) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.canToggle {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
|
||||
@@ -317,10 +338,6 @@ if (!mock) {
|
||||
> .count {
|
||||
color: var(--MI_THEME-accent);
|
||||
}
|
||||
|
||||
> .icon {
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
*/
|
||||
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { pleaseLogin } from '@/utility/please-login.js';
|
||||
import type { Ref } from 'vue';
|
||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||
import type { DI as DIType } from '@/di.js';
|
||||
import type { ExtractInjectedType } from '@/types/misc.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { pleaseLogin } from '@/utility/please-login.js';
|
||||
import { checkWordMute } from '@/utility/check-word-mute.js';
|
||||
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
@@ -34,9 +37,6 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||
import MkReactionsViewerDetails from '@/components/MkReactionsViewer.details.vue';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
import type { DI as DIType } from '@/di.js';
|
||||
import type { ExtractInjectedType } from '@/types/misc.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
export interface UseNoteProps {
|
||||
note: Misskey.entities.Note;
|
||||
@@ -50,7 +50,7 @@ export interface UseNoteElements {
|
||||
menuButton?: Ref<HTMLElement | null>;
|
||||
renoteButton?: Ref<HTMLElement | null>;
|
||||
renoteTime?: Ref<HTMLElement | null>;
|
||||
reactButton?: Ref<HTMLElement | null>;
|
||||
reactButton: Ref<HTMLElement>;
|
||||
clipButton?: Ref<HTMLElement | null>;
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ export function useNote(
|
||||
}).then(() => {
|
||||
noteEvents.emit(`reacted:${appearNote.id}`, { userId: $i!.id, reaction: '❤️' });
|
||||
});
|
||||
if (els.reactButton != null && els.reactButton.value != null && prefer.s.animation) {
|
||||
if (prefer.s.animation) {
|
||||
const rect = els.reactButton.value.getBoundingClientRect();
|
||||
const { dispose } = os.popup(MkRippleEffect, {
|
||||
x: rect.left + (els.reactButton.value.offsetWidth / 2),
|
||||
@@ -266,7 +266,7 @@ export function useNote(
|
||||
}
|
||||
} else {
|
||||
blur();
|
||||
reactionPicker.show(els.reactButton?.value ?? null, rawNote, async (reaction) => {
|
||||
reactionPicker.show(els.reactButton.value, rawNote, async (reaction) => {
|
||||
if (prefer.s.confirmOnReact) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
|
||||
@@ -4,400 +4,409 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps_s">
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.rateLimitFactor, 'rateLimitFactor'])" v-model:policyMeta="policyMetaModel.rateLimitFactor" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
|
||||
<template #valueText>{{ Math.floor(valuesModel.rateLimitFactor * 100) }}%</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkRange v-model="valuesModel.rateLimitFactor" :disabled="disabled" :min="0.3" :max="3" :step="0.1" :textConverter="(v) => `${Math.round(v * 100)}%`">
|
||||
<template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
|
||||
</MkRange>
|
||||
</template>
|
||||
</XFolder>
|
||||
<div class="_gaps_s">
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.rateLimitFactor, 'rateLimitFactor'])" v-model:policyMeta="policyMetaModel.rateLimitFactor" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
|
||||
<template #valueText>{{ Math.floor(valuesModel.rateLimitFactor * 100) }}%</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkRange v-model="valuesModel.rateLimitFactor" :disabled="disabled" :min="0.3" :max="3" :step="0.1" :textConverter="(v) => `${Math.round(v * 100)}%`">
|
||||
<template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
|
||||
</MkRange>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])" v-model:policyMeta="policyMetaModel.gtlAvailable" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
|
||||
<template #valueText>{{ valuesModel.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.gtlAvailable" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])" v-model:policyMeta="policyMetaModel.gtlAvailable" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
|
||||
<template #valueText>{{ valuesModel.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.gtlAvailable" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.ltlAvailable, 'ltlAvailable'])" v-model:policyMeta="policyMetaModel.ltlAvailable" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
|
||||
<template #valueText>{{ valuesModel.ltlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.ltlAvailable" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.ltlAvailable, 'ltlAvailable'])" v-model:policyMeta="policyMetaModel.ltlAvailable" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
|
||||
<template #valueText>{{ valuesModel.ltlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.ltlAvailable" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canPublicNote, 'canPublicNote'])" v-model:policyMeta="policyMetaModel.canPublicNote" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canPublicNote }}</template>
|
||||
<template #valueText>{{ valuesModel.canPublicNote ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canPublicNote" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canPublicNote, 'canPublicNote'])" v-model:policyMeta="policyMetaModel.canPublicNote" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canPublicNote }}</template>
|
||||
<template #valueText>{{ valuesModel.canPublicNote ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canPublicNote" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.chatAvailability, 'chatAvailability'])" v-model:policyMeta="policyMetaModel.chatAvailability" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.chatAvailability }}</template>
|
||||
<template #valueText>{{ valuesModel.chatAvailability === 'available' ? i18n.ts.yes : valuesModel.chatAvailability === 'readonly' ? i18n.ts.readonly : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSelect
|
||||
v-model="valuesModel.chatAvailability"
|
||||
:disabled="disabled"
|
||||
:items="[
|
||||
{ label: i18n.ts.enabled, value: 'available' },
|
||||
{ label: i18n.ts.readonly, value: 'readonly' },
|
||||
{ label: i18n.ts.disabled, value: 'unavailable' },
|
||||
]"
|
||||
>
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSelect>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.chatAvailability, 'chatAvailability'])" v-model:policyMeta="policyMetaModel.chatAvailability" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.chatAvailability }}</template>
|
||||
<template #valueText>{{ valuesModel.chatAvailability === 'available' ? i18n.ts.yes : valuesModel.chatAvailability === 'readonly' ? i18n.ts.readonly : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSelect
|
||||
v-model="valuesModel.chatAvailability"
|
||||
:disabled="disabled"
|
||||
:items="[
|
||||
{ label: i18n.ts.enabled, value: 'available' },
|
||||
{ label: i18n.ts.readonly, value: 'readonly' },
|
||||
{ label: i18n.ts.disabled, value: 'unavailable' },
|
||||
]"
|
||||
>
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSelect>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])" v-model:policyMeta="policyMetaModel.mentionLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||
<template #valueText>{{ valuesModel.mentionLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.mentionLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])" v-model:policyMeta="policyMetaModel.mentionLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||
<template #valueText>{{ valuesModel.mentionLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.mentionLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])" v-model:policyMeta="policyMetaModel.canInvite" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #valueText>{{ valuesModel.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canInvite" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])" v-model:policyMeta="policyMetaModel.canInvite" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #valueText>{{ valuesModel.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canInvite" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])" v-model:policyMeta="policyMetaModel.inviteLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
|
||||
<template #valueText>{{ valuesModel.inviteLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.inviteLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])" v-model:policyMeta="policyMetaModel.inviteLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
|
||||
<template #valueText>{{ valuesModel.inviteLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.inviteLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])" v-model:policyMeta="policyMetaModel.inviteLimitCycle" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template>
|
||||
<template #valueText>{{ valuesModel.inviteLimitCycle + i18n.ts._time.minute }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.inviteLimitCycle" type="number" :disabled="disabled">
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])" v-model:policyMeta="policyMetaModel.inviteLimitCycle" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template>
|
||||
<template #valueText>{{ valuesModel.inviteLimitCycle + i18n.ts._time.minute }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.inviteLimitCycle" type="number" :disabled="disabled">
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])" v-model:policyMeta="policyMetaModel.inviteExpirationTime" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template>
|
||||
<template #valueText>{{ valuesModel.inviteExpirationTime + i18n.ts._time.minute }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.inviteExpirationTime" type="number" :disabled="disabled">
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])" v-model:policyMeta="policyMetaModel.inviteExpirationTime" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template>
|
||||
<template #valueText>{{ valuesModel.inviteExpirationTime + i18n.ts._time.minute }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.inviteExpirationTime" type="number" :disabled="disabled">
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canManageAvatarDecorations, 'canManageAvatarDecorations'])" v-model:policyMeta="policyMetaModel.canManageAvatarDecorations" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template>
|
||||
<template #valueText>{{ valuesModel.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canManageAvatarDecorations" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canManageAvatarDecorations, 'canManageAvatarDecorations'])" v-model:policyMeta="policyMetaModel.canManageAvatarDecorations" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template>
|
||||
<template #valueText>{{ valuesModel.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canManageAvatarDecorations" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])" v-model:policyMeta="policyMetaModel.canManageCustomEmojis" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #valueText>{{ valuesModel.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canManageCustomEmojis" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])" v-model:policyMeta="policyMetaModel.canManageCustomEmojis" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #valueText>{{ valuesModel.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canManageCustomEmojis" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])" v-model:policyMeta="policyMetaModel.canSearchNotes" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
||||
<template #valueText>{{ valuesModel.canSearchNotes ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canSearchNotes" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])" v-model:policyMeta="policyMetaModel.canSearchNotes" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
||||
<template #valueText>{{ valuesModel.canSearchNotes ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canSearchNotes" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canSearchUsers, 'canSearchUsers'])" v-model:policyMeta="policyMetaModel.canSearchUsers" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canSearchUsers }}</template>
|
||||
<template #valueText>{{ valuesModel.canSearchUsers ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canSearchUsers" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canSearchUsers, 'canSearchUsers'])" v-model:policyMeta="policyMetaModel.canSearchUsers" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canSearchUsers }}</template>
|
||||
<template #valueText>{{ valuesModel.canSearchUsers ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canSearchUsers" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canUseTranslator'])" v-model:policyMeta="policyMetaModel.canUseTranslator" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canUseTranslator }}</template>
|
||||
<template #valueText>{{ valuesModel.canUseTranslator ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canUseTranslator" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canUseTranslator'])" v-model:policyMeta="policyMetaModel.canUseTranslator" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canUseTranslator }}</template>
|
||||
<template #valueText>{{ valuesModel.canUseTranslator ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canUseTranslator" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canCreateChannel, 'canCreateChannel'])" v-model:policyMeta="policyMetaModel.canCreateChannel" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canCreateChannel }}</template>
|
||||
<template #valueText>{{ valuesModel.canCreateChannel ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canCreateChannel" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canCreateChannel, 'canCreateChannel'])" v-model:policyMeta="policyMetaModel.canCreateChannel" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canCreateChannel }}</template>
|
||||
<template #valueText>{{ valuesModel.canCreateChannel ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canCreateChannel" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])" v-model:policyMeta="policyMetaModel.driveCapacityMb" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #valueText>{{ valuesModel.driveCapacityMb }}MB</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.driveCapacityMb" type="number" :disabled="disabled">
|
||||
<template #suffix>MB</template>
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])" v-model:policyMeta="policyMetaModel.driveCapacityMb" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #valueText>{{ valuesModel.driveCapacityMb }}MB</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.driveCapacityMb" type="number" :disabled="disabled">
|
||||
<template #suffix>MB</template>
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.maxFileSize, 'maxFileSizeMb'])" v-model:policyMeta="policyMetaModel.maxFileSizeMb" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.maxFileSize }}</template>
|
||||
<template #valueText>{{ valuesModel.maxFileSizeMb }}MB</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.maxFileSizeMb" type="number" :disabled="disabled">
|
||||
<template #suffix>MB</template>
|
||||
<template #caption>
|
||||
<div>{{ i18n.tsx._role._options.maxFileSize_caption2({ max: `${Math.floor(instance.maxFileSize / (1024 * 1024))}MB` }) }}</div>
|
||||
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._role._options.maxFileSize_caption }}</div>
|
||||
</template>
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.maxFileSize, 'maxFileSizeMb'])" v-model:policyMeta="policyMetaModel.maxFileSizeMb" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.maxFileSize }}</template>
|
||||
<template #valueText>{{ valuesModel.maxFileSizeMb }}MB</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.maxFileSizeMb" type="number" :disabled="disabled">
|
||||
<template #suffix>MB</template>
|
||||
<template #caption>
|
||||
<div>{{ i18n.tsx._role._options.maxFileSize_caption2({ max: `${Math.floor(instance.maxFileSize / (1024 * 1024))}MB` }) }}</div>
|
||||
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._role._options.maxFileSize_caption }}</div>
|
||||
</template>
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.uploadableFileTypes, 'uploadableFileTypes'])" v-model:policyMeta="policyMetaModel.uploadableFileTypes" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.uploadableFileTypes }}</template>
|
||||
<template #valueText>...</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkTextarea :modelValue="valuesModel.uploadableFileTypes.join('\n')" :disabled="disabled" @update:modelValue="v => valuesModel.uploadableFileTypes = v.split('\n')">
|
||||
<template #caption>
|
||||
<div>{{ i18n.ts._role._options.uploadableFileTypes_caption }}</div>
|
||||
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.tsx._role._options.uploadableFileTypes_caption2({ x: 'application/octet-stream' }) }}</div>
|
||||
</template>
|
||||
</MkTextarea>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.uploadableFileTypes, 'uploadableFileTypes'])" v-model:policyMeta="policyMetaModel.uploadableFileTypes" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.uploadableFileTypes }}</template>
|
||||
<template #valueText>...</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkTextarea :modelValue="valuesModel.uploadableFileTypes.join('\n')" :disabled="disabled" @update:modelValue="v => valuesModel.uploadableFileTypes = v.split('\n')">
|
||||
<template #caption>
|
||||
<div>{{ i18n.ts._role._options.uploadableFileTypes_caption }}</div>
|
||||
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.tsx._role._options.uploadableFileTypes_caption2({ x: 'application/octet-stream' }) }}</div>
|
||||
</template>
|
||||
</MkTextarea>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])" v-model:policyMeta="policyMetaModel.alwaysMarkNsfw" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template>
|
||||
<template #valueText>{{ valuesModel.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.alwaysMarkNsfw" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])" v-model:policyMeta="policyMetaModel.alwaysMarkNsfw" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template>
|
||||
<template #valueText>{{ valuesModel.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.alwaysMarkNsfw" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])" v-model:policyMeta="policyMetaModel.canUpdateBioMedia" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template>
|
||||
<template #valueText>{{ valuesModel.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canUpdateBioMedia" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])" v-model:policyMeta="policyMetaModel.canUpdateBioMedia" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template>
|
||||
<template #valueText>{{ valuesModel.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canUpdateBioMedia" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])" v-model:policyMeta="policyMetaModel.pinLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
||||
<template #valueText>{{ valuesModel.pinLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.pinLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])" v-model:policyMeta="policyMetaModel.pinLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
||||
<template #valueText>{{ valuesModel.pinLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.pinLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])" v-model:policyMeta="policyMetaModel.antennaLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
|
||||
<template #valueText>{{ valuesModel.antennaLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.antennaLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])" v-model:policyMeta="policyMetaModel.antennaLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
|
||||
<template #valueText>{{ valuesModel.antennaLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.antennaLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.wordMuteMax, 'wordMuteLimit'])" v-model:policyMeta="policyMetaModel.wordMuteLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
|
||||
<template #valueText>{{ valuesModel.wordMuteLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.wordMuteLimit" type="number" :disabled="disabled">
|
||||
<template #suffix>chars</template>
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.wordMuteMax, 'wordMuteLimit'])" v-model:policyMeta="policyMetaModel.wordMuteLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
|
||||
<template #valueText>{{ valuesModel.wordMuteLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.wordMuteLimit" type="number" :disabled="disabled">
|
||||
<template #suffix>chars</template>
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.webhookMax, 'webhookLimit'])" v-model:policyMeta="policyMetaModel.webhookLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
|
||||
<template #valueText>{{ valuesModel.webhookLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.webhookLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.webhookMax, 'webhookLimit'])" v-model:policyMeta="policyMetaModel.webhookLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
|
||||
<template #valueText>{{ valuesModel.webhookLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.webhookLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.clipMax, 'clipLimit'])" v-model:policyMeta="policyMetaModel.clipLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.clipMax }}</template>
|
||||
<template #valueText>{{ valuesModel.clipLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.clipLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.clipMax, 'clipLimit'])" v-model:policyMeta="policyMetaModel.clipLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.clipMax }}</template>
|
||||
<template #valueText>{{ valuesModel.clipLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.clipLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.noteEachClipsMax, 'noteEachClipsLimit'])" v-model:policyMeta="policyMetaModel.noteEachClipsLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
|
||||
<template #valueText>{{ valuesModel.noteEachClipsLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.noteEachClipsLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.noteEachClipsMax, 'noteEachClipsLimit'])" v-model:policyMeta="policyMetaModel.noteEachClipsLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
|
||||
<template #valueText>{{ valuesModel.noteEachClipsLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.noteEachClipsLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.userListMax, 'userListLimit'])" v-model:policyMeta="policyMetaModel.userListLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.userListMax }}</template>
|
||||
<template #valueText>{{ valuesModel.userListLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.userListLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.userListMax, 'userListLimit'])" v-model:policyMeta="policyMetaModel.userListLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.userListMax }}</template>
|
||||
<template #valueText>{{ valuesModel.userListLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.userListLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.userEachUserListsMax, 'userEachUserListsLimit'])" v-model:policyMeta="policyMetaModel.userEachUserListsLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
|
||||
<template #valueText>{{ valuesModel.userEachUserListsLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.userEachUserListsLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.userEachUserListsMax, 'userEachUserListsLimit'])" v-model:policyMeta="policyMetaModel.userEachUserListsLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
|
||||
<template #valueText>{{ valuesModel.userEachUserListsLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.userEachUserListsLimit" type="number" :disabled="disabled">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canHideAds, 'canHideAds'])" v-model:policyMeta="policyMetaModel.canHideAds" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canHideAds }}</template>
|
||||
<template #valueText>{{ valuesModel.canHideAds ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canHideAds" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canHideAds, 'canHideAds'])" v-model:policyMeta="policyMetaModel.canHideAds" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canHideAds }}</template>
|
||||
<template #valueText>{{ valuesModel.canHideAds ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canHideAds" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])" v-model:policyMeta="policyMetaModel.avatarDecorationLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
|
||||
<template #valueText>{{ valuesModel.avatarDecorationLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="avatarDecorationLimit" type="number" :disabled="disabled" :min="0" :max="16" @update:modelValue="updateAvatarDecorationLimit">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])" v-model:policyMeta="policyMetaModel.avatarDecorationLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
|
||||
<template #valueText>{{ valuesModel.avatarDecorationLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="avatarDecorationLimit" type="number" :disabled="disabled" :min="0" :max="16" @update:modelValue="updateAvatarDecorationLimit">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canImportAntennas, 'canImportAntennas'])" v-model:policyMeta="policyMetaModel.canImportAntennas" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canImportAntennas }}</template>
|
||||
<template #valueText>{{ valuesModel.canImportAntennas ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canImportAntennas" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canImportAntennas, 'canImportAntennas'])" v-model:policyMeta="policyMetaModel.canImportAntennas" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canImportAntennas }}</template>
|
||||
<template #valueText>{{ valuesModel.canImportAntennas ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canImportAntennas" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canImportBlocking, 'canImportBlocking'])" v-model:policyMeta="policyMetaModel.canImportBlocking" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canImportBlocking }}</template>
|
||||
<template #valueText>{{ valuesModel.canImportBlocking ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canImportBlocking" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canImportBlocking, 'canImportBlocking'])" v-model:policyMeta="policyMetaModel.canImportBlocking" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canImportBlocking }}</template>
|
||||
<template #valueText>{{ valuesModel.canImportBlocking ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canImportBlocking" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canImportFollowing, 'canImportFollowing'])" v-model:policyMeta="policyMetaModel.canImportFollowing" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canImportFollowing }}</template>
|
||||
<template #valueText>{{ valuesModel.canImportFollowing ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canImportFollowing" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canImportFollowing, 'canImportFollowing'])" v-model:policyMeta="policyMetaModel.canImportFollowing" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canImportFollowing }}</template>
|
||||
<template #valueText>{{ valuesModel.canImportFollowing ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canImportFollowing" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canImportMuting, 'canImportMuting'])" v-model:policyMeta="policyMetaModel.canImportMuting" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canImportMuting }}</template>
|
||||
<template #valueText>{{ valuesModel.canImportMuting ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canImportMuting" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canImportMuting, 'canImportMuting'])" v-model:policyMeta="policyMetaModel.canImportMuting" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canImportMuting }}</template>
|
||||
<template #valueText>{{ valuesModel.canImportMuting ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canImportMuting" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canImportUserLists, 'canImportUserList'])" v-model:policyMeta="policyMetaModel.canImportUserLists" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canImportUserLists }}</template>
|
||||
<template #valueText>{{ valuesModel.canImportUserLists ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canImportUserLists" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.canImportUserLists, 'canImportUserList'])" v-model:policyMeta="policyMetaModel.canImportUserLists" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.canImportUserLists }}</template>
|
||||
<template #valueText>{{ valuesModel.canImportUserLists ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.canImportUserLists" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.noteDraftLimit, 'noteDraftLimit'])" v-model:policyMeta="policyMetaModel.noteDraftLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.noteDraftLimit }}</template>
|
||||
<template #valueText>{{ valuesModel.noteDraftLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.noteDraftLimit" type="number" :disabled="disabled" :min="0">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.noteDraftLimit, 'noteDraftLimit'])" v-model:policyMeta="policyMetaModel.noteDraftLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.noteDraftLimit }}</template>
|
||||
<template #valueText>{{ valuesModel.noteDraftLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.noteDraftLimit" type="number" :disabled="disabled" :min="0">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.scheduledNoteLimit, 'scheduledNoteLimit'])" v-model:policyMeta="policyMetaModel.scheduledNoteLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.scheduledNoteLimit }}</template>
|
||||
<template #valueText>{{ valuesModel.scheduledNoteLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.scheduledNoteLimit" type="number" :disabled="disabled" :min="0">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.scheduledNoteLimit, 'scheduledNoteLimit'])" v-model:policyMeta="policyMetaModel.scheduledNoteLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.scheduledNoteLimit }}</template>
|
||||
<template #valueText>{{ valuesModel.scheduledNoteLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.scheduledNoteLimit" type="number" :disabled="disabled" :min="0">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.watermarkAvailable, 'watermarkAvailable'])" v-model:policyMeta="policyMetaModel.watermarkAvailable" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.watermarkAvailable }}</template>
|
||||
<template #valueText>{{ valuesModel.watermarkAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.watermarkAvailable" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
</div>
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.watermarkAvailable, 'watermarkAvailable'])" v-model:policyMeta="policyMetaModel.watermarkAvailable" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.watermarkAvailable }}</template>
|
||||
<template #valueText>{{ valuesModel.watermarkAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkSwitch v-model="valuesModel.watermarkAvailable" :disabled="disabled">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</XFolder>
|
||||
|
||||
<XFolder v-if="matchQuery([i18n.ts._role._options.spReactionsMonthlyLimit, 'spReactionsMonthlyLimit'])" v-model:policyMeta="policyMetaModel.spReactionsMonthlyLimit" :isBaseRole="isBaseRole" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role._options.spReactionsMonthlyLimit }}</template>
|
||||
<template #valueText>{{ valuesModel.spReactionsMonthlyLimit }}</template>
|
||||
<template #default="{ disabled }">
|
||||
<MkInput v-model="valuesModel.spReactionsMonthlyLimit" type="number" :disabled="disabled" :min="0">
|
||||
</MkInput>
|
||||
</template>
|
||||
</XFolder>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -96,6 +96,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkFolder>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker v-slot="slotProps" :keywords="['features']">
|
||||
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
|
||||
<template #icon><SearchIcon><i class="ti ti-puzzle"></i></SearchIcon></template>
|
||||
<template #label><SearchLabel>{{ i18n.ts._serverSettings.features }}</SearchLabel></template>
|
||||
<template v-if="featuresForm.modified.value" #footer>
|
||||
<MkFormFooter :form="featuresForm"/>
|
||||
</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<SearchMarker>
|
||||
<MkSwitch v-model="featuresForm.state.enableSpReaction">
|
||||
<template #label><SearchLabel>{{ i18n.ts._serverSettings._spReactions.enable }}</SearchLabel><span v-if="featuresForm.modifiedStates.enableSpReaction" class="_modified">{{ i18n.ts.modified }}</span></template>
|
||||
<template #caption>
|
||||
<SearchText>{{ i18n.ts._serverSettings._spReactions.description1 }}</SearchText>
|
||||
<div>{{ i18n.ts._serverSettings._spReactions.description2 }}</div>
|
||||
</template>
|
||||
</MkSwitch>
|
||||
</SearchMarker>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker v-slot="slotProps" :keywords="['pinned', 'users']">
|
||||
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
|
||||
<template #icon><SearchIcon><i class="ti ti-user-star"></i></SearchIcon></template>
|
||||
@@ -433,6 +455,15 @@ const infoForm = useForm({
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
||||
const featuresForm = useForm({
|
||||
enableSpReaction: meta.enableSpReaction,
|
||||
}, async (state) => {
|
||||
await os.apiWithDialog('admin/update-meta', {
|
||||
enableSpReaction: state.enableSpReaction,
|
||||
});
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
||||
const pinnedUsersForm = useForm({
|
||||
pinnedUsers: meta.pinnedUsers.join('\n'),
|
||||
}, async (state) => {
|
||||
|
||||
@@ -25,6 +25,8 @@ import { genEmbedCode } from '@/utility/get-embed-code.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { getPluginHandlers } from '@/plugin.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { reactionPicker } from '@/utility/reaction-picker.js';
|
||||
import { noteEvents } from '@/composables/use-note-capture.js';
|
||||
|
||||
const isInBrowserTranslationAvailable = (
|
||||
'LanguageDetector' in window &&
|
||||
@@ -238,6 +240,17 @@ export function getNoteMenu(props: {
|
||||
});
|
||||
}
|
||||
|
||||
function spReaction(ev: PointerEvent): void {
|
||||
reactionPicker.show((ev.currentTarget ?? ev.target) as HTMLElement, appearNote, async (reaction) => {
|
||||
misskeyApi('notes/sp-reactions/create', {
|
||||
noteId: appearNote.id,
|
||||
reaction: reaction,
|
||||
}).then(() => {
|
||||
noteEvents.emit(`sp-reacted:${appearNote.id}`, { userId: $i!.id, reaction: reaction });
|
||||
});
|
||||
}, () => { focus(); });
|
||||
}
|
||||
|
||||
function toggleThreadMute(mute: boolean): void {
|
||||
os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
|
||||
noteId: appearNote.id,
|
||||
@@ -429,6 +442,12 @@ export function getNoteMenu(props: {
|
||||
children: () => getNoteClipMenu(props),
|
||||
});
|
||||
|
||||
menuItems.push({
|
||||
icon: 'ti ti-sparkle-highlight',
|
||||
text: i18n.ts.spReaction,
|
||||
action: (ev) => spReaction(ev),
|
||||
});
|
||||
|
||||
menuItems.push(statePromise.then(state => state.isMutedThread ? {
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts.unmuteThread,
|
||||
|
||||
@@ -25,7 +25,7 @@ class ReactionPicker {
|
||||
}
|
||||
|
||||
public show(
|
||||
anchorElement: HTMLElement | null,
|
||||
anchorElement: HTMLElement,
|
||||
targetNote: Misskey.entities.Note | null,
|
||||
onChosen?: (reaction: string) => void,
|
||||
onClosed?: () => void,
|
||||
|
||||
@@ -5703,6 +5703,14 @@ export interface Locale extends ILocale {
|
||||
* ピクセルアート拡大モード
|
||||
*/
|
||||
"pixelatedZoom": string;
|
||||
/**
|
||||
* スペシャルリアクション
|
||||
*/
|
||||
"spReaction": string;
|
||||
/**
|
||||
* スペシャルリアクションの残り使用可能回数がありません
|
||||
*/
|
||||
"noRemainingUsesForSpReaction": string;
|
||||
"_imageEditing": {
|
||||
"_vars": {
|
||||
/**
|
||||
@@ -7028,6 +7036,24 @@ export interface Locale extends ILocale {
|
||||
* アクティビティを表示する
|
||||
*/
|
||||
"showActivitiesForVisitor": string;
|
||||
/**
|
||||
* 機能
|
||||
*/
|
||||
"features": string;
|
||||
"_spReactions": {
|
||||
/**
|
||||
* スペシャルリアクションを有効にする
|
||||
*/
|
||||
"enable": string;
|
||||
/**
|
||||
* 通常のリアクションより目立つ「スペシャルリアクション」をノートに送れる機能です。
|
||||
*/
|
||||
"description1": string;
|
||||
/**
|
||||
* 有効にする場合、ロールポリシーで、毎月送ることのできる最大数を設定してください。
|
||||
*/
|
||||
"description2": string;
|
||||
};
|
||||
"_userGeneratedContentsVisibilityForVisitor": {
|
||||
/**
|
||||
* 全て公開
|
||||
@@ -8289,6 +8315,10 @@ export interface Locale extends ILocale {
|
||||
* ウォーターマーク機能の使用可否
|
||||
*/
|
||||
"watermarkAvailable": string;
|
||||
/**
|
||||
* スペシャルリアクションの月間送信上限
|
||||
*/
|
||||
"spReactionsMonthlyLimit": string;
|
||||
};
|
||||
"_condition": {
|
||||
/**
|
||||
@@ -11033,6 +11063,10 @@ export interface Locale extends ILocale {
|
||||
* リアクション
|
||||
*/
|
||||
"reaction": string;
|
||||
/**
|
||||
* スペシャルリアクション
|
||||
*/
|
||||
"spReaction": string;
|
||||
/**
|
||||
* アンケートが終了
|
||||
*/
|
||||
|
||||
@@ -3141,7 +3141,7 @@ type Notification_2 = components['schemas']['Notification'];
|
||||
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollEnded", "scheduledNotePosted", "scheduledNotePostFailed", "receiveFollowRequest", "followRequestAccepted", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned", "exportCompleted", "test", "login", "createToken"];
|
||||
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "spReaction", "pollEnded", "scheduledNotePosted", "scheduledNotePostFailed", "receiveFollowRequest", "followRequestAccepted", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned", "exportCompleted", "test", "login", "createToken"];
|
||||
|
||||
// @public (undocumented)
|
||||
export function nyaize(text: string): string;
|
||||
@@ -3356,7 +3356,7 @@ type RoleLite = components['schemas']['RoleLite'];
|
||||
type RolePolicies = components['schemas']['RolePolicies'];
|
||||
|
||||
// @public (undocumented)
|
||||
export const rolePolicies: readonly ["gtlAvailable", "ltlAvailable", "canPublicNote", "mentionLimit", "canInvite", "inviteLimit", "inviteLimitCycle", "inviteExpirationTime", "canManageCustomEmojis", "canManageAvatarDecorations", "canSearchNotes", "canSearchUsers", "canUseTranslator", "canHideAds", "canCreateChannel", "driveCapacityMb", "maxFileSizeMb", "alwaysMarkNsfw", "canUpdateBioMedia", "pinLimit", "antennaLimit", "wordMuteLimit", "webhookLimit", "clipLimit", "noteEachClipsLimit", "userListLimit", "userEachUserListsLimit", "rateLimitFactor", "avatarDecorationLimit", "canImportAntennas", "canImportBlocking", "canImportFollowing", "canImportMuting", "canImportUserLists", "chatAvailability", "uploadableFileTypes", "noteDraftLimit", "scheduledNoteLimit", "watermarkAvailable"];
|
||||
export const rolePolicies: readonly ["gtlAvailable", "ltlAvailable", "canPublicNote", "mentionLimit", "canInvite", "inviteLimit", "inviteLimitCycle", "inviteExpirationTime", "canManageCustomEmojis", "canManageAvatarDecorations", "canSearchNotes", "canSearchUsers", "canUseTranslator", "canHideAds", "canCreateChannel", "driveCapacityMb", "maxFileSizeMb", "alwaysMarkNsfw", "canUpdateBioMedia", "pinLimit", "antennaLimit", "wordMuteLimit", "webhookLimit", "clipLimit", "noteEachClipsLimit", "userListLimit", "userEachUserListsLimit", "rateLimitFactor", "avatarDecorationLimit", "canImportAntennas", "canImportBlocking", "canImportFollowing", "canImportMuting", "canImportUserLists", "chatAvailability", "uploadableFileTypes", "noteDraftLimit", "scheduledNoteLimit", "watermarkAvailable", "spReactionsMonthlyLimit"];
|
||||
|
||||
// @public (undocumented)
|
||||
type RolesListResponse = operations['roles___list']['responses']['200']['content']['application/json'];
|
||||
|
||||
@@ -4231,6 +4231,15 @@ export type components = {
|
||||
/** Format: misskey:id */
|
||||
userListId: string;
|
||||
};
|
||||
spReaction?: {
|
||||
/** @enum {string} */
|
||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||
} | {
|
||||
/** @enum {string} */
|
||||
type: 'list';
|
||||
/** Format: misskey:id */
|
||||
userListId: string;
|
||||
};
|
||||
pollEnded?: {
|
||||
/** @enum {string} */
|
||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||
@@ -4667,6 +4676,18 @@ export type components = {
|
||||
userId: string;
|
||||
note: components['schemas']['Note'];
|
||||
reaction: string;
|
||||
} | {
|
||||
/** Format: id */
|
||||
id: string;
|
||||
/** Format: date-time */
|
||||
createdAt: string;
|
||||
/** @enum {string} */
|
||||
type: 'spReaction';
|
||||
user: components['schemas']['UserLite'];
|
||||
/** Format: id */
|
||||
userId: string;
|
||||
note: components['schemas']['Note'];
|
||||
reaction: string;
|
||||
} | {
|
||||
/** Format: id */
|
||||
id: string;
|
||||
@@ -9575,6 +9596,7 @@ export interface operations {
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: number;
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes: number;
|
||||
showRoleBadgesOfRemoteUsers: boolean;
|
||||
enableSpReaction: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -13095,6 +13117,7 @@ export interface operations {
|
||||
remoteNotesCleaningExpiryDaysForEachNotes?: number;
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes?: number;
|
||||
showRoleBadgesOfRemoteUsers?: boolean;
|
||||
enableSpReaction?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -26616,8 +26639,8 @@ export interface operations {
|
||||
untilDate?: number;
|
||||
/** @default true */
|
||||
markAsRead?: boolean;
|
||||
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'spReaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'spReaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -26701,8 +26724,8 @@ export interface operations {
|
||||
untilDate?: number;
|
||||
/** @default true */
|
||||
markAsRead?: boolean;
|
||||
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'spReaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'spReaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -27967,6 +27990,15 @@ export interface operations {
|
||||
/** Format: misskey:id */
|
||||
userListId: string;
|
||||
};
|
||||
spReaction?: {
|
||||
/** @enum {string} */
|
||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||
} | {
|
||||
/** @enum {string} */
|
||||
type: 'list';
|
||||
/** Format: misskey:id */
|
||||
userListId: string;
|
||||
};
|
||||
pollEnded?: {
|
||||
/** @enum {string} */
|
||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||
|
||||
@@ -24,6 +24,7 @@ export const notificationTypes = [
|
||||
'renote',
|
||||
'quote',
|
||||
'reaction',
|
||||
'spReaction',
|
||||
'pollEnded',
|
||||
'scheduledNotePosted',
|
||||
'scheduledNotePostFailed',
|
||||
@@ -232,6 +233,7 @@ export const rolePolicies = [
|
||||
'noteDraftLimit',
|
||||
'scheduledNoteLimit',
|
||||
'watermarkAvailable',
|
||||
'spReactionsMonthlyLimit',
|
||||
] as const;
|
||||
|
||||
export const queueTypes = [
|
||||
|
||||
Generated
+1457
-420
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,7 @@ allowBuilds:
|
||||
vue-demi: true
|
||||
exifreader: false
|
||||
protobufjs: false
|
||||
ttf2woff2: false
|
||||
minimumReleaseAge: 10080 # delay 7days to mitigate supply-chain attack
|
||||
minimumReleaseAgeExclude:
|
||||
- '@misskey-dev/*'
|
||||
|
||||
Reference in New Issue
Block a user