mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-24 06:54:45 +02:00
Compare commits
5 Commits
feature/ro
...
2026.7.0-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f54416f63 | ||
|
|
a63c8ef939 | ||
|
|
015868f17f | ||
|
|
dc3fd42d63 | ||
|
|
34a4b9b74c |
@@ -20,9 +20,15 @@ reviews:
|
||||
instructions: |
|
||||
【レビュー対象外】
|
||||
- フォーマット違反、型エラー、SPDXヘッダーの記載漏れ等、静的解析で検出可能な問題については、別Workflow(CI/CD)でチェックされるため、レビュー対象外として無視してください。
|
||||
|
||||
【特別な対応を要するレビュー】
|
||||
- プルリクエストで変更されていない部分において、プルリクエストのタイトルや説明文に照らしてスコープ外であるが修正すべき点を見つけた場合は、このプルリクエストで修正させるのではなく、別のプルリクエストを開いてそれを修正するように指示してください。\
|
||||
なお、当該プルリクエストで行が追加/削除されたことによりスコープ外の問題が起こりうる場合は、例外的にこのプルリクエストで修正させてください。
|
||||
|
||||
【注力してほしい観点】
|
||||
- ビジネスロジックの不備、セキュリティ、パフォーマンス、設計パターンの適切性などに焦点を当ててレビューしてください。
|
||||
- プルリクエストのタイトルや説明文に照らしてスコープ外の変更が含まれていないかをレビューしてください。なお、明らかにスコープ外であると確信できるもの(例: 全く関係ないファイルを編集している等)のみを対象とし、スコープ外かどうかの判断に困る場合は指摘しないでください。\
|
||||
スコープ外の変更が含まれていた場合は指摘し、その部分について、このプルリクエストに含めずに別のプルリクエストを開いて変更するように指示してください。
|
||||
path_filters:
|
||||
- "!CHANGELOG.md"
|
||||
- "!**/__snapshots__/**"
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
- バックエンドで画像処理に用いているライブラリ sharp のシステム要件の変更により、**SSE4.2 命令セットをサポートしていない x86_64 CPU では Misskey が正しく動作しなくなります**。仮想マシンに Misskey をデプロイしている場合や、古いハードウェアをお使いの場合は、アップデート前にお使いの環境をご確認ください。なお、ARM64 など x86_64 ではない環境においてはこの変更による影響はありません。
|
||||
|
||||
### General
|
||||
- Feat: 自分に表示する公開ロールとロールバッジをユーザー側で選べるように
|
||||
- 必ず表示する必要があるロールを作成することもできます(既存のロールについてはすべて表示必須設定となります。必要に応じて設定を変更してください)
|
||||
- 今後新規作成するロールのデフォルト値は表示必須設定なし(ユーザーが表示・非表示を選べる)となります。
|
||||
- Feat: コントロールパネルから二要素認証を解除できるように
|
||||
- Feat: 条件に一致したURLプレビューのサムネイルを隠すことができるように
|
||||
(Based on https://github.com/MisskeyIO/misskey/pull/214)
|
||||
|
||||
@@ -2083,8 +2083,6 @@ _role:
|
||||
isConditionalRole: "これはコンディショナルロールです。"
|
||||
isPublic: "公開ロール"
|
||||
descriptionOfIsPublic: "ユーザーのプロフィールでこのロールが表示されます。"
|
||||
isPublicDisplayRequired: "非表示を許可しない(常に表示)"
|
||||
descriptionOfIsPublicDisplayRequired: "有効にすると、ユーザーはこの公開ロール/ロールバッジを非表示にできません。"
|
||||
options: "オプション"
|
||||
policies: "ポリシー"
|
||||
baseRole: "ベースロール"
|
||||
@@ -3247,13 +3245,6 @@ _gridComponent:
|
||||
patternNotMatch: "この値は{pattern}のパターンに一致しません"
|
||||
notUnique: "この値は一意である必要があります"
|
||||
|
||||
_roleDisplay:
|
||||
title: "ロールの表示/非表示"
|
||||
description: "自分に割り当てられているロールを確認したり、プロフィールやノート上で表示・公開するロールを選択したりできます。"
|
||||
roleExplorableAlert: "このロールは、管理者により、{link}への表示とロールタイムラインの有効化が設定されています。プロフィール上で非表示にすることはできますが、あなたにこのロールが付与されていることが知られる可能性があります。"
|
||||
displayToggle: "ロール/ロールバッジを表示する"
|
||||
alwaysShownByAdmin: "管理者の設定により非表示にすることはできません。"
|
||||
|
||||
_roleSelectDialog:
|
||||
notSelected: "選択されていません"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2026.7.0-beta.3",
|
||||
"version": "2026.7.0-beta.4",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RoleDisplayVisibility1783059479536 {
|
||||
name = 'RoleDisplayVisibility1783059479536';
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "hiddenRoleIds" character varying(32) array NOT NULL DEFAULT '{}'`);
|
||||
await queryRunner.query(`ALTER TABLE "role" ADD "isPublicDisplayRequired" boolean NOT NULL DEFAULT false`);
|
||||
|
||||
// 既存のロールについてはすべて強制表示とする(新規作成分についてはfalse)
|
||||
await queryRunner.query(`UPDATE "role" SET "isPublicDisplayRequired" = true`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "isPublicDisplayRequired"`);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "hiddenRoleIds"`);
|
||||
}
|
||||
}
|
||||
@@ -676,7 +676,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
target: values.target,
|
||||
condFormula: values.condFormula,
|
||||
isPublic: values.isPublic,
|
||||
isPublicDisplayRequired: values.isPublicDisplayRequired ?? false,
|
||||
isAdministrator: values.isAdministrator,
|
||||
isModerator: values.isModerator,
|
||||
isExplorable: values.isExplorable,
|
||||
|
||||
@@ -55,7 +55,6 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||
makeNotesHiddenBefore: null,
|
||||
chatScope: 'mutual',
|
||||
emojis: [],
|
||||
hiddenRoleIds: [],
|
||||
score: 0,
|
||||
host: null,
|
||||
inbox: null,
|
||||
|
||||
@@ -64,7 +64,6 @@ export class RoleEntityService {
|
||||
target: role.target,
|
||||
condFormula: role.condFormula,
|
||||
isPublic: role.isPublic,
|
||||
isPublicDisplayRequired: role.isPublicDisplayRequired,
|
||||
isAdministrator: role.isAdministrator,
|
||||
isModerator: role.isModerator,
|
||||
isExplorable: role.isExplorable,
|
||||
@@ -84,32 +83,5 @@ export class RoleEntityService {
|
||||
) {
|
||||
return Promise.all(roles.map(x => this.pack(x, me)));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiRole['id'] | MiRole,
|
||||
): Promise<Packed<'RoleLite'>> {
|
||||
const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
color: role.color,
|
||||
iconUrl: role.iconUrl,
|
||||
description: role.description,
|
||||
isModerator: role.isModerator,
|
||||
isAdministrator: role.isAdministrator,
|
||||
asBadge: role.asBadge,
|
||||
isPublicDisplayRequired: role.isPublicDisplayRequired,
|
||||
isExplorable: role.isExplorable,
|
||||
displayOrder: role.displayOrder,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packLiteMany(
|
||||
roles: (MiRole | MiRole['id'])[],
|
||||
) {
|
||||
return Promise.all(roles.map(x => this.packLite(x)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { Promiseable } from '@/misc/prelude/await-all.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||
import type { MiRole } from '@/models/Role.js';
|
||||
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import {
|
||||
birthdaySchema,
|
||||
@@ -52,7 +51,6 @@ import { ChatService } from '@/core/ChatService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { NoteEntityService } from './NoteEntityService.js';
|
||||
import type { PageEntityService } from './PageEntityService.js';
|
||||
import type { RoleEntityService } from './RoleEntityService.js';
|
||||
import { toArray } from '@/misc/prelude/array.js';
|
||||
|
||||
const Ajv = _Ajv.default;
|
||||
@@ -90,7 +88,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
private apPersonService: ApPersonService;
|
||||
private noteEntityService: NoteEntityService;
|
||||
private pageEntityService: PageEntityService;
|
||||
private roleEntityService: RoleEntityService;
|
||||
private customEmojiService: CustomEmojiService;
|
||||
private announcementService: AnnouncementService;
|
||||
private roleService: RoleService;
|
||||
@@ -147,7 +144,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
this.apPersonService = this.moduleRef.get('ApPersonService');
|
||||
this.noteEntityService = this.moduleRef.get('NoteEntityService');
|
||||
this.pageEntityService = this.moduleRef.get('PageEntityService');
|
||||
this.roleEntityService = this.moduleRef.get('RoleEntityService');
|
||||
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
||||
this.announcementService = this.moduleRef.get('AnnouncementService');
|
||||
this.roleService = this.moduleRef.get('RoleService');
|
||||
@@ -406,30 +402,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
return `${this.config.url}/users/${userId}`;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private prepareRoles<T extends Pick<MiRole, 'id' | 'isPublic' | 'displayOrder' | 'isPublicDisplayRequired'>>(roles: T[], user: MiUser, iAmModerator: boolean, hideByUserPreference = true): T[] {
|
||||
const hiddenRoleIds = new Set(user.hiddenRoleIds);
|
||||
return roles.filter(role => (role.isPublic || iAmModerator) && (role.isPublicDisplayRequired || iAmModerator || !hideByUserPreference || !hiddenRoleIds.has(role.id))).sort((a, b) => b.displayOrder - a.displayOrder);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private sanitizeHiddenRoleIds(roles: Pick<MiRole, 'id' | 'isPublic' | 'isPublicDisplayRequired'>[], user: MiUser): MiRole['id'][] {
|
||||
const hideableRoleIds = new Set(roles
|
||||
.filter(role => role.isPublic && !role.isPublicDisplayRequired)
|
||||
.map(role => role.id));
|
||||
const sanitizedRoleIds: MiRole['id'][] = [];
|
||||
const seenRoleIds = new Set<MiRole['id']>();
|
||||
|
||||
for (const roleId of user.hiddenRoleIds) {
|
||||
if (!hideableRoleIds.has(roleId) || seenRoleIds.has(roleId)) continue;
|
||||
|
||||
sanitizedRoleIds.push(roleId);
|
||||
seenRoleIds.add(roleId);
|
||||
}
|
||||
|
||||
return sanitizedRoleIds;
|
||||
}
|
||||
|
||||
public async pack<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
|
||||
src: MiUser['id'] | MiUser,
|
||||
me?: { id: MiUser['id']; } | null | undefined,
|
||||
@@ -453,7 +425,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
const meId = me ? me.id : null;
|
||||
const isMe = meId === user.id;
|
||||
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
||||
const userRoles = isDetailed ? this.roleService.getUserRoles(user.id) : null;
|
||||
|
||||
const profile = isDetailed
|
||||
? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }))
|
||||
@@ -543,9 +514,10 @@ export class UserEntityService implements OnModuleInit {
|
||||
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||
onlineStatus: this.getOnlineStatus(user),
|
||||
// パフォーマンス上の理由で、明示的に設定しない場合はローカルユーザーのみ取得
|
||||
badgeRoles: (this.meta.showRoleBadgesOfRemoteUsers || user.host == null) ? this.roleService.getUserBadgeRoles(user.id).then((rs) => this.prepareRoles(rs, user, iAmModerator)
|
||||
badgeRoles: (this.meta.showRoleBadgesOfRemoteUsers || user.host == null) ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
|
||||
.filter((r) => r.isPublic || iAmModerator)
|
||||
.sort((a, b) => b.displayOrder - a.displayOrder)
|
||||
.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
iconUrl: r.iconUrl,
|
||||
displayOrder: r.displayOrder,
|
||||
@@ -588,7 +560,16 @@ export class UserEntityService implements OnModuleInit {
|
||||
followingVisibility: profile!.followingVisibility,
|
||||
chatScope: user.chatScope,
|
||||
canChat: this.roleService.getUserPolicies(user.id).then(r => r.chatAvailability === 'available'),
|
||||
roles: userRoles!.then(roles => this.roleEntityService.packLiteMany(this.prepareRoles(roles, user, iAmModerator, !isMe))),
|
||||
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
color: role.color,
|
||||
iconUrl: role.iconUrl,
|
||||
description: role.description,
|
||||
isModerator: role.isModerator,
|
||||
isAdministrator: role.isAdministrator,
|
||||
displayOrder: role.displayOrder,
|
||||
}))),
|
||||
memo: memo,
|
||||
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
|
||||
} : {}),
|
||||
@@ -617,7 +598,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
preventAiLearning: profile!.preventAiLearning,
|
||||
isExplorable: user.isExplorable,
|
||||
isDeleted: user.isDeleted,
|
||||
hiddenRoleIds: userRoles!.then(roles => this.sanitizeHiddenRoleIds(roles, user)),
|
||||
twoFactorBackupCodesStock: profile?.twoFactorBackupSecret?.length === 5 ? 'full' : (profile?.twoFactorBackupSecret?.length ?? 0) > 0 ? 'partial' : 'none',
|
||||
hideOnlineStatus: user.hideOnlineStatus,
|
||||
hasUnreadSpecifiedNotes: false, // 後方互換性のため
|
||||
|
||||
@@ -227,11 +227,6 @@ export class MiRole {
|
||||
})
|
||||
public isPublic: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isPublicDisplayRequired: boolean;
|
||||
|
||||
// trueの場合ユーザー名の横にバッジとして表示
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiDriveFile } from './DriveFile.js';
|
||||
import type { MiRole } from './Role.js';
|
||||
|
||||
@Entity('user')
|
||||
@Index(['usernameLower', 'host'], { unique: true })
|
||||
@@ -230,11 +229,6 @@ export class MiUser {
|
||||
})
|
||||
public emojis: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32, array: true, default: '{}',
|
||||
})
|
||||
public hiddenRoleIds: MiRole['id'][];
|
||||
|
||||
// チャットを許可する相手
|
||||
// everyone: 誰からでも
|
||||
// followers: フォロワーのみ
|
||||
|
||||
@@ -369,21 +369,6 @@ export const packedRoleLiteSchema = {
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
asBadge: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
isPublicDisplayRequired: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
isExplorable: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
displayOrder: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
@@ -427,11 +412,6 @@ export const packedRoleSchema = {
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
isPublicDisplayRequired: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
isExplorable: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
||||
@@ -176,11 +176,6 @@ export const packedUserLiteSchema = {
|
||||
type: 'object',
|
||||
nullable: false, optional: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
format: 'id',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
@@ -516,15 +511,6 @@ export const packedMeDetailedOnlySchema = {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
hiddenRoleIds: {
|
||||
type: 'array',
|
||||
nullable: false, optional: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
twoFactorBackupCodesStock: {
|
||||
type: 'string',
|
||||
enum: ['full', 'partial', 'none'],
|
||||
|
||||
@@ -32,7 +32,6 @@ export const paramDef = {
|
||||
target: { type: 'string', enum: ['manual', 'conditional'] },
|
||||
condFormula: { type: 'object' },
|
||||
isPublic: { type: 'boolean' },
|
||||
isPublicDisplayRequired: { type: 'boolean', default: false },
|
||||
isModerator: { type: 'boolean' },
|
||||
isAdministrator: { type: 'boolean' },
|
||||
isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility
|
||||
|
||||
@@ -37,7 +37,6 @@ export const paramDef = {
|
||||
target: { type: 'string', enum: ['manual', 'conditional'] },
|
||||
condFormula: { type: 'object' },
|
||||
isPublic: { type: 'boolean' },
|
||||
isPublicDisplayRequired: { type: 'boolean' },
|
||||
isModerator: { type: 'boolean' },
|
||||
isAdministrator: { type: 'boolean' },
|
||||
isExplorable: { type: 'boolean' },
|
||||
@@ -76,7 +75,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
target: ps.target,
|
||||
condFormula: ps.condFormula,
|
||||
isPublic: ps.isPublic,
|
||||
isPublicDisplayRequired: ps.isPublicDisplayRequired,
|
||||
isModerator: ps.isModerator,
|
||||
isAdministrator: ps.isAdministrator,
|
||||
isExplorable: ps.isExplorable,
|
||||
|
||||
@@ -191,12 +191,6 @@ export const paramDef = {
|
||||
followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||
followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||
chatScope: { type: 'string', enum: ['everyone', 'followers', 'following', 'mutual', 'none'] },
|
||||
hiddenRoleIds: {
|
||||
type: 'array',
|
||||
maxItems: 256,
|
||||
uniqueItems: true,
|
||||
items: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
mutedWords: muteWords,
|
||||
hardMutedWords: muteWords,
|
||||
@@ -299,20 +293,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
|
||||
if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
|
||||
if (ps.chatScope !== undefined) updates.chatScope = ps.chatScope;
|
||||
if (ps.hiddenRoleIds !== undefined) {
|
||||
const roles = await this.roleService.getUserRoles(user.id);
|
||||
const allowedRoleIds = new Set(roles.filter(role => role.isPublic && !role.isPublicDisplayRequired).map(role => role.id));
|
||||
const hiddenRoleIds: string[] = [];
|
||||
const seenRoleIds = new Set<string>();
|
||||
|
||||
for (const roleId of ps.hiddenRoleIds) {
|
||||
if (seenRoleIds.has(roleId) || !allowedRoleIds.has(roleId)) continue;
|
||||
seenRoleIds.add(roleId);
|
||||
hiddenRoleIds.push(roleId);
|
||||
}
|
||||
|
||||
updates.hiddenRoleIds = hiddenRoleIds;
|
||||
}
|
||||
|
||||
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
|
||||
const count = (arr: (string[] | string)[]) => {
|
||||
|
||||
@@ -128,7 +128,6 @@ describe('ユーザー', () => {
|
||||
preventAiLearning: user.preventAiLearning,
|
||||
isExplorable: user.isExplorable,
|
||||
isDeleted: user.isDeleted,
|
||||
hiddenRoleIds: user.hiddenRoleIds ?? [],
|
||||
twoFactorBackupCodesStock: user.twoFactorBackupCodesStock,
|
||||
hideOnlineStatus: user.hideOnlineStatus,
|
||||
hasUnreadSpecifiedNotes: user.hasUnreadSpecifiedNotes,
|
||||
@@ -661,16 +660,12 @@ describe('ユーザー', () => {
|
||||
description: rolePublic.description,
|
||||
isModerator: rolePublic.isModerator,
|
||||
isAdministrator: rolePublic.isAdministrator,
|
||||
asBadge: rolePublic.asBadge,
|
||||
isPublicDisplayRequired: rolePublic.isPublicDisplayRequired,
|
||||
isExplorable: rolePublic.isExplorable,
|
||||
displayOrder: rolePublic.displayOrder,
|
||||
}]);
|
||||
});
|
||||
test('を取得することができ、バッヂロールがセットされていること', async () => {
|
||||
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRoleBadge.id }, user: alice });
|
||||
assert.deepStrictEqual(response.badgeRoles, [{
|
||||
id: roleBadge.id,
|
||||
name: roleBadge.name,
|
||||
iconUrl: roleBadge.iconUrl,
|
||||
displayOrder: roleBadge.displayOrder,
|
||||
@@ -683,42 +678,9 @@ describe('ユーザー', () => {
|
||||
description: roleBadge.description,
|
||||
isModerator: roleBadge.isModerator,
|
||||
isAdministrator: roleBadge.isAdministrator,
|
||||
asBadge: roleBadge.asBadge,
|
||||
isPublicDisplayRequired: roleBadge.isPublicDisplayRequired,
|
||||
isExplorable: roleBadge.isExplorable,
|
||||
displayOrder: roleBadge.displayOrder,
|
||||
}]);
|
||||
});
|
||||
test('i/updateでhiddenRoleIdsが保存時に表示可能な割り当て済みロールだけへsanitizeされること', async () => {
|
||||
const user = await signup({ username: 'userHiddenRoles' });
|
||||
const visibleRole = await role(root, { isPublic: true, name: 'Hideable Role' });
|
||||
const secondVisibleRole = await role(root, { isPublic: true, name: 'Second Hideable Role' });
|
||||
const privateRole = await role(root, { isPublic: false, name: 'Private Role' });
|
||||
const forcedRole = await role(root, { isPublic: true, isPublicDisplayRequired: true, name: 'Forced Role' });
|
||||
const unassignedRole = await role(root, { isPublic: true, name: 'Unassigned Role' });
|
||||
|
||||
await api('admin/roles/assign', { userId: user.id, roleId: visibleRole.id }, root);
|
||||
await api('admin/roles/assign', { userId: user.id, roleId: secondVisibleRole.id }, root);
|
||||
await api('admin/roles/assign', { userId: user.id, roleId: privateRole.id }, root);
|
||||
await api('admin/roles/assign', { userId: user.id, roleId: forcedRole.id }, root);
|
||||
|
||||
const response = await successfulApiCall({
|
||||
endpoint: 'i/update',
|
||||
parameters: {
|
||||
hiddenRoleIds: [
|
||||
secondVisibleRole.id,
|
||||
'unknownroleid',
|
||||
unassignedRole.id,
|
||||
privateRole.id,
|
||||
forcedRole.id,
|
||||
visibleRole.id,
|
||||
],
|
||||
},
|
||||
user,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(response.hiddenRoleIds, [secondVisibleRole.id, visibleRole.id]);
|
||||
});
|
||||
test('をID指定のリスト形式で取得することができる(空)', async () => {
|
||||
const parameters = { userIds: [] };
|
||||
const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: alice });
|
||||
|
||||
@@ -14,10 +14,7 @@ import { genAidx } from '@/misc/id/aidx.js';
|
||||
import {
|
||||
BlockingsRepository,
|
||||
FollowingsRepository, FollowRequestsRepository,
|
||||
MiRole,
|
||||
MiUserProfile, MutingsRepository, RenoteMutingsRepository,
|
||||
RoleAssignmentsRepository,
|
||||
RolesRepository,
|
||||
UserMemoRepository,
|
||||
UserProfilesRepository,
|
||||
UsersRepository,
|
||||
@@ -30,7 +27,6 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
@@ -71,13 +67,6 @@ describe('UserEntityService', () => {
|
||||
let blockingRepository: BlockingsRepository;
|
||||
let mutingRepository: MutingsRepository;
|
||||
let renoteMutingsRepository: RenoteMutingsRepository;
|
||||
let rolesRepository: RolesRepository;
|
||||
let roleAssignmentsRepository: RoleAssignmentsRepository;
|
||||
let roleService: RoleService;
|
||||
type RoleServiceCacheController = {
|
||||
rolesCache: { delete(): void };
|
||||
roleAssignmentByUserIdCache: { delete(userId: MiUser['id']): void };
|
||||
};
|
||||
|
||||
async function createUser(userData: Partial<MiUser> = {}, profileData: Partial<MiUserProfile> = {}) {
|
||||
const un = secureRndstr(16);
|
||||
@@ -98,32 +87,6 @@ describe('UserEntityService', () => {
|
||||
return user;
|
||||
}
|
||||
|
||||
async function createRole(roleData: Partial<MiRole> = {}) {
|
||||
const role = await rolesRepository
|
||||
.insert({
|
||||
id: genAidx(Date.now()),
|
||||
updatedAt: new Date(),
|
||||
lastUsedAt: new Date(),
|
||||
name: '',
|
||||
description: '',
|
||||
...roleData,
|
||||
})
|
||||
.then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
(roleService as unknown as RoleServiceCacheController).rolesCache.delete();
|
||||
return role;
|
||||
}
|
||||
|
||||
async function assignRole(user: MiUser, role: MiRole) {
|
||||
await roleAssignmentsRepository.insert({
|
||||
id: genAidx(Date.now()),
|
||||
userId: user.id,
|
||||
roleId: role.id,
|
||||
});
|
||||
|
||||
(roleService as unknown as RoleServiceCacheController).roleAssignmentByUserIdCache.delete(user.id);
|
||||
}
|
||||
|
||||
async function memo(writer: MiUser, target: MiUser, memo: string) {
|
||||
await userMemosRepository.insert({
|
||||
id: genAidx(Date.now()),
|
||||
@@ -183,7 +146,6 @@ describe('UserEntityService', () => {
|
||||
ApPersonService,
|
||||
NoteEntityService,
|
||||
PageEntityService,
|
||||
RoleEntityService,
|
||||
CustomEmojiService,
|
||||
AnnouncementService,
|
||||
RoleService,
|
||||
@@ -234,9 +196,6 @@ describe('UserEntityService', () => {
|
||||
blockingRepository = app.get<BlockingsRepository>(DI.blockingsRepository);
|
||||
mutingRepository = app.get<MutingsRepository>(DI.mutingsRepository);
|
||||
renoteMutingsRepository = app.get<RenoteMutingsRepository>(DI.renoteMutingsRepository);
|
||||
rolesRepository = app.get<RolesRepository>(DI.rolesRepository);
|
||||
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
|
||||
roleService = app.get<RoleService>(RoleService);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -290,60 +249,6 @@ describe('UserEntityService', () => {
|
||||
expect(actual.achievements).toEqual(achievements);
|
||||
});
|
||||
|
||||
test('MeDetailed hiddenRoleIds are sanitized to assigned public non-forced roles in stored order', async() => {
|
||||
const me = await createUser();
|
||||
const visibleRole = await createRole({ name: 'visible', isPublic: true });
|
||||
const secondVisibleRole = await createRole({ name: 'visible2', isPublic: true });
|
||||
const privateRole = await createRole({ name: 'private', isPublic: false });
|
||||
const forcedRole = await createRole({ name: 'forced', isPublic: true, isPublicDisplayRequired: true });
|
||||
const unassignedRole = await createRole({ name: 'unassigned', isPublic: true });
|
||||
|
||||
await assignRole(me, visibleRole);
|
||||
await assignRole(me, secondVisibleRole);
|
||||
await assignRole(me, privateRole);
|
||||
await assignRole(me, forcedRole);
|
||||
await usersRepository.update(me.id, {
|
||||
hiddenRoleIds: [
|
||||
secondVisibleRole.id,
|
||||
'unknownroleid',
|
||||
unassignedRole.id,
|
||||
privateRole.id,
|
||||
forcedRole.id,
|
||||
visibleRole.id,
|
||||
secondVisibleRole.id,
|
||||
],
|
||||
});
|
||||
const updatedMe = await usersRepository.findOneByOrFail({ id: me.id });
|
||||
|
||||
const actual = await service.pack(updatedMe, updatedMe, { schema: 'MeDetailed' });
|
||||
|
||||
expect(actual.hiddenRoleIds).toEqual([secondVisibleRole.id, visibleRole.id]);
|
||||
});
|
||||
|
||||
test('UserDetailed filters hidden display roles for normal viewers, preserves forced roles, and bypasses for moderators', async() => {
|
||||
const [viewer, moderator, target] = await Promise.all([createUser(), createUser(), createUser()]);
|
||||
const hiddenRole = await createRole({ name: 'hidden', isPublic: true, asBadge: true, displayOrder: 30 });
|
||||
const visibleRole = await createRole({ name: 'visible', isPublic: true, asBadge: true, displayOrder: 20 });
|
||||
const forcedRole = await createRole({ name: 'forced', isPublic: true, isPublicDisplayRequired: true, asBadge: true, displayOrder: 10 });
|
||||
const moderatorRole = await createRole({ name: 'moderator', isModerator: true });
|
||||
|
||||
await assignRole(target, hiddenRole);
|
||||
await assignRole(target, visibleRole);
|
||||
await assignRole(target, forcedRole);
|
||||
await assignRole(moderator, moderatorRole);
|
||||
await usersRepository.update(target.id, { hiddenRoleIds: [hiddenRole.id, forcedRole.id] });
|
||||
const updatedTarget = await usersRepository.findOneByOrFail({ id: target.id });
|
||||
|
||||
const normalView = await service.pack(updatedTarget, viewer, { schema: 'UserDetailed' });
|
||||
const moderatorView = await service.pack(updatedTarget, moderator, { schema: 'UserDetailed' });
|
||||
|
||||
expect(normalView.roles.map(role => role.id)).toEqual([visibleRole.id, forcedRole.id]);
|
||||
expect(normalView.badgeRoles?.map(role => role.id)).toEqual([visibleRole.id, forcedRole.id]);
|
||||
expect(normalView.badgeRoles?.every(role => typeof role.id === 'string')).toBe(true);
|
||||
expect(moderatorView.roles.map(role => role.id)).toEqual([hiddenRole.id, visibleRole.id, forcedRole.id]);
|
||||
expect(moderatorView.badgeRoles?.map(role => role.id)).toEqual([hiddenRole.id, visibleRole.id, forcedRole.id]);
|
||||
});
|
||||
|
||||
test('alsoKnownAs as string does not throw', async () => {
|
||||
const me = await createUser();
|
||||
const who = await createUser();
|
||||
|
||||
@@ -357,7 +357,6 @@ export function role(params: {
|
||||
isPublic?: boolean,
|
||||
isExplorable?: boolean,
|
||||
asBadge?: boolean,
|
||||
isPublicDisplayRequired?: boolean,
|
||||
canEditMembersByModerator?: boolean,
|
||||
usersCount?: number,
|
||||
}, seed?: string): entities.Role {
|
||||
@@ -381,7 +380,6 @@ export function role(params: {
|
||||
isPublic: params.isPublic ?? true,
|
||||
isExplorable: params.isExplorable ?? true,
|
||||
asBadge: params.asBadge ?? true,
|
||||
isPublicDisplayRequired: params.isPublicDisplayRequired ?? false,
|
||||
canEditMembersByModerator: params.canEditMembersByModerator ?? false,
|
||||
usersCount: params.usersCount ?? 10,
|
||||
preserveAssignmentOnMoveAccount: false,
|
||||
|
||||
@@ -284,7 +284,7 @@ function shouldHideInGallery(content: Content): boolean {
|
||||
if (!hiddenByDefault) return false;
|
||||
|
||||
// ギャラリー起動時に最初に開いたセンシティブ画像だけは初期表示で隠さない
|
||||
if (content.file.isSensitive && prefer.s.nsfw !== 'force' && props.initiallyOpened) {
|
||||
if (content.file.isSensitive && props.initiallyOpened) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @pointerup="onClick" @contextmenu.stop="onContextmenu">
|
||||
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="onClick" @contextmenu.stop="onContextmenu">
|
||||
<component
|
||||
:is="disableImageLink ? 'div' : 'a'"
|
||||
v-bind="disableImageLink ? {
|
||||
@@ -61,8 +61,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
||||
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
|
||||
</div>
|
||||
<button :class="[$style.menu, $style.menuBottom]" class="_button" @click.stop="showMenu" @pointerup.stop><i class="ti ti-dots" style="vertical-align: middle;" aria-hidden="true"></i></button>
|
||||
<button :class="[$style.menu, $style.menuTop]" class="_button" @click.stop="hide = true" @pointerup.stop><i class="ti ti-eye-off" style="vertical-align: middle;" aria-hidden="true"></i></button>
|
||||
<button :class="[$style.menu, $style.menuBottom]" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;" aria-hidden="true"></i></button>
|
||||
<button :class="[$style.menu, $style.menuTop]" class="_button" @click.stop="hide = true"><i class="ti ti-eye-off" style="vertical-align: middle;" aria-hidden="true"></i></button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-else :class="$style.videoRoot" @pointerup="emit('mediaClick', $event)">
|
||||
<div v-else :class="$style.videoRoot" @click="emit('mediaClick', $event)">
|
||||
<img
|
||||
v-if="video.thumbnailUrl"
|
||||
:class="$style.video"
|
||||
@@ -41,8 +41,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i class="ti ti-player-play"></i>
|
||||
</div>
|
||||
</div>
|
||||
<button :class="[$style.menu, $style.menuBottom]" class="_button" @click.stop="showMenu" @pointerup.stop><i class="ti ti-dots" style="vertical-align: middle;" aria-hidden="true"></i></button>
|
||||
<button :class="[$style.menu, $style.menuTop]" class="_button" @click.stop="hide = true" @pointerup.stop><i class="ti ti-eye-off" style="vertical-align: middle;" aria-hidden="true"></i></button>
|
||||
<button :class="[$style.menu, $style.menuBottom]" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;" aria-hidden="true"></i></button>
|
||||
<button :class="[$style.menu, $style.menuTop]" class="_button" @click.stop="hide = true"><i class="ti ti-eye-off" style="vertical-align: middle;" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -69,8 +69,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.noteHeaderUsername">
|
||||
<MkAcct :user="appearNote.user"/>
|
||||
</div>
|
||||
<div v-if="badgeRoles" :class="$style.noteHeaderBadgeRoles">
|
||||
<img v-for="role in badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/>
|
||||
<div v-if="appearNote.user.badgeRoles" :class="$style.noteHeaderBadgeRoles">
|
||||
<img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
|
||||
@@ -251,7 +251,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 { $i } from '@/i.js';
|
||||
import type { Keymap } from '@/utility/hotkey.js';
|
||||
|
||||
// コンポーネント外部の依存関係
|
||||
@@ -333,14 +332,6 @@ provide(DI.mfmEmojiReactCallback, reactViaMfmEmoji);
|
||||
// MkNoteDetailed固有
|
||||
const tab = ref(props.initialTab);
|
||||
const reactionTabType = ref<string | null>(null);
|
||||
const badgeRoles = computed(() => {
|
||||
const roles = appearNote.user.badgeRoles;
|
||||
if (roles == null || $i == null || $i.id !== appearNote.userId) return roles;
|
||||
|
||||
// 自分のノートを自分で見た場合レスポンスに非表示ロールも含まれるので、別途除外する必要がある
|
||||
const hiddenRoleIds = new Set($i.hiddenRoleIds ?? []);
|
||||
return roles.filter(role => !hiddenRoleIds.has(role.id));
|
||||
});
|
||||
|
||||
const renotesPaginator = markRaw(new Paginator('notes/renotes', {
|
||||
limit: 10,
|
||||
|
||||
@@ -13,8 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkA>
|
||||
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
||||
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
||||
<div v-if="badgeRoles" :class="$style.badgeRoles">
|
||||
<img v-for="role in badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
|
||||
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
|
||||
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
|
||||
</div>
|
||||
<div :class="$style.info">
|
||||
<div v-if="mock">
|
||||
@@ -35,26 +35,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import { inject } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { $i } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { DI } from '@/di.js';
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
}>();
|
||||
|
||||
const badgeRoles = computed(() => {
|
||||
const roles = props.note.user.badgeRoles;
|
||||
if (roles == null || $i == null || $i.id !== props.note.userId) return roles;
|
||||
|
||||
// 自分のプロフィールを自分で見た場合レスポンスに非表示ロールも含まれるので、別途除外する必要がある
|
||||
const hiddenRoleIds = new Set(($i.hiddenRoleIds) ?? []);
|
||||
return roles.filter(role => !hiddenRoleIds.has(role.id));
|
||||
});
|
||||
const mock = inject(DI.mock, false);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const props = defineProps<{
|
||||
id?: string;
|
||||
}>();
|
||||
|
||||
type RoleLike = Pick<Misskey.entities.Role, 'name' | 'description' | 'isAdministrator' | 'isModerator' | 'color' | 'iconUrl' | 'target' | 'isPublic' | 'isPublicDisplayRequired' | 'isExplorable' | 'asBadge' | 'canEditMembersByModerator' | 'displayOrder' | 'preserveAssignmentOnMoveAccount'> & {
|
||||
type RoleLike = Pick<Misskey.entities.Role, 'name' | 'description' | 'isAdministrator' | 'isModerator' | 'color' | 'iconUrl' | 'target' | 'isPublic' | 'isExplorable' | 'asBadge' | 'canEditMembersByModerator' | 'displayOrder' | 'preserveAssignmentOnMoveAccount'> & {
|
||||
condFormula: any;
|
||||
policies: any;
|
||||
};
|
||||
@@ -62,7 +62,6 @@ if (props.id) {
|
||||
target: 'manual',
|
||||
condFormula: { id: genId(), type: 'isRemote' },
|
||||
isPublic: false,
|
||||
isPublicDisplayRequired: false,
|
||||
isExplorable: false,
|
||||
asBadge: false,
|
||||
canEditMembersByModerator: false,
|
||||
|
||||
@@ -62,12 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfIsPublic }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="role.isPublicDisplayRequired" :disabled="!role.isPublic" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.isPublicDisplayRequired }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfIsPublicDisplayRequired }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="role.asBadge" :disabled="!role.isPublic" :readonly="readonly">
|
||||
<MkSwitch v-model="role.asBadge" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.asBadge }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfAsBadge }}</template>
|
||||
</MkSwitch>
|
||||
@@ -115,7 +110,7 @@ import { instance } from '@/instance.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import type { PolicyMeta } from './roles.policy-editor.vue';
|
||||
|
||||
type RoleLike = Pick<Misskey.entities.Role, 'name' | 'description' | 'isAdministrator' | 'isModerator' | 'color' | 'iconUrl' | 'target' | 'isPublic' | 'isPublicDisplayRequired' | 'isExplorable' | 'asBadge' | 'canEditMembersByModerator' | 'displayOrder' | 'preserveAssignmentOnMoveAccount'> & {
|
||||
type RoleLike = Pick<Misskey.entities.Role, 'name' | 'description' | 'isAdministrator' | 'isModerator' | 'color' | 'iconUrl' | 'target' | 'isPublic' | 'isExplorable' | 'asBadge' | 'canEditMembersByModerator' | 'displayOrder' | 'preserveAssignmentOnMoveAccount'> & {
|
||||
id?: Misskey.entities.Role['id'] | null;
|
||||
condFormula: any;
|
||||
policies: any;
|
||||
@@ -205,7 +200,6 @@ const save = throttle(100, () => {
|
||||
isAdministrator: role.value.isAdministrator,
|
||||
isModerator: role.value.isModerator,
|
||||
isPublic: role.value.isPublic,
|
||||
isPublicDisplayRequired: role.value.isPublicDisplayRequired,
|
||||
isExplorable: role.value.isExplorable,
|
||||
asBadge: role.value.asBadge,
|
||||
canEditMembersByModerator: role.value.canEditMembersByModerator,
|
||||
|
||||
@@ -49,6 +49,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkFolder>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['roles']">
|
||||
<MkFolder>
|
||||
<template #icon><SearchIcon><i class="ti ti-badges"></i></SearchIcon></template>
|
||||
<template #label><SearchLabel>{{ i18n.ts.rolesAssignedToMe }}</SearchLabel></template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkRolePreview v-for="role in $i.roles" :key="role.id" :role="role" :forModeration="false"/>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['account', 'move', 'migration']">
|
||||
<MkFolder>
|
||||
<template #icon><SearchIcon><i class="ti ti-plane"></i></SearchIcon></template>
|
||||
@@ -115,15 +126,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="_gaps_s">
|
||||
<!-- ページ移動したのでリンクを残してある。そのうち消しても良いかも -->
|
||||
<FormLink to="/settings/roles">
|
||||
<template #icon><i class="ti ti-badges"></i></template>
|
||||
{{ i18n.ts.rolesAssignedToMe }}
|
||||
</FormLink>
|
||||
|
||||
<FormLink to="/registry"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink>
|
||||
</div>
|
||||
<FormLink to="/registry"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink>
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -162,6 +166,7 @@ import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import { signout } from '@/signout.js';
|
||||
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
|
||||
import { suggestReload } from '@/utility/reload-suggest.js';
|
||||
|
||||
@@ -72,11 +72,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</SearchMarker>
|
||||
|
||||
<FormLink to="/settings/roles">
|
||||
<template #icon><i class="ti ti-badges"></i></template>
|
||||
<SearchLabel>{{ i18n.ts._roleDisplay.title }}</SearchLabel>
|
||||
</FormLink>
|
||||
|
||||
<SearchMarker :keywords="['chat']">
|
||||
<FormSection>
|
||||
<template #label><SearchLabel>{{ i18n.ts.directMessage }}</SearchLabel></template>
|
||||
@@ -228,7 +223,6 @@ import { instance } from '@/instance.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<SearchMarker path="/settings/roles" :label="i18n.ts.roles" :keywords="['role', 'badge']" icon="ti ti-badges">
|
||||
<div class="_gaps_m">
|
||||
<MkFeatureBanner icon="/fluent-emoji/1f3f7.png" color="#ffbf00">
|
||||
<SearchText>{{ i18n.ts._roleDisplay.description }}</SearchText>
|
||||
</MkFeatureBanner>
|
||||
<div class="_gaps_s">
|
||||
<MkResult v-if="roleDisplayRoles.length === 0" type="empty"/>
|
||||
<div v-for="role in roleDisplayRoles" :key="role.id" class="_panel _gaps" :class="$style.roleItem">
|
||||
<MkRolePreview :role="role" :forModeration="false"/>
|
||||
<MkSwitch
|
||||
:modelValue="isRoleDisplayShown(role)"
|
||||
:disabled="role.isPublicDisplayRequired"
|
||||
@update:modelValue="value => updateRoleDisplay(role, value)"
|
||||
>
|
||||
<template #label>{{ i18n.ts._roleDisplay.displayToggle }}</template>
|
||||
<template v-if="role.isPublicDisplayRequired" #caption>{{ i18n.ts._roleDisplay.alwaysShownByAdmin }}</template>
|
||||
<template v-else-if="role.isExplorable" #caption>
|
||||
<i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> <I18n :src="i18n.ts._roleDisplay.roleExplorableAlert">
|
||||
<template #link>
|
||||
<MkA class="_link" :to="`/roles/${role.id}`">{{ i18n.ts.explore }}</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SearchMarker>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { updateCurrentAccount } from '@/accounts.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const hiddenRoleIds = ref([...getHiddenRoleIds($i)]);
|
||||
const roleDisplayRoles = computed(() => $i.roles);
|
||||
|
||||
function getHiddenRoleIds(user: { hiddenRoleIds?: string[] }): string[] {
|
||||
return user.hiddenRoleIds ?? [];
|
||||
}
|
||||
|
||||
function isRoleDisplayShown(role: Misskey.entities.RoleLite): boolean {
|
||||
return role.isPublicDisplayRequired === true || !hiddenRoleIds.value.includes(role.id);
|
||||
}
|
||||
|
||||
async function updateRoleDisplay(role: Misskey.entities.RoleLite, visible: boolean) {
|
||||
if (role.isPublicDisplayRequired === true) return;
|
||||
|
||||
const nextHiddenRoleIds = new Set(hiddenRoleIds.value);
|
||||
if (visible) {
|
||||
nextHiddenRoleIds.delete(role.id);
|
||||
} else {
|
||||
nextHiddenRoleIds.add(role.id);
|
||||
}
|
||||
|
||||
const nextIds = roleDisplayRoles.value
|
||||
.filter(role => role.isPublicDisplayRequired !== true && nextHiddenRoleIds.has(role.id))
|
||||
.map(role => role.id);
|
||||
|
||||
const updated = await misskeyApi('i/update', {
|
||||
hiddenRoleIds: nextIds,
|
||||
});
|
||||
|
||||
updateCurrentAccount(updated);
|
||||
hiddenRoleIds.value = [...getHiddenRoleIds(updated)];
|
||||
}
|
||||
|
||||
definePage(() => ({
|
||||
title: i18n.ts.roles,
|
||||
icon: 'ti ti-badges',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.roleItem {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -53,8 +53,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user" class="_selectable"/></MkSparkle></div>
|
||||
</MkFukidashi>
|
||||
</div>
|
||||
<div v-if="visibleProfileRoles.length > 0" class="roles">
|
||||
<span v-for="role in visibleProfileRoles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color ?? '' }">
|
||||
<div v-if="user.roles.length > 0" class="roles">
|
||||
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color ?? '' }">
|
||||
<MkA v-adaptive-bg :to="`/roles/${role.id}`">
|
||||
<img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
|
||||
{{ role.name }}
|
||||
@@ -222,14 +222,6 @@ const emit = defineEmits<{
|
||||
const router = useRouter();
|
||||
|
||||
const user = ref(props.user);
|
||||
const visibleProfileRoles = computed(() => {
|
||||
const roles = user.value.roles;
|
||||
if ($i == null || $i.id !== user.value.id) return roles;
|
||||
|
||||
// 自分のプロフィールを自分で見た場合レスポンスに非表示ロールも含まれるので、別途除外する必要がある
|
||||
const hiddenRoleIds = new Set($i.hiddenRoleIds ?? []);
|
||||
return roles.filter(role => role.isPublicDisplayRequired === true || !hiddenRoleIds.has(role.id));
|
||||
});
|
||||
const narrow = ref<null | boolean>(null);
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const bannerEl = useTemplateRef('bannerEl');
|
||||
|
||||
@@ -88,10 +88,6 @@ export const ROUTE_DEF = [{
|
||||
path: '/privacy',
|
||||
name: 'privacy',
|
||||
component: page(() => import('@/pages/settings/privacy.vue')),
|
||||
}, {
|
||||
path: '/roles',
|
||||
name: 'roles',
|
||||
component: page(() => import('@/pages/settings/roles.vue')),
|
||||
}, {
|
||||
path: '/emoji-palette',
|
||||
name: 'emoji-palette',
|
||||
|
||||
@@ -411,7 +411,8 @@ export class Paginator<
|
||||
public releaseQueue(): void {
|
||||
if (this.aheadQueue.length === 0) return; // これやらないと余計なre-renderが走る
|
||||
for (const item of this.aheadQueue) {
|
||||
item._shouldAnimateIn_ = true;
|
||||
item._shouldAnimateIn_ = false; // 一気に入るときは挿入アニメーションさせない
|
||||
item._shouldAnimateOut_ = false;
|
||||
}
|
||||
this.unshiftItems(this.aheadQueue);
|
||||
this.aheadQueue = [];
|
||||
|
||||
@@ -8026,14 +8026,6 @@ export interface Locale extends ILocale {
|
||||
* ユーザーのプロフィールでこのロールが表示されます。
|
||||
*/
|
||||
"descriptionOfIsPublic": string;
|
||||
/**
|
||||
* 非表示を許可しない(常に表示)
|
||||
*/
|
||||
"isPublicDisplayRequired": string;
|
||||
/**
|
||||
* 有効にすると、ユーザーはこの公開ロール/ロールバッジを非表示にできません。
|
||||
*/
|
||||
"descriptionOfIsPublicDisplayRequired": string;
|
||||
/**
|
||||
* オプション
|
||||
*/
|
||||
@@ -12187,28 +12179,6 @@ export interface Locale extends ILocale {
|
||||
"notUnique": string;
|
||||
};
|
||||
};
|
||||
"_roleDisplay": {
|
||||
/**
|
||||
* ロールの表示/非表示
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* 自分に割り当てられているロールを確認したり、プロフィールやノート上で表示・公開するロールを選択したりできます。
|
||||
*/
|
||||
"description": string;
|
||||
/**
|
||||
* このロールは、管理者により、{link}への表示とロールタイムラインの有効化が設定されています。プロフィール上で非表示にすることはできますが、あなたにこのロールが付与されていることが知られる可能性があります。
|
||||
*/
|
||||
"roleExplorableAlert": ParameterizedString<"link">;
|
||||
/**
|
||||
* ロール/ロールバッジを表示する
|
||||
*/
|
||||
"displayToggle": string;
|
||||
/**
|
||||
* 管理者の設定により非表示にすることはできません。
|
||||
*/
|
||||
"alwaysShownByAdmin": string;
|
||||
};
|
||||
"_roleSelectDialog": {
|
||||
/**
|
||||
* 選択されていません
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2026.7.0-beta.3",
|
||||
"version": "2026.7.0-beta.4",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
||||
@@ -4061,8 +4061,6 @@ export type components = {
|
||||
/** @enum {string} */
|
||||
onlineStatus: 'unknown' | 'online' | 'active' | 'offline';
|
||||
badgeRoles?: {
|
||||
/** Format: id */
|
||||
id: string;
|
||||
name: string;
|
||||
iconUrl: string | null;
|
||||
displayOrder: number;
|
||||
@@ -4153,7 +4151,6 @@ export type components = {
|
||||
preventAiLearning: boolean;
|
||||
isExplorable: boolean;
|
||||
isDeleted: boolean;
|
||||
hiddenRoleIds: string[];
|
||||
/** @enum {string} */
|
||||
twoFactorBackupCodesStock: 'full' | 'partial' | 'none';
|
||||
hideOnlineStatus: boolean;
|
||||
@@ -5325,12 +5322,6 @@ export type components = {
|
||||
isModerator: boolean;
|
||||
/** @example false */
|
||||
isAdministrator: boolean;
|
||||
/** @example false */
|
||||
asBadge: boolean;
|
||||
/** @example false */
|
||||
isPublicDisplayRequired: boolean;
|
||||
/** @example false */
|
||||
isExplorable: boolean;
|
||||
/** @example 0 */
|
||||
displayOrder: number;
|
||||
};
|
||||
@@ -5345,8 +5336,6 @@ export type components = {
|
||||
/** @example false */
|
||||
isPublic: boolean;
|
||||
/** @example false */
|
||||
isPublicDisplayRequired: boolean;
|
||||
/** @example false */
|
||||
isExplorable: boolean;
|
||||
/** @example false */
|
||||
asBadge: boolean;
|
||||
@@ -11052,8 +11041,6 @@ export interface operations {
|
||||
target: 'manual' | 'conditional';
|
||||
condFormula: Record<string, never>;
|
||||
isPublic: boolean;
|
||||
/** @default false */
|
||||
isPublicDisplayRequired?: boolean;
|
||||
isModerator: boolean;
|
||||
isAdministrator: boolean;
|
||||
/** @default false */
|
||||
@@ -11389,7 +11376,6 @@ export interface operations {
|
||||
target?: 'manual' | 'conditional';
|
||||
condFormula?: Record<string, never>;
|
||||
isPublic?: boolean;
|
||||
isPublicDisplayRequired?: boolean;
|
||||
isModerator?: boolean;
|
||||
isAdministrator?: boolean;
|
||||
isExplorable?: boolean;
|
||||
@@ -27903,7 +27889,6 @@ export interface operations {
|
||||
followersVisibility?: 'public' | 'followers' | 'private';
|
||||
/** @enum {string} */
|
||||
chatScope?: 'everyone' | 'followers' | 'following' | 'mutual' | 'none';
|
||||
hiddenRoleIds?: string[];
|
||||
/** Format: misskey:id */
|
||||
pinnedPageId?: string | null;
|
||||
mutedWords?: (string[] | string)[];
|
||||
|
||||
Reference in New Issue
Block a user