feat: チャンネルに色を設定できるように
This commit is contained in:
parent
0cbdbf24f1
commit
d535ec21a2
11 changed files with 61 additions and 1 deletions
|
@ -25,6 +25,7 @@
|
|||
(デスクトップ表示ではusernameの右側のボタンからも追加可能)
|
||||
- アカウントの引っ越し(フォロワー引き継ぎ)に対応
|
||||
* 一度引っ越したアカウントは利用に制限がかかります
|
||||
- チャンネルに色を設定できるようになりました。各ノートに設定した色のインジケーターが表示されます。
|
||||
- ロールタイムラインをロールごとに表示するかどうかの選択できるようになりました。
|
||||
* デフォルトがオフになるので、ロールタイムラインを表示する場合はオンにしてください。
|
||||
- カスタム絵文字のライセンスを複数でセットできるようになりました。
|
||||
|
|
11
packages/backend/migration/1682985520254-channelColor.js
Normal file
11
packages/backend/migration/1682985520254-channelColor.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export class ChannelColor1682985520254 {
|
||||
name = 'ChannelColor1682985520254'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "channel" ADD "color" character varying(16) NOT NULL DEFAULT '#86b300'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "color"`);
|
||||
}
|
||||
}
|
|
@ -74,6 +74,7 @@ export class ChannelEntityService {
|
|||
userId: channel.userId,
|
||||
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
||||
pinnedNoteIds: channel.pinnedNoteIds,
|
||||
color: channel.color,
|
||||
usersCount: channel.usersCount,
|
||||
notesCount: channel.notesCount,
|
||||
|
||||
|
|
|
@ -335,6 +335,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
channel: channel ? {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
color: channel.color,
|
||||
} : undefined,
|
||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||
uri: note.uri ?? undefined,
|
||||
|
|
|
@ -64,6 +64,12 @@ export class Channel {
|
|||
})
|
||||
public pinnedNoteIds: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 16,
|
||||
default: '#86b300',
|
||||
})
|
||||
public color: string;
|
||||
|
||||
@Index()
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
|
|
|
@ -59,5 +59,9 @@ export const packedChannelSchema = {
|
|||
format: 'id',
|
||||
},
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -43,6 +43,7 @@ export const paramDef = {
|
|||
name: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
|
||||
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
color: { type: 'string', minLength: 1, maxLength: 16 },
|
||||
},
|
||||
required: ['name'],
|
||||
} as const;
|
||||
|
@ -80,6 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
name: ps.name,
|
||||
description: ps.description ?? null,
|
||||
bannerId: banner ? banner.id : null,
|
||||
...(ps.color !== undefined ? { color: ps.color } : {}),
|
||||
} as Channel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
return await this.channelEntityService.pack(channel, me);
|
||||
|
|
|
@ -53,6 +53,7 @@ export const paramDef = {
|
|||
type: 'string', format: 'misskey:id',
|
||||
},
|
||||
},
|
||||
color: { type: 'string', minLength: 1, maxLength: 16 },
|
||||
},
|
||||
required: ['channelId'],
|
||||
} as const;
|
||||
|
@ -104,6 +105,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
...(ps.name !== undefined ? { name: ps.name } : {}),
|
||||
...(ps.description !== undefined ? { description: ps.description } : {}),
|
||||
...(ps.pinnedNoteIds !== undefined ? { pinnedNoteIds: ps.pinnedNoteIds } : {}),
|
||||
...(ps.color !== undefined ? { color: ps.color } : {}),
|
||||
...(banner ? { bannerId: banner.id } : {}),
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<div :class="$style.label"><slot name="label"></slot></div>
|
||||
<div :class="[$style.input, { disabled, focused }]">
|
||||
<div :class="[$style.input, { disabled }]">
|
||||
<input
|
||||
ref="inputEl"
|
||||
v-model="v"
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>-->
|
||||
<!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>-->
|
||||
<div v-if="isRenote" :class="$style.renote">
|
||||
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
||||
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
|
||||
<i class="ti ti-repeat" style="margin-right: 4px;"></i>
|
||||
<I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText">
|
||||
|
@ -40,6 +41,7 @@
|
|||
<Mfm :text="getNoteSummary(appearNote)" :plain="true" :nowrap="true" :author="appearNote.user" :class="$style.collapsedRenoteTargetText" @click="renoteCollapsed = false"/>
|
||||
</div>
|
||||
<article v-else :class="$style.article" @contextmenu.stop="onContextmenu">
|
||||
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
|
||||
<MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/>
|
||||
<div :class="$style.main">
|
||||
<MkNoteHeader :class="$style.header" :note="appearNote" :mini="true"/>
|
||||
|
@ -546,6 +548,7 @@ function showReactions(): void {
|
|||
}
|
||||
|
||||
.renote {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 32px 8px 32px;
|
||||
|
@ -556,6 +559,10 @@ function showReactions(): void {
|
|||
& + .article {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
> .colorBar {
|
||||
height: calc(100% - 6px);
|
||||
}
|
||||
}
|
||||
|
||||
.renoteAvatar {
|
||||
|
@ -627,6 +634,16 @@ function showReactions(): void {
|
|||
padding: 28px 32px;
|
||||
}
|
||||
|
||||
.colorBar {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 5px;
|
||||
height: calc(100% - 16px);
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
display: block !important;
|
||||
|
@ -842,6 +859,13 @@ function showReactions(): void {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colorBar {
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
width: 4px;
|
||||
height: calc(100% - 12px);
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 300px) {
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkColorInput v-model="color">
|
||||
<template #label>{{ i18n.ts.color }}</template>
|
||||
</MkColorInput>
|
||||
|
||||
<div>
|
||||
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
|
||||
<div v-else-if="bannerUrl">
|
||||
|
@ -55,6 +59,7 @@ import { computed, ref, watch, defineAsyncComponent } from 'vue';
|
|||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkColorInput from '@/components/MkColorInput.vue';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
import { useRouter } from '@/router';
|
||||
|
@ -75,6 +80,7 @@ let name = $ref(null);
|
|||
let description = $ref(null);
|
||||
let bannerUrl = $ref<string | null>(null);
|
||||
let bannerId = $ref<string | null>(null);
|
||||
let color = $ref(null);
|
||||
const pinnedNotes = ref([]);
|
||||
|
||||
watch(() => bannerId, async () => {
|
||||
|
@ -101,6 +107,7 @@ async function fetchChannel() {
|
|||
pinnedNotes.value = channel.pinnedNoteIds.map(id => ({
|
||||
id,
|
||||
}));
|
||||
color = channel.color;
|
||||
}
|
||||
|
||||
fetchChannel();
|
||||
|
@ -128,6 +135,7 @@ function save() {
|
|||
description: description,
|
||||
bannerId: bannerId,
|
||||
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
|
||||
color: color,
|
||||
};
|
||||
|
||||
if (props.channelId) {
|
||||
|
|
Loading…
Reference in a new issue