diff --git a/CHANGELOG.md b/CHANGELOG.md index ab09b9a760..e655bef8b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ - Feat: ユーザーごとのハイライト - Feat: プライバシーポリシー・運営者情報(Impressum)の指定が可能になりました - プライバシーポリシーはサーバー登録時に同意確認が入ります +- Feat: タイムラインがリアルタイム更新中に広告を挿入できるようになりました + - デフォルトは無効 + - 頻度はコントロールパネルから設定できます。運営中のサーバーのTLの流速を見て、最適な値を指定してください。 - Enhance: ソフトワードミュートとハードワードミュートは統合されました - Enhance: モデレーションログ機能の強化 - Enhance: ローカリゼーションの更新 diff --git a/locales/index.d.ts b/locales/index.d.ts index d90f8fa6f2..8a429e3b66 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1627,6 +1627,10 @@ export interface Locale { "reduceFrequencyOfThisAd": string; "hide": string; "timezoneinfo": string; + "adsSettings": string; + "notesPerOneAd": string; + "setZeroToDisable": string; + "adsTooClose": string; }; "_forgotPassword": { "enterEmail": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c92d834366..52e06e720d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1546,6 +1546,10 @@ _ad: reduceFrequencyOfThisAd: "この広告の表示頻度を下げる" hide: "表示しない" timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。" + adsSettings: "広告配信設定" + notesPerOneAd: "リアルタイム更新中に広告を配信する間隔(ノートの個数)" + setZeroToDisable: "0でリアルタイム更新時の広告配信を無効" + adsTooClose: "広告の配信間隔が極めて短いため、ユーザー体験が著しく損われる可能性があります。" _forgotPassword: enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" diff --git a/packages/backend/migration/1696743032098-AdsOnStream.js b/packages/backend/migration/1696743032098-AdsOnStream.js new file mode 100644 index 0000000000..c86ee84883 --- /dev/null +++ b/packages/backend/migration/1696743032098-AdsOnStream.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AdsOnStream1696743032098 { + name = 'AdsOnStream1696743032098' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "notesPerOneAd" integer NOT NULL DEFAULT '0'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "notesPerOneAd"`); + } +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 60fdaab54a..d2bd0c26e9 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -503,4 +503,9 @@ export class MiMeta { default: 300, }) public perUserListTimelineCacheMax: number; + + @Column('integer', { + default: 0, + }) + public notesPerOneAd: number; } diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index ddca5b4709..5a74456ab0 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -297,6 +297,10 @@ export const meta = { type: 'number', optional: false, nullable: false, }, + notesPerOneAd: { + type: 'number', + optional: false, nullable: false, + }, }, }, } as const; @@ -408,6 +412,7 @@ export default class extends Endpoint { // eslint- perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, + notesPerOneAd: instance.notesPerOneAd, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index ee7c564e23..7db25e659f 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -114,6 +114,7 @@ export const paramDef = { perRemoteUserUserTimelineCacheMax: { type: 'integer' }, perUserHomeTimelineCacheMax: { type: 'integer' }, perUserListTimelineCacheMax: { type: 'integer' }, + notesPerOneAd: { type: 'integer' }, }, required: [], } as const; @@ -471,6 +472,10 @@ export default class extends Endpoint { // eslint- set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax; } + if (ps.notesPerOneAd !== undefined) { + set.notesPerOneAd = ps.notesPerOneAd; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index e7c8d26827..2727e4f093 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -181,6 +181,11 @@ export const meta = { }, }, }, + notesPerOneAd: { + type: 'number', + optional: false, nullable: false, + default: 0, + }, requireSetup: { type: 'boolean', optional: false, nullable: false, @@ -331,6 +336,7 @@ export default class extends Endpoint { // eslint- imageUrl: ad.imageUrl, dayOfWeek: ad.dayOfWeek, })), + notesPerOneAd: instance.notesPerOneAd, enableEmail: instance.enableEmail, enableServiceWorker: instance.enableServiceWorker, diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index 315ce958c5..9490d80428 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -23,6 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only :spellcheck="spellcheck" :step="step" :list="id" + :min="min" + :max="max" @focus="focused = true" @blur="focused = false" @keydown="onKeydown($event)" @@ -59,6 +61,8 @@ const props = defineProps<{ spellcheck?: boolean; step?: any; datalist?: string[]; + min?: string; + max?: string; inline?: boolean; debounce?: boolean; manualSave?: boolean; diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index c4a34667ef..4e71c048b2 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -13,6 +13,7 @@ import MkNotes from '@/components/MkNotes.vue'; import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; import { $i } from '@/account.js'; +import { instance } from '@/instance.js'; import { defaultStore } from '@/store.js'; const props = withDefaults(defineProps<{ @@ -38,7 +39,15 @@ provide('inChannel', computed(() => props.src === 'channel')); const tlComponent: InstanceType = $ref(); +let tlNotesCount = 0; + const prepend = note => { + tlNotesCount++; + + if (instance.notesPerOneAd > 0 && tlNotesCount % instance.notesPerOneAd === 0) { + note._shouldInsertAd_ = true; + } + tlComponent.pagingComponent?.prepend(note); emit('note'); diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 2e3f1611dc..2db0dd5c3a 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -107,6 +107,22 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + +
+
+ + + + + + {{ i18n.ts._ad.adsTooClose }} + +
+
+
@@ -127,6 +143,7 @@ import XHeader from './_header_.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; +import MkInfo from '@/components/MkInfo.vue'; import FormSection from '@/components/form/section.vue'; import FormSplit from '@/components/form/split.vue'; import FormSuspense from '@/components/form/suspense.vue'; @@ -152,6 +169,7 @@ let perLocalUserUserTimelineCacheMax: number = $ref(0); let perRemoteUserUserTimelineCacheMax: number = $ref(0); let perUserHomeTimelineCacheMax: number = $ref(0); let perUserListTimelineCacheMax: number = $ref(0); +let notesPerOneAd: number = $ref(0); async function init(): Promise { const meta = await os.api('admin/meta'); @@ -171,10 +189,11 @@ async function init(): Promise { perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax; perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax; perUserListTimelineCacheMax = meta.perUserListTimelineCacheMax; + notesPerOneAd = meta.notesPerOneAd; } -function save(): void { - os.apiWithDialog('admin/update-meta', { +async function save(): void { + await os.apiWithDialog('admin/update-meta', { name, shortName: shortName === '' ? null : shortName, description, @@ -191,9 +210,10 @@ function save(): void { perRemoteUserUserTimelineCacheMax, perUserHomeTimelineCacheMax, perUserListTimelineCacheMax, - }).then(() => { - fetchInstance(); + notesPerOneAd, }); + + fetchInstance(); } const headerTabs = $computed(() => []); diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index fe913886c7..09662073ed 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2448,6 +2448,7 @@ type LiteInstanceMetadata = { url: string; imageUrl: string; }[]; + notesPerOneAd: number; translatorAvailable: boolean; serverRules: string[]; }; @@ -2980,7 +2981,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts // src/api.types.ts:630:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/entities.ts:107:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts -// src/entities.ts:596:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts +// src/entities.ts:597:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index b60056e21a..b02f3e1f9b 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -362,6 +362,7 @@ export type LiteInstanceMetadata = { url: string; imageUrl: string; }[]; + notesPerOneAd: number; translatorAvailable: boolean; serverRules: string[]; };