feat: sensitive channel (#11438)
* feat(backend): add isSensitive to Channel * feat(backend): support isSensitive in channel endpoints * feat(frontend/channel-editor): support isSensitive in create/edit channel page * feat(frontend/channel): show sensitive indicator for sensitive channels * docs(changelog): add チャンネルをセンシティブ指定できるようになりました * chore: license header for each file * chore: add isSensitive of channel to Note object
This commit is contained in:
parent
79966d33b5
commit
c5b8766a18
12 changed files with 73 additions and 0 deletions
|
@ -51,6 +51,7 @@
|
||||||
- ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました
|
- ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました
|
||||||
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
|
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
|
||||||
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
|
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
|
||||||
|
- チャンネルをセンシティブ指定できるようになりました
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
|
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
|
||||||
|
|
17
packages/backend/migration/1690782653311-SensitiveChannel.js
Normal file
17
packages/backend/migration/1690782653311-SensitiveChannel.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SensitiveChannel1690782653311 {
|
||||||
|
name = 'SensitiveChannel1690782653311'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel"
|
||||||
|
ADD "isSensitive" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "isSensitive"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -92,6 +92,7 @@ export class ChannelEntityService {
|
||||||
isArchived: channel.isArchived,
|
isArchived: channel.isArchived,
|
||||||
usersCount: channel.usersCount,
|
usersCount: channel.usersCount,
|
||||||
notesCount: channel.notesCount,
|
notesCount: channel.notesCount,
|
||||||
|
isSensitive: channel.isSensitive,
|
||||||
|
|
||||||
...(me ? {
|
...(me ? {
|
||||||
isFollowing,
|
isFollowing,
|
||||||
|
|
|
@ -333,6 +333,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
color: channel.color,
|
color: channel.color,
|
||||||
|
isSensitive: channel.isSensitive,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||||
uri: note.uri ?? undefined,
|
uri: note.uri ?? undefined,
|
||||||
|
|
|
@ -94,4 +94,9 @@ export class Channel {
|
||||||
comment: 'The count of users.',
|
comment: 'The count of users.',
|
||||||
})
|
})
|
||||||
public usersCount: number;
|
public usersCount: number;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public isSensitive: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,5 +72,9 @@ export const packedChannelSchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isSensitive: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -139,6 +139,10 @@ export const packedNoteSchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
isSensitive: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -49,6 +49,7 @@ export const paramDef = {
|
||||||
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
|
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
|
||||||
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
color: { type: 'string', minLength: 1, maxLength: 16 },
|
color: { type: 'string', minLength: 1, maxLength: 16 },
|
||||||
|
isSensitive: { type: 'boolean', nullable: true },
|
||||||
},
|
},
|
||||||
required: ['name'],
|
required: ['name'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -86,6 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
name: ps.name,
|
name: ps.name,
|
||||||
description: ps.description ?? null,
|
description: ps.description ?? null,
|
||||||
bannerId: banner ? banner.id : null,
|
bannerId: banner ? banner.id : null,
|
||||||
|
isSensitive: ps.isSensitive ?? false,
|
||||||
...(ps.color !== undefined ? { color: ps.color } : {}),
|
...(ps.color !== undefined ? { color: ps.color } : {}),
|
||||||
} as Channel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
|
} as Channel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,7 @@ export const paramDef = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
color: { type: 'string', minLength: 1, maxLength: 16 },
|
color: { type: 'string', minLength: 1, maxLength: 16 },
|
||||||
|
isSensitive: { type: 'boolean', nullable: true },
|
||||||
},
|
},
|
||||||
required: ['channelId'],
|
required: ['channelId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -114,6 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
...(ps.color !== undefined ? { color: ps.color } : {}),
|
...(ps.color !== undefined ? { color: ps.color } : {}),
|
||||||
...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}),
|
...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}),
|
||||||
...(banner ? { bannerId: banner.id } : {}),
|
...(banner ? { bannerId: banner.id } : {}),
|
||||||
|
...(typeof ps.isSensitive === 'boolean' ? { isSensitive: ps.isSensitive } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.channelEntityService.pack(channel.id, me);
|
return await this.channelEntityService.pack(channel.id, me);
|
||||||
|
|
|
@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="banner" :style="bannerStyle">
|
<div class="banner" :style="bannerStyle">
|
||||||
<div class="fade"></div>
|
<div class="fade"></div>
|
||||||
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
|
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
|
||||||
|
<div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<div>
|
<div>
|
||||||
<i class="ti ti-users ti-fw"></i>
|
<i class="ti ti-users ti-fw"></i>
|
||||||
|
@ -102,6 +103,19 @@ const bannerStyle = computed(() => {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .sensitiveIndicator {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: 16px;
|
||||||
|
left: 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: var(--warn);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 4px 7px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> article {
|
> article {
|
||||||
|
|
|
@ -20,6 +20,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.color }}</template>
|
<template #label>{{ i18n.ts.color }}</template>
|
||||||
</MkColorInput>
|
</MkColorInput>
|
||||||
|
|
||||||
|
<MkSwitch v-model="isSensitive">
|
||||||
|
<template #label>{{ i18n.ts.sensitive }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
|
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
|
||||||
<div v-else-if="bannerUrl">
|
<div v-else-if="bannerUrl">
|
||||||
|
@ -72,6 +76,7 @@ import { useRouter } from '@/router';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkSwitch from "@/components/MkSwitch.vue";
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||||
|
|
||||||
|
@ -87,6 +92,7 @@ let description = $ref(null);
|
||||||
let bannerUrl = $ref<string | null>(null);
|
let bannerUrl = $ref<string | null>(null);
|
||||||
let bannerId = $ref<string | null>(null);
|
let bannerId = $ref<string | null>(null);
|
||||||
let color = $ref('#000');
|
let color = $ref('#000');
|
||||||
|
let isSensitive = $ref(false);
|
||||||
const pinnedNotes = ref([]);
|
const pinnedNotes = ref([]);
|
||||||
|
|
||||||
watch(() => bannerId, async () => {
|
watch(() => bannerId, async () => {
|
||||||
|
@ -110,6 +116,7 @@ async function fetchChannel() {
|
||||||
description = channel.description;
|
description = channel.description;
|
||||||
bannerId = channel.bannerId;
|
bannerId = channel.bannerId;
|
||||||
bannerUrl = channel.bannerUrl;
|
bannerUrl = channel.bannerUrl;
|
||||||
|
isSensitive = channel.isSensitive;
|
||||||
pinnedNotes.value = channel.pinnedNoteIds.map(id => ({
|
pinnedNotes.value = channel.pinnedNoteIds.map(id => ({
|
||||||
id,
|
id,
|
||||||
}));
|
}));
|
||||||
|
@ -142,6 +149,7 @@ function save() {
|
||||||
bannerId: bannerId,
|
bannerId: bannerId,
|
||||||
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
|
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
|
||||||
color: color,
|
color: color,
|
||||||
|
isSensitive: isSensitive,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.channelId) {
|
if (props.channelId) {
|
||||||
|
|
|
@ -17,6 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
<div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
||||||
<div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
<div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
|
||||||
<div :class="$style.bannerFade"></div>
|
<div :class="$style.bannerFade"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="channel.description" :class="$style.description">
|
<div v-if="channel.description" :class="$style.description">
|
||||||
|
@ -274,4 +275,17 @@ definePageMetadata(computed(() => channel ? {
|
||||||
.description {
|
.description {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sensitiveIndicator {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: 16px;
|
||||||
|
left: 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: var(--warn);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 4px 7px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue