From 73d83591cacf2e1acdb41e7750ed60ea25728af8 Mon Sep 17 00:00:00 2001 From: yukineko <27853966+hideki0403@users.noreply.github.com> Date: Tue, 19 Mar 2024 23:38:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=81=E3=83=A3=E3=83=B3=E3=83=8D?= =?UTF-8?q?=E3=83=AB=E5=86=85=E3=81=8A=E7=9F=A5=E3=82=89=E3=81=9B=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=20(#138)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: チャンネル内お知らせ機能のbackend側を実装 * update: 型定義を更新 * fix: channel.annoucementのjson-schemaがoptionalを許容していたのを修正 * fix: 誤字修正 * fix: チャンネル作成時にお知らせを設定出来ないようになっていたのを修正 * feat: クライアント側を実装 --- locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + .../1710854614048-feat-channnel-announcement.js | 11 +++++++++++ .../src/core/entities/ChannelEntityService.ts | 1 + packages/backend/src/models/Channel.ts | 6 ++++++ .../backend/src/models/json-schema/channel.ts | 4 ++++ .../src/server/api/endpoints/channels/create.ts | 2 ++ .../src/server/api/endpoints/channels/update.ts | 2 ++ packages/frontend/src/pages/channel-editor.vue | 8 ++++++++ packages/frontend/src/pages/channel.vue | 15 +++++++++++++++ packages/misskey-js/src/autogen/types.ts | 2 ++ 11 files changed, 56 insertions(+) create mode 100644 packages/backend/migration/1710854614048-feat-channnel-announcement.js diff --git a/locales/index.d.ts b/locales/index.d.ts index 5370573f89b7..f2a565174eef 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5280,6 +5280,10 @@ export interface Locale extends ILocale { * 自分のプロフィールのアクティビティ (概要/アクティビティタブ) を他人が見れないようにします。このオプションを有効にしても、自分であればプロフィールのアクティビティタブから引き続き閲覧できます。 */ "hideActivityDescription": string; + /** + * このお知らせはチャンネルのタイムライン上部に表示されます。最初の1行がタイトルとして表示され、2行目以降はお知らせをタップすることで表示されるようになります。 + */ + "channelAnnouncementDescription": string; "_bubbleGame": { /** * 遊び方 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b2778245e959..1ebeab52c499 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1316,6 +1316,7 @@ scheduledNoteDelete: "ノートの自己消滅" noteDeletationAt: "このノートは{time}に消滅します" hideActivity: "アクティビティを非公開にする" hideActivityDescription: "自分のプロフィールのアクティビティ (概要/アクティビティタブ) を他人が見れないようにします。このオプションを有効にしても、自分であればプロフィールのアクティビティタブから引き続き閲覧できます。" +channelAnnouncementDescription: "このお知らせはチャンネルのタイムライン上部に表示されます。最初の1行がタイトルとして表示され、2行目以降はお知らせをタップすることで表示されるようになります。" _bubbleGame: howToPlay: "遊び方" diff --git a/packages/backend/migration/1710854614048-feat-channnel-announcement.js b/packages/backend/migration/1710854614048-feat-channnel-announcement.js new file mode 100644 index 000000000000..6a8254fad0c2 --- /dev/null +++ b/packages/backend/migration/1710854614048-feat-channnel-announcement.js @@ -0,0 +1,11 @@ +export class FeatChannnelAnnouncement1710854614048 { + name = 'FeatChannnelAnnouncement1710854614048' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "channel" ADD "announcement" character varying(2048)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "announcement"`); + } +} diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts index 1ba7ca8e57e7..a0347b6fdeec 100644 --- a/packages/backend/src/core/entities/ChannelEntityService.ts +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -95,6 +95,7 @@ export class ChannelEntityService { ...(detailed ? { pinnedNotes: (await this.noteEntityService.packMany(pinnedNotes, me)).sort((a, b) => channel.pinnedNoteIds.indexOf(a.id) - channel.pinnedNoteIds.indexOf(b.id)), + announcement: channel.announcement, } : {}), }; } diff --git a/packages/backend/src/models/Channel.ts b/packages/backend/src/models/Channel.ts index f5e9b17e3e1e..a629252b5e3a 100644 --- a/packages/backend/src/models/Channel.ts +++ b/packages/backend/src/models/Channel.ts @@ -63,6 +63,12 @@ export class MiChannel { }) public pinnedNoteIds: string[]; + @Column('varchar', { + length: 2048, + nullable: true, + }) + public announcement: string | null; + @Column('varchar', { length: 16, default: '#86b300', diff --git a/packages/backend/src/models/json-schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts index d233f7858d9a..8d462777ed4a 100644 --- a/packages/backend/src/models/json-schema/channel.ts +++ b/packages/backend/src/models/json-schema/channel.ts @@ -89,5 +89,9 @@ export const packedChannelSchema = { ref: 'Note', }, }, + announcement: { + type: 'string', + optional: false, nullable: true, + }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index 2866db54240c..269f65392fa9 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -47,6 +47,7 @@ export const paramDef = { properties: { name: { type: 'string', minLength: 1, maxLength: 128 }, description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, + announcement: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, bannerId: { type: 'string', format: 'misskey:id', nullable: true }, color: { type: 'string', minLength: 1, maxLength: 16 }, isSensitive: { type: 'boolean', nullable: true }, @@ -89,6 +90,7 @@ export default class extends Endpoint { // eslint- isSensitive: ps.isSensitive ?? false, ...(ps.color !== undefined ? { color: ps.color } : {}), allowRenoteToExternal: ps.allowRenoteToExternal ?? true, + announcement: ps.announcement ?? null, } as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0])); return await this.channelEntityService.pack(channel, me); diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index dba2938b3993..15f4a725ecff 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -59,6 +59,7 @@ export const paramDef = { type: 'string', format: 'misskey:id', }, }, + announcement: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, color: { type: 'string', minLength: 1, maxLength: 16 }, isSensitive: { type: 'boolean', nullable: true }, allowRenoteToExternal: { type: 'boolean', nullable: true }, @@ -112,6 +113,7 @@ export default class extends Endpoint { // eslint- ...(ps.name !== undefined ? { name: ps.name } : {}), ...(ps.description !== undefined ? { description: ps.description } : {}), ...(ps.pinnedNoteIds !== undefined ? { pinnedNoteIds: ps.pinnedNoteIds } : {}), + ...(ps.announcement !== undefined ? { announcement: ps.announcement } : {}), ...(ps.color !== undefined ? { color: ps.color } : {}), ...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}), ...(banner ? { bannerId: banner.id } : {}), diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index d3f4a65b8932..696d37636858 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -16,6 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + @@ -101,6 +106,7 @@ const color = ref('#000'); const isSensitive = ref(false); const allowRenoteToExternal = ref(true); const pinnedNotes = ref<{ id: Misskey.entities.Note['id'] }[]>([]); +const announcement = ref(null); watch(() => bannerId.value, async () => { if (bannerId.value == null) { @@ -127,6 +133,7 @@ async function fetchChannel() { pinnedNotes.value = channel.value.pinnedNoteIds.map(id => ({ id, })); + announcement.value = channel.value.announcement; color.value = channel.value.color; allowRenoteToExternal.value = channel.value.allowRenoteToExternal; } @@ -156,6 +163,7 @@ function save() { description: description.value, bannerId: bannerId.value, pinnedNoteIds: pinnedNotes.value.map(x => x.id), + announcement: announcement.value, color: color.value, isSensitive: isSensitive.value, allowRenoteToExternal: allowRenoteToExternal.value, diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 611ae6feca0f..6f9ce063f549 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -36,6 +36,8 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.thisChannelArchived }} + {{ channel.announcement.split('\n')[0] }} ({{ i18n.ts.clickToShow }}) + @@ -240,6 +242,15 @@ const headerActions = computed(() => { } }); +function showAnnouncement() { + if (!channel.value?.announcement) return; + const announce = channel.value.announcement.split('\n'); + os.alert({ + title: announce.shift(), + text: announce.join('\n'), + }); +} + const headerTabs = computed(() => [{ key: 'overview', title: i18n.ts.overview, @@ -338,4 +349,8 @@ definePageMetadata(() => ({ font-size: 1em; padding: 4px 7px; } + +.clickable { + cursor: pointer; +} diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 797f87194f5c..ef564ec35ebd 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4520,6 +4520,7 @@ export type components = { isFollowing?: boolean; isFavorited?: boolean; pinnedNotes?: components['schemas']['Note'][]; + annoucement: string | null; }; QueueCount: { waiting: number; @@ -11317,6 +11318,7 @@ export type operations = { bannerId?: string | null; isArchived?: boolean | null; pinnedNoteIds?: string[]; + announcement?: string | null; color?: string; isSensitive?: boolean | null; allowRenoteToExternal?: boolean | null;