Feat: タイムライン更新中に広告を挿入 (#11989)

* Feat: タイムライン更新中に広告を挿入

* 翻訳を変更

* Run api extractor

* fix api extractor

* Update locales/ja-JP.yml

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* confirm -> mkinfo

* MkInputにmin, maxを指定できるように

* 負の値が指定されたら何もしない

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
かっこかり 2023-10-08 17:56:44 +09:00 committed by GitHub
parent 9240db35f3
commit 4bbfc98883
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 88 additions and 5 deletions

View file

@ -28,6 +28,9 @@
- Feat: ユーザーごとのハイライト - Feat: ユーザーごとのハイライト
- Feat: プライバシーポリシー・運営者情報Impressumの指定が可能になりました - Feat: プライバシーポリシー・運営者情報Impressumの指定が可能になりました
- プライバシーポリシーはサーバー登録時に同意確認が入ります - プライバシーポリシーはサーバー登録時に同意確認が入ります
- Feat: タイムラインがリアルタイム更新中に広告を挿入できるようになりました
- デフォルトは無効
- 頻度はコントロールパネルから設定できます。運営中のサーバーのTLの流速を見て、最適な値を指定してください。
- Enhance: ソフトワードミュートとハードワードミュートは統合されました - Enhance: ソフトワードミュートとハードワードミュートは統合されました
- Enhance: モデレーションログ機能の強化 - Enhance: モデレーションログ機能の強化
- Enhance: ローカリゼーションの更新 - Enhance: ローカリゼーションの更新

4
locales/index.d.ts vendored
View file

@ -1627,6 +1627,10 @@ export interface Locale {
"reduceFrequencyOfThisAd": string; "reduceFrequencyOfThisAd": string;
"hide": string; "hide": string;
"timezoneinfo": string; "timezoneinfo": string;
"adsSettings": string;
"notesPerOneAd": string;
"setZeroToDisable": string;
"adsTooClose": string;
}; };
"_forgotPassword": { "_forgotPassword": {
"enterEmail": string; "enterEmail": string;

View file

@ -1546,6 +1546,10 @@ _ad:
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる" reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
hide: "表示しない" hide: "表示しない"
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。" timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。"
adsSettings: "広告配信設定"
notesPerOneAd: "リアルタイム更新中に広告を配信する間隔(ノートの個数)"
setZeroToDisable: "0でリアルタイム更新時の広告配信を無効"
adsTooClose: "広告の配信間隔が極めて短いため、ユーザー体験が著しく損われる可能性があります。"
_forgotPassword: _forgotPassword:
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"

View file

@ -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"`);
}
}

View file

@ -503,4 +503,9 @@ export class MiMeta {
default: 300, default: 300,
}) })
public perUserListTimelineCacheMax: number; public perUserListTimelineCacheMax: number;
@Column('integer', {
default: 0,
})
public notesPerOneAd: number;
} }

View file

@ -297,6 +297,10 @@ export const meta = {
type: 'number', type: 'number',
optional: false, nullable: false, optional: false, nullable: false,
}, },
notesPerOneAd: {
type: 'number',
optional: false, nullable: false,
},
}, },
}, },
} as const; } as const;
@ -408,6 +412,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
notesPerOneAd: instance.notesPerOneAd,
}; };
}); });
} }

View file

@ -114,6 +114,7 @@ export const paramDef = {
perRemoteUserUserTimelineCacheMax: { type: 'integer' }, perRemoteUserUserTimelineCacheMax: { type: 'integer' },
perUserHomeTimelineCacheMax: { type: 'integer' }, perUserHomeTimelineCacheMax: { type: 'integer' },
perUserListTimelineCacheMax: { type: 'integer' }, perUserListTimelineCacheMax: { type: 'integer' },
notesPerOneAd: { type: 'integer' },
}, },
required: [], required: [],
} as const; } as const;
@ -471,6 +472,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax; set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax;
} }
if (ps.notesPerOneAd !== undefined) {
set.notesPerOneAd = ps.notesPerOneAd;
}
const before = await this.metaService.fetch(true); const before = await this.metaService.fetch(true);
await this.metaService.update(set); await this.metaService.update(set);

View file

@ -181,6 +181,11 @@ export const meta = {
}, },
}, },
}, },
notesPerOneAd: {
type: 'number',
optional: false, nullable: false,
default: 0,
},
requireSetup: { requireSetup: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@ -331,6 +336,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
imageUrl: ad.imageUrl, imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek, dayOfWeek: ad.dayOfWeek,
})), })),
notesPerOneAd: instance.notesPerOneAd,
enableEmail: instance.enableEmail, enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker, enableServiceWorker: instance.enableServiceWorker,

View file

@ -23,6 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
:spellcheck="spellcheck" :spellcheck="spellcheck"
:step="step" :step="step"
:list="id" :list="id"
:min="min"
:max="max"
@focus="focused = true" @focus="focused = true"
@blur="focused = false" @blur="focused = false"
@keydown="onKeydown($event)" @keydown="onKeydown($event)"
@ -59,6 +61,8 @@ const props = defineProps<{
spellcheck?: boolean; spellcheck?: boolean;
step?: any; step?: any;
datalist?: string[]; datalist?: string[];
min?: string;
max?: string;
inline?: boolean; inline?: boolean;
debounce?: boolean; debounce?: boolean;
manualSave?: boolean; manualSave?: boolean;

View file

@ -13,6 +13,7 @@ import MkNotes from '@/components/MkNotes.vue';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { instance } from '@/instance.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@ -38,7 +39,15 @@ provide('inChannel', computed(() => props.src === 'channel'));
const tlComponent: InstanceType<typeof MkNotes> = $ref(); const tlComponent: InstanceType<typeof MkNotes> = $ref();
let tlNotesCount = 0;
const prepend = note => { const prepend = note => {
tlNotesCount++;
if (instance.notesPerOneAd > 0 && tlNotesCount % instance.notesPerOneAd === 0) {
note._shouldInsertAd_ = true;
}
tlComponent.pagingComponent?.prepend(note); tlComponent.pagingComponent?.prepend(note);
emit('note'); emit('note');

View file

@ -107,6 +107,22 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput> </MkInput>
</div> </div>
</FormSection> </FormSection>
<FormSection>
<template #label>{{ i18n.ts._ad.adsSettings }}</template>
<div class="_gaps_m">
<div class="_gaps_s">
<MkInput v-model="notesPerOneAd" min="0" type="number">
<template #label>{{ i18n.ts._ad.notesPerOneAd }}</template>
<template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
</MkInput>
<MkInfo v-if="notesPerOneAd > 0 && notesPerOneAd < 20" :warn="true">
{{ i18n.ts._ad.adsTooClose }}
</MkInfo>
</div>
</div>
</FormSection>
</div> </div>
</FormSuspense> </FormSuspense>
</MkSpacer> </MkSpacer>
@ -127,6 +143,7 @@ import XHeader from './_header_.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import MkInfo from '@/components/MkInfo.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
@ -152,6 +169,7 @@ let perLocalUserUserTimelineCacheMax: number = $ref(0);
let perRemoteUserUserTimelineCacheMax: number = $ref(0); let perRemoteUserUserTimelineCacheMax: number = $ref(0);
let perUserHomeTimelineCacheMax: number = $ref(0); let perUserHomeTimelineCacheMax: number = $ref(0);
let perUserListTimelineCacheMax: number = $ref(0); let perUserListTimelineCacheMax: number = $ref(0);
let notesPerOneAd: number = $ref(0);
async function init(): Promise<void> { async function init(): Promise<void> {
const meta = await os.api('admin/meta'); const meta = await os.api('admin/meta');
@ -171,10 +189,11 @@ async function init(): Promise<void> {
perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax; perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax;
perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax; perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax;
perUserListTimelineCacheMax = meta.perUserListTimelineCacheMax; perUserListTimelineCacheMax = meta.perUserListTimelineCacheMax;
notesPerOneAd = meta.notesPerOneAd;
} }
function save(): void { async function save(): void {
os.apiWithDialog('admin/update-meta', { await os.apiWithDialog('admin/update-meta', {
name, name,
shortName: shortName === '' ? null : shortName, shortName: shortName === '' ? null : shortName,
description, description,
@ -191,9 +210,10 @@ function save(): void {
perRemoteUserUserTimelineCacheMax, perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax, perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax, perUserListTimelineCacheMax,
}).then(() => { notesPerOneAd,
fetchInstance();
}); });
fetchInstance();
} }
const headerTabs = $computed(() => []); const headerTabs = $computed(() => []);

View file

@ -2448,6 +2448,7 @@ type LiteInstanceMetadata = {
url: string; url: string;
imageUrl: string; imageUrl: string;
}[]; }[];
notesPerOneAd: number;
translatorAvailable: boolean; translatorAvailable: boolean;
serverRules: string[]; 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: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/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: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 // 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) // (No @packageDocumentation comment for this package)

View file

@ -362,6 +362,7 @@ export type LiteInstanceMetadata = {
url: string; url: string;
imageUrl: string; imageUrl: string;
}[]; }[];
notesPerOneAd: number;
translatorAvailable: boolean; translatorAvailable: boolean;
serverRules: string[]; serverRules: string[];
}; };