enhance(sensitive-flag):センシティブフラグの機能の強化 (MisskeyIO#936)
This commit is contained in:
parent
7a94724098
commit
abdaa18666
18 changed files with 197 additions and 20 deletions
13
locales/index.d.ts
vendored
13
locales/index.d.ts
vendored
|
@ -5362,6 +5362,15 @@ export interface Locale extends ILocale {
|
||||||
* {x}に投稿されます
|
* {x}に投稿されます
|
||||||
*/
|
*/
|
||||||
"willBePostedAt": ParameterizedString<"x">;
|
"willBePostedAt": ParameterizedString<"x">;
|
||||||
|
/**
|
||||||
|
* 管理者によって、ドライブのファイルがセンシティブとして設定されました。
|
||||||
|
* 詳細については、[NSFWガイドライン](https://go.misskey.io/media-guideline)を確認してください。
|
||||||
|
*/
|
||||||
|
"sensitiveByModerator": string;
|
||||||
|
/**
|
||||||
|
* この情報は他のユーザーには公開されません。
|
||||||
|
*/
|
||||||
|
"thisInfoIsNotVisibleOtherUser": string;
|
||||||
"_bubbleGame": {
|
"_bubbleGame": {
|
||||||
/**
|
/**
|
||||||
* 遊び方
|
* 遊び方
|
||||||
|
@ -9747,6 +9756,10 @@ export interface Locale extends ILocale {
|
||||||
* 通知の履歴をリセットする
|
* 通知の履歴をリセットする
|
||||||
*/
|
*/
|
||||||
"flushNotification": string;
|
"flushNotification": string;
|
||||||
|
/**
|
||||||
|
* ドライブのファイルがセンシティブとして設定されました
|
||||||
|
*/
|
||||||
|
"sensitiveFlagAssigned": string;
|
||||||
"_types": {
|
"_types": {
|
||||||
/**
|
/**
|
||||||
* すべて
|
* すべて
|
||||||
|
|
|
@ -1334,6 +1334,8 @@ scheduled: "予約済み"
|
||||||
unschedule: "予約を解除"
|
unschedule: "予約を解除"
|
||||||
setScheduledTime: "予約日時を設定"
|
setScheduledTime: "予約日時を設定"
|
||||||
willBePostedAt: "{x}に投稿されます"
|
willBePostedAt: "{x}に投稿されます"
|
||||||
|
sensitiveByModerator: "管理者によって、ドライブのファイルがセンシティブとして設定されました。\n詳細については、[NSFWガイドライン](https://go.misskey.io/media-guideline)を確認してください。"
|
||||||
|
thisInfoIsNotVisibleOtherUser: "この情報は他のユーザーには公開されません。"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "遊び方"
|
howToPlay: "遊び方"
|
||||||
|
@ -2562,6 +2564,7 @@ _notification:
|
||||||
renotedBySomeUsers: "{n}人がリノートしました"
|
renotedBySomeUsers: "{n}人がリノートしました"
|
||||||
followedBySomeUsers: "{n}人にフォローされました"
|
followedBySomeUsers: "{n}人にフォローされました"
|
||||||
flushNotification: "通知の履歴をリセットする"
|
flushNotification: "通知の履歴をリセットする"
|
||||||
|
sensitiveFlagAssigned: "ドライブのファイルがセンシティブとして設定されました"
|
||||||
|
|
||||||
_types:
|
_types:
|
||||||
all: "すべて"
|
all: "すべて"
|
||||||
|
|
13
packages/backend/migration/1739335129758-sensitiveFlag.js
Normal file
13
packages/backend/migration/1739335129758-sensitiveFlag.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export class SensitiveFlag1739335129758 {
|
||||||
|
name = 'SensitiveFlag1739335129758'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "drive_file" ADD "isSensitiveByModerator" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_e779d1afdfa44dc3d64213cd2e" ON "drive_file" ("isSensitiveByModerator") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_e779d1afdfa44dc3d64213cd2e"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "isSensitiveByModerator"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,7 @@ import { correctFilename } from '@/misc/correct-filename.js';
|
||||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
|
|
||||||
type AddFileArgs = {
|
type AddFileArgs = {
|
||||||
/** User who wish to add file */
|
/** User who wish to add file */
|
||||||
|
@ -129,6 +130,7 @@ export class DriveService {
|
||||||
private driveChart: DriveChart,
|
private driveChart: DriveChart,
|
||||||
private perUserDriveChart: PerUserDriveChart,
|
private perUserDriveChart: PerUserDriveChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
const logger = this.loggerService.getLogger('drive', 'blue');
|
const logger = this.loggerService.getLogger('drive', 'blue');
|
||||||
this.registerLogger = logger.createSubLogger('register', 'yellow');
|
this.registerLogger = logger.createSubLogger('register', 'yellow');
|
||||||
|
@ -664,13 +666,15 @@ export class DriveService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
|
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
|
||||||
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;
|
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;
|
||||||
|
const isModerator = await this.roleService.isModerator(updater);
|
||||||
|
|
||||||
if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) {
|
if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) {
|
||||||
throw new DriveService.InvalidFileNameError();
|
throw new DriveService.InvalidFileNameError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive && alwaysMarkNsfw && !values.isSensitive) {
|
if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive && !values.isSensitive) {
|
||||||
throw new DriveService.CannotUnmarkSensitiveError();
|
if (alwaysMarkNsfw) throw new DriveService.CannotUnmarkSensitiveError();
|
||||||
|
if (file.isSensitiveByModerator && (file.userId === updater.id)) throw new DriveService.CannotUnmarkSensitiveError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.folderId != null) {
|
if (values.folderId != null) {
|
||||||
|
@ -684,6 +688,10 @@ export class DriveService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isModerator && file.userId !== updater.id) {
|
||||||
|
values.isSensitiveByModerator = values.isSensitive;
|
||||||
|
}
|
||||||
|
|
||||||
await this.driveFilesRepository.update(file.id, values);
|
await this.driveFilesRepository.update(file.id, values);
|
||||||
|
|
||||||
const fileObj = await this.driveFileEntityService.pack(file.id, updater, { self: true });
|
const fileObj = await this.driveFileEntityService.pack(file.id, updater, { self: true });
|
||||||
|
@ -693,7 +701,7 @@ export class DriveService {
|
||||||
this.globalEventService.publishDriveStream(file.userId, 'fileUpdated', fileObj);
|
this.globalEventService.publishDriveStream(file.userId, 'fileUpdated', fileObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.roleService.isModerator(updater) && (file.userId !== updater.id)) {
|
if (isModerator && (file.userId !== updater.id)) {
|
||||||
if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive) {
|
if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive) {
|
||||||
const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null;
|
const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null;
|
||||||
if (values.isSensitive) {
|
if (values.isSensitive) {
|
||||||
|
@ -703,6 +711,11 @@ export class DriveService {
|
||||||
fileUserUsername: user?.username ?? null,
|
fileUserUsername: user?.username ?? null,
|
||||||
fileUserHost: user?.host ?? null,
|
fileUserHost: user?.host ?? null,
|
||||||
});
|
});
|
||||||
|
if (file.userId) {
|
||||||
|
this.notificationService.createNotification(file.userId, 'sensitiveFlagAssigned', {
|
||||||
|
fileId: file.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.moderationLogService.log(updater, 'unmarkSensitiveDriveFile', {
|
this.moderationLogService.log(updater, 'unmarkSensitiveDriveFile', {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
|
|
|
@ -210,6 +210,9 @@ export class DriveFileEntityService {
|
||||||
md5: file.md5,
|
md5: file.md5,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
isSensitive: file.isSensitive,
|
isSensitive: file.isSensitive,
|
||||||
|
...(opts.detail ? {
|
||||||
|
isSensitiveByModerator: file.isSensitiveByModerator,
|
||||||
|
} : {}),
|
||||||
blurhash: file.blurhash,
|
blurhash: file.blurhash,
|
||||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||||
url: opts.self ? file.url : this.getPublicUrl(file),
|
url: opts.self ? file.url : this.getPublicUrl(file),
|
||||||
|
@ -246,6 +249,9 @@ export class DriveFileEntityService {
|
||||||
md5: file.md5,
|
md5: file.md5,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
isSensitive: file.isSensitive,
|
isSensitive: file.isSensitive,
|
||||||
|
...(opts.detail ? {
|
||||||
|
isSensitiveByModerator: file.isSensitiveByModerator,
|
||||||
|
} : {}),
|
||||||
blurhash: file.blurhash,
|
blurhash: file.blurhash,
|
||||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||||
url: opts.self ? file.url : this.getPublicUrl(file),
|
url: opts.self ? file.url : this.getPublicUrl(file),
|
||||||
|
|
|
@ -183,6 +183,9 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
header: notification.customHeader,
|
header: notification.customHeader,
|
||||||
icon: notification.customIcon,
|
icon: notification.customIcon,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
...(notification.type === 'sensitiveFlagAssigned' ? {
|
||||||
|
fileId: notification.fileId,
|
||||||
|
} : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -162,6 +162,12 @@ export class MiDriveFile {
|
||||||
})
|
})
|
||||||
public isSensitive: boolean;
|
public isSensitive: boolean;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public isSensitiveByModerator: boolean;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
|
|
|
@ -93,6 +93,11 @@ export type MiNotification = {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
draftId: MiScheduledNote['id'];
|
draftId: MiScheduledNote['id'];
|
||||||
|
} | {
|
||||||
|
type: 'sensitiveFlagAssigned'
|
||||||
|
id: string;
|
||||||
|
fileId: string;
|
||||||
|
createdAt: string;
|
||||||
} | {
|
} | {
|
||||||
type: 'app';
|
type: 'app';
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -42,6 +42,10 @@ export const packedDriveFileSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isSensitiveByModerator: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
},
|
||||||
blurhash: {
|
blurhash: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
|
|
@ -309,8 +309,8 @@ export const packedNotificationSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
ref: 'NoteDraft',
|
ref: 'NoteDraft',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}, {
|
}, {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -324,8 +324,8 @@ export const packedNotificationSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
ref: 'Note',
|
ref: 'Note',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}, {
|
}, {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -339,8 +339,21 @@ export const packedNotificationSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
ref: 'NoteDraft',
|
ref: 'NoteDraft',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['sensitiveFlagAssigned'],
|
||||||
|
},
|
||||||
|
fileId: {
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|
|
@ -51,6 +51,12 @@ export const meta = {
|
||||||
code: 'RESTRICTED_BY_ROLE',
|
code: 'RESTRICTED_BY_ROLE',
|
||||||
id: '7f59dccb-f465-75ab-5cf4-3ce44e3282f7',
|
id: '7f59dccb-f465-75ab-5cf4-3ce44e3282f7',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
restrictedByModerator: {
|
||||||
|
message: 'The isSensitive specified by the administrator cannot be changed.',
|
||||||
|
code: 'RESTRICTED_BY_ADMINISTRATOR',
|
||||||
|
id: '20e6c501-e579-400d-97e4-1c7efc286f35',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -105,7 +111,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
} else if (e instanceof DriveService.NoSuchFolderError) {
|
} else if (e instanceof DriveService.NoSuchFolderError) {
|
||||||
throw new ApiError(meta.errors.noSuchFolder);
|
throw new ApiError(meta.errors.noSuchFolder);
|
||||||
} else if (e instanceof DriveService.CannotUnmarkSensitiveError) {
|
} else if (e instanceof DriveService.CannotUnmarkSensitiveError) {
|
||||||
throw new ApiError(meta.errors.restrictedByRole);
|
if (file.isSensitiveByModerator) {
|
||||||
|
throw new ApiError(meta.errors.restrictedByModerator);
|
||||||
|
} else {
|
||||||
|
throw new ApiError(meta.errors.restrictedByRole);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import type { MiNote } from '@/models/Note.js';
|
||||||
* noteScheduled - 予約投稿が予約された
|
* noteScheduled - 予約投稿が予約された
|
||||||
* scheduledNotePosted - 予約投稿が投稿された
|
* scheduledNotePosted - 予約投稿が投稿された
|
||||||
* scheduledNoteError - 予約投稿がエラーになった
|
* scheduledNoteError - 予約投稿がエラーになった
|
||||||
|
* sensitiveFlagAssigned - センシティブフラグが付与された
|
||||||
* app - アプリ通知
|
* app - アプリ通知
|
||||||
* test - テスト通知(サーバー側)
|
* test - テスト通知(サーバー側)
|
||||||
*/
|
*/
|
||||||
|
@ -45,6 +46,7 @@ export const notificationTypes = [
|
||||||
'noteScheduled',
|
'noteScheduled',
|
||||||
'scheduledNotePosted',
|
'scheduledNotePosted',
|
||||||
'scheduledNoteError',
|
'scheduledNoteError',
|
||||||
|
'sensitiveFlagAssigned',
|
||||||
'app',
|
'app',
|
||||||
'test',
|
'test',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
|
@ -95,6 +95,7 @@ describe('NoteCreateService', () => {
|
||||||
folderId: null,
|
folderId: null,
|
||||||
folder: null,
|
folder: null,
|
||||||
isSensitive: false,
|
isSensitive: false,
|
||||||
|
isSensitiveByModerator: false,
|
||||||
maybeSensitive: false,
|
maybeSensitive: false,
|
||||||
maybePorn: false,
|
maybePorn: false,
|
||||||
isLink: false,
|
isLink: false,
|
||||||
|
|
|
@ -8,6 +8,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.head">
|
<div :class="$style.head">
|
||||||
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'noteScheduled', 'scheduledNotePosted', 'scheduledNoteError'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'noteScheduled', 'scheduledNotePosted', 'scheduledNoteError'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||||
|
<div
|
||||||
|
v-else-if="notification.type === 'sensitiveFlagAssigned'"
|
||||||
|
:class="$style.iconFrame"
|
||||||
|
>
|
||||||
|
<div :class="[$style.iconInner]">
|
||||||
|
<img :class="$style.iconImg" src="/fluent-emoji/1f6a9.png">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
||||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
||||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||||
|
@ -71,6 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-else-if="notification.type === 'reaction:grouped'" :class="$style.headerName">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
<span v-else-if="notification.type === 'reaction:grouped'" :class="$style.headerName">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||||
<span v-else-if="notification.type === 'renote:grouped'" :class="$style.headerName">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
<span v-else-if="notification.type === 'renote:grouped'" :class="$style.headerName">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||||
<span v-else-if="notification.type === 'app'" :class="$style.headerName">{{ notification.header }}</span>
|
<span v-else-if="notification.type === 'app'" :class="$style.headerName">{{ notification.header }}</span>
|
||||||
|
<MkA v-else-if="notification.type === 'sensitiveFlagAssigned'" :to="'/my/drive/file/'+notification.fileId" :class="$style.headerName">{{ i18n.ts._notification.sensitiveFlagAssigned }}</MkA>
|
||||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
|
@ -159,6 +168,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
|
<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Mfm v-else-if="notification.type === 'sensitiveFlagAssigned'" :text="i18n.ts.sensitiveByModerator"/>
|
||||||
|
<span v-if="['sensitiveFlagAssigned'].includes(notification.type)" :class="$style.text" style="opacity: 0.6;">
|
||||||
|
{{ i18n.ts.thisInfoIsNotVisibleOtherUser }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -341,6 +354,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.t_sensitiveFlagAssigned {
|
||||||
|
padding: 3px;
|
||||||
|
background: var(--eventOther);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tail {
|
.tail {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
@ -430,6 +449,42 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconFrame {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
filter: drop-shadow(0px 2px 2px #00000044);
|
||||||
|
box-shadow: 0 1px 0px #ffffff88 inset;
|
||||||
|
overflow: clip;
|
||||||
|
background: linear-gradient(0deg, #703827, #d37566);
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconImg {
|
||||||
|
width: calc(100% - 12px);
|
||||||
|
height: calc(100% - 12px);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: auto;
|
||||||
|
filter: drop-shadow(0px 1px 2px #000000aa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconInner {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 100%;
|
||||||
|
box-shadow: 0 1px 0px #ffffff88 inset;
|
||||||
|
background: linear-gradient(0deg, #d37566, #703827);
|
||||||
|
}
|
||||||
|
|
||||||
@container (max-width: 600px) {
|
@container (max-width: 600px) {
|
||||||
.root {
|
.root {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
|
@ -70,6 +70,7 @@ export const notificationTypes = [
|
||||||
'noteScheduled',
|
'noteScheduled',
|
||||||
'scheduledNotePosted',
|
'scheduledNotePosted',
|
||||||
'scheduledNoteError',
|
'scheduledNoteError',
|
||||||
|
'sensitiveFlagAssigned',
|
||||||
'app',
|
'app',
|
||||||
] as const;
|
] as const;
|
||||||
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
||||||
|
|
|
@ -6,6 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo>
|
<MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo>
|
||||||
|
<MkInfo v-if="file && file.isSensitiveByModerator" :warn="true">
|
||||||
|
<Mfm :text="i18n.ts.sensitiveByModerator"/>
|
||||||
|
</MkInfo>
|
||||||
<MkLoading v-if="fetching"/>
|
<MkLoading v-if="fetching"/>
|
||||||
<div v-else-if="file" class="_gaps">
|
<div v-else-if="file" class="_gaps">
|
||||||
<div :class="$style.filePreviewRoot">
|
<div :class="$style.filePreviewRoot">
|
||||||
|
@ -23,12 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<button v-if="isImage" v-tooltip="i18n.ts.cropImage" class="_button" :class="$style.fileQuickActionsOthersButton" @click="crop()">
|
<button v-if="isImage" v-tooltip="i18n.ts.cropImage" class="_button" :class="$style.fileQuickActionsOthersButton" @click="crop()">
|
||||||
<i class="ti ti-crop"></i>
|
<i class="ti ti-crop"></i>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="file.isSensitive" v-tooltip="i18n.ts.unmarkAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()">
|
<span v-if="!file.isSensitiveByModerator">
|
||||||
<i class="ti ti-eye"></i>
|
<button v-if="file.isSensitive" v-tooltip="i18n.ts.unmarkAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()">
|
||||||
</button>
|
<i class="ti ti-eye"></i>
|
||||||
<button v-else v-tooltip="i18n.ts.markAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()">
|
</button>
|
||||||
<i class="ti ti-eye-exclamation"></i>
|
<button v-else v-tooltip="i18n.ts.markAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()">
|
||||||
</button>
|
<i class="ti ti-eye-exclamation"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
<a v-tooltip="i18n.ts.download" :href="file.url" :download="file.name" class="_button" :class="$style.fileQuickActionsOthersButton">
|
<a v-tooltip="i18n.ts.download" :href="file.url" :download="file.name" class="_button" :class="$style.fileQuickActionsOthersButton">
|
||||||
<i class="ti ti-download"></i>
|
<i class="ti ti-download"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -21,6 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div class="_margin _gaps_s">
|
<div class="_margin _gaps_s">
|
||||||
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
|
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
|
||||||
|
<MkInfo v-if="hasSensitiveFiles" :warn="true">
|
||||||
|
<Mfm :text="i18n.ts.sensitiveByModerator"/>
|
||||||
|
</MkInfo>
|
||||||
<MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note"/>
|
<MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="clips && clips.length > 0" class="_margin">
|
<div v-if="clips && clips.length > 0" class="_margin">
|
||||||
|
@ -61,6 +64,8 @@ import { i18n } from '@/i18n.js';
|
||||||
import { dateString } from '@/filters/date.js';
|
import { dateString } from '@/filters/date.js';
|
||||||
import MkClipPreview from '@/components/MkClipPreview.vue';
|
import MkClipPreview from '@/components/MkClipPreview.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
noteId: string;
|
noteId: string;
|
||||||
|
@ -69,6 +74,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
const note = ref<null | Misskey.entities.Note>();
|
const note = ref<null | Misskey.entities.Note>();
|
||||||
const clips = ref<Misskey.entities.Clip[]>();
|
const clips = ref<Misskey.entities.Clip[]>();
|
||||||
|
const hasSensitiveFiles = ref(false);
|
||||||
const showPrev = ref<'user' | 'channel' | false>(false);
|
const showPrev = ref<'user' | 'channel' | false>(false);
|
||||||
const showNext = ref<'user' | 'channel' | false>(false);
|
const showNext = ref<'user' | 'channel' | false>(false);
|
||||||
const error = ref();
|
const error = ref();
|
||||||
|
@ -119,6 +125,15 @@ function fetchNote() {
|
||||||
noteId: props.noteId,
|
noteId: props.noteId,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
note.value = res;
|
note.value = res;
|
||||||
|
|
||||||
|
if (note.value.userId === $i?.id && note.value.fileIds) {
|
||||||
|
Promise.all(note.value.fileIds.map(fileId =>
|
||||||
|
misskeyApi('drive/files/show', { fileId: fileId }),
|
||||||
|
)).then(files => {
|
||||||
|
hasSensitiveFiles.value = files.some(file => file.isSensitiveByModerator);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 古いノートは被クリップ数をカウントしていないので、2023-10-01以前のものは強制的にnotes/clipsを叩く
|
// 古いノートは被クリップ数をカウントしていないので、2023-10-01以前のものは強制的にnotes/clipsを叩く
|
||||||
if (note.value.clippedCount > 0 || new Date(note.value.createdAt).getTime() < new Date('2023-10-01').getTime()) {
|
if (note.value.clippedCount > 0 || new Date(note.value.createdAt).getTime() < new Date('2023-10-01').getTime()) {
|
||||||
misskeyApi('notes/clips', {
|
misskeyApi('notes/clips', {
|
||||||
|
|
|
@ -4513,6 +4513,14 @@ export type components = {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'scheduledNoteError';
|
type: 'scheduledNoteError';
|
||||||
draft: components['schemas']['NoteDraft'];
|
draft: components['schemas']['NoteDraft'];
|
||||||
|
} | {
|
||||||
|
/** Format: id */
|
||||||
|
id: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: 'sensitiveFlagAssigned';
|
||||||
|
fileId: unknown;
|
||||||
} | {
|
} | {
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -4572,6 +4580,7 @@ export type components = {
|
||||||
/** @example 51469 */
|
/** @example 51469 */
|
||||||
size: number;
|
size: number;
|
||||||
isSensitive: boolean;
|
isSensitive: boolean;
|
||||||
|
isSensitiveByModerator?: boolean | null;
|
||||||
blurhash: string | null;
|
blurhash: string | null;
|
||||||
properties: {
|
properties: {
|
||||||
/** @example 1280 */
|
/** @example 1280 */
|
||||||
|
@ -20467,8 +20476,8 @@ export type operations = {
|
||||||
untilId?: string;
|
untilId?: string;
|
||||||
/** @default true */
|
/** @default true */
|
||||||
markAsRead?: boolean;
|
markAsRead?: boolean;
|
||||||
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'sensitiveFlagAssigned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||||
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'sensitiveFlagAssigned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -20535,8 +20544,8 @@ export type operations = {
|
||||||
untilId?: string;
|
untilId?: string;
|
||||||
/** @default true */
|
/** @default true */
|
||||||
markAsRead?: boolean;
|
markAsRead?: boolean;
|
||||||
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'sensitiveFlagAssigned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
||||||
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'noteScheduled' | 'scheduledNotePosted' | 'scheduledNoteError' | 'sensitiveFlagAssigned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue