diff --git a/locales/en-US.yml b/locales/en-US.yml index c6c442df7..b7248e033 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1114,6 +1114,9 @@ isPatron: "Calckey Patron" reactionPickerSkinTone: "Preferred emoji skin tone" enableServerMachineStats: "Enable server hardware statistics" enableIdenticonGeneration: "Enable Identicon generation" +showPopup: "Notify users with popup" +showWithSparkles: "Show with sparkles" +youHaveUnreadAnnouncements: "You have unread announcements" _sensitiveMediaDetection: description: "Reduces the effort of server moderation through automatically recognizing diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 05c4f3ab3..95fb7bcf8 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -980,6 +980,9 @@ preventAiLearningDescription: "投稿したノート、添付した画像など noGraze: "ブラウザの拡張機能「Graze for Mastodon」は、Calckeyの動作を妨げるため、無効にしてください。" enableServerMachineStats: "サーバーのマシン情報を公開する" enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする" +showPopup: "ポップアップを表示してユーザーに知らせる" +showWithSparkles: "タイトルをキラキラさせる" +youHaveUnreadAnnouncements: "未読のお知らせがあります" _sensitiveMediaDetection: description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。" diff --git a/packages/backend/migration/1688845537045-announcement-popup.js b/packages/backend/migration/1688845537045-announcement-popup.js new file mode 100644 index 000000000..196590d3e --- /dev/null +++ b/packages/backend/migration/1688845537045-announcement-popup.js @@ -0,0 +1,21 @@ +export class AnnouncementPopup1688845537045 { + name = "AnnouncementPopup1688845537045"; + + async up(queryRunner) { + await queryRunner.query( + `ALTER TABLE "announcement" ADD "showPopup" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "announcement" ADD "isGoodNews" boolean NOT NULL DEFAULT false`, + ); + } + + async down(queryRunner) { + await queryRunner.query( + `ALTER TABLE "announcement" DROP COLUMN "isGoodNews"`, + ); + await queryRunner.query( + `ALTER TABLE "announcement" DROP COLUMN "showPopup"`, + ); + } +} diff --git a/packages/backend/src/models/entities/announcement.ts b/packages/backend/src/models/entities/announcement.ts index 1939604b9..7872c0fe1 100644 --- a/packages/backend/src/models/entities/announcement.ts +++ b/packages/backend/src/models/entities/announcement.ts @@ -36,6 +36,16 @@ export class Announcement { }) public imageUrl: string | null; + @Column("boolean", { + default: false, + }) + public showPopup: boolean; + + @Column("boolean", { + default: false, + }) + public isGoodNews: boolean; + constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index a532b6677..754cc6c89 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -47,6 +47,16 @@ export const meta = { optional: false, nullable: true, }, + showPopup: { + type: "boolean", + optional: true, + nullable: false, + }, + isGoodNews: { + type: "boolean", + optional: true, + nullable: false, + }, }, }, } as const; @@ -57,6 +67,8 @@ export const paramDef = { title: { type: "string", minLength: 1 }, text: { type: "string", minLength: 1 }, imageUrl: { type: "string", nullable: true, minLength: 1 }, + showPopup: { type: "boolean" }, + isGoodNews: { type: "boolean" }, }, required: ["title", "text", "imageUrl"], } as const; @@ -69,6 +81,8 @@ export default define(meta, paramDef, async (ps) => { title: ps.title, text: ps.text, imageUrl: ps.imageUrl, + showPopup: ps.showPopup ?? false, + isGoodNews: ps.isGoodNews ?? false, }).then((x) => Announcements.findOneByOrFail(x.identifiers[0])); return Object.assign({}, announcement, { diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index fc5b02070..e96517c68 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -57,6 +57,16 @@ export const meta = { optional: false, nullable: false, }, + showPopup: { + type: "boolean", + optional: true, + nullable: false, + }, + isGoodNews: { + type: "boolean", + optional: true, + nullable: false, + }, }, }, }, @@ -100,5 +110,7 @@ export default define(meta, paramDef, async (ps) => { text: announcement.text, imageUrl: announcement.imageUrl, reads: reads.get(announcement)!, + showPopup: announcement.showPopup, + isGoodNews: announcement.isGoodNews, })); }); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index 35e64f281..616b94d69 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -24,6 +24,8 @@ export const paramDef = { title: { type: "string", minLength: 1 }, text: { type: "string", minLength: 1 }, imageUrl: { type: "string", nullable: true, minLength: 1 }, + showPopup: { type: "boolean" }, + isGoodNews: { type: "boolean" }, }, required: ["id", "title", "text", "imageUrl"], } as const; @@ -38,5 +40,7 @@ export default define(meta, paramDef, async (ps, me) => { title: ps.title, text: ps.text, imageUrl: ps.imageUrl, + showPopup: ps.showPopup ?? false, + isGoodNews: ps.isGoodNews ?? false, }); }); diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index 00634cc42..1bab61ba2 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -56,6 +56,16 @@ export const meta = { optional: true, nullable: false, }, + showPopup: { + type: "boolean", + optional: false, + nullable: false, + }, + isGoodNews: { + type: "boolean", + optional: false, + nullable: false, + }, }, }, }, diff --git a/packages/client/src/components/MkAnnouncement.vue b/packages/client/src/components/MkAnnouncement.vue new file mode 100644 index 000000000..24bf886dd --- /dev/null +++ b/packages/client/src/components/MkAnnouncement.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/packages/client/src/components/MkManyAnnouncements.vue b/packages/client/src/components/MkManyAnnouncements.vue new file mode 100644 index 000000000..5c8b225de --- /dev/null +++ b/packages/client/src/components/MkManyAnnouncements.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts index 1c68b87c9..11b9a2eda 100644 --- a/packages/client/src/init.ts +++ b/packages/client/src/init.ts @@ -36,7 +36,7 @@ import { version, ui, lang, host } from "@/config"; import { applyTheme } from "@/scripts/theme"; import { isDeviceDarkmode } from "@/scripts/is-device-darkmode"; import { i18n } from "@/i18n"; -import { confirm, alert, post, popup, toast } from "@/os"; +import { confirm, alert, post, popup, toast, api } from "@/os"; import { stream } from "@/stream"; import * as sound from "@/scripts/sound"; import { $i, refreshAccount, login, updateAccount, signout } from "@/account"; @@ -272,6 +272,42 @@ function checkForSplash() { } } + if ( + $i && + defaultStore.state.tutorial === -1 && + !["/announcements", "/announcements/"].includes(window.location.pathname) + ) { + api("announcements", { withUnreads: true, limit: 10 }) + .then((announcements) => { + const unreadAnnouncements = announcements.filter((item) => { + return !item.isRead; + }); + if (unreadAnnouncements.length > 3) { + popup( + defineAsyncComponent( + () => import("@/components/MkManyAnnouncements.vue"), + ), + {}, + {}, + "closed", + ); + } else { + unreadAnnouncements.forEach((item) => { + if (item.showPopup) + popup( + defineAsyncComponent( + () => import("@/components/MkAnnouncement.vue"), + ), + { announcement: item }, + {}, + "closed", + ); + }); + } + }) + .catch((err) => console.log(err)); + } + // NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため) watch( defaultStore.reactiveState.darkMode, diff --git a/packages/client/src/pages/admin/announcements.vue b/packages/client/src/pages/admin/announcements.vue index 9853158de..b2172b307 100644 --- a/packages/client/src/pages/admin/announcements.vue +++ b/packages/client/src/pages/admin/announcements.vue @@ -7,7 +7,7 @@ :display-back-button="true" /> -
+
+ {{ i18n.ts.showPopup }} + {{ i18n.ts.showWithSparkles }}

{{ i18n.t("nUsersRead", { n: announcement.reads }) @@ -57,6 +68,7 @@ import {} from "vue"; import MkButton from "@/components/MkButton.vue"; import MkInput from "@/components/form/input.vue"; +import MkSwitch from "@/components/form/switch.vue"; import MkTextarea from "@/components/form/textarea.vue"; import * as os from "@/os"; import { i18n } from "@/i18n"; @@ -74,6 +86,8 @@ function add() { title: "", text: "", imageUrl: null, + showPopup: false, + isGoodNews: false, }); } @@ -137,8 +151,8 @@ definePageMetadata({ }); -