Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 10 commits into from
Oct 8, 2023
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
- Feat: ユーザーごとのハイライト
- Feat: プライバシーポリシー・運営者情報(Impressum)の指定が可能になりました
- プライバシーポリシーはサーバー登録時に同意確認が入ります
- Feat: タイムラインがリアルタイム更新中に広告を挿入できるようになりました
- デフォルトは無効
- 頻度はコントロールパネルから設定できます。運営中のサーバーのTLの流速を見て、最適な値を指定してください。
- Enhance: ソフトワードミュートとハードワードミュートは統合されました
- Enhance: モデレーションログ機能の強化
- Enhance: ローカリゼーションの更新
Expand Down
4 changes: 4 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1627,6 +1627,10 @@ export interface Locale {
"reduceFrequencyOfThisAd": string;
"hide": string;
"timezoneinfo": string;
"adsSettings": string;
"notesPerOneAd": string;
"setZeroToDisable": string;
"adsTooClose": string;
};
"_forgotPassword": {
"enterEmail": string;
Expand Down
4 changes: 4 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1546,6 +1546,10 @@ _ad:
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
hide: "表示しない"
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。"
adsSettings: "広告配信設定"
notesPerOneAd: "リアルタイム更新中に広告を配信する間隔(ノートの個数)"
setZeroToDisable: "0でリアルタイム更新時の広告配信を無効"
adsTooClose: "広告の配信間隔が極めて短いため、ユーザー体験が著しく損われる可能性があります。"

_forgotPassword:
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/migration/1696743032098-AdsOnStream.js
Original file line number Diff line number Diff line change
@@ -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"`);
}
}
5 changes: 5 additions & 0 deletions packages/backend/src/models/Meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,4 +503,9 @@ export class MiMeta {
default: 300,
})
public perUserListTimelineCacheMax: number;

@Column('integer', {
default: 0,
})
public notesPerOneAd: number;
}
5 changes: 5 additions & 0 deletions packages/backend/src/server/api/endpoints/admin/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,10 @@ export const meta = {
type: 'number',
optional: false, nullable: false,
},
notesPerOneAd: {
type: 'number',
optional: false, nullable: false,
},
},
},
} as const;
Expand Down Expand Up @@ -408,6 +412,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
notesPerOneAd: instance.notesPerOneAd,
};
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export const paramDef = {
perRemoteUserUserTimelineCacheMax: { type: 'integer' },
perUserHomeTimelineCacheMax: { type: 'integer' },
perUserListTimelineCacheMax: { type: 'integer' },
notesPerOneAd: { type: 'integer' },
},
required: [],
} as const;
Expand Down Expand Up @@ -471,6 +472,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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);
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/server/api/endpoints/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ export const meta = {
},
},
},
notesPerOneAd: {
type: 'number',
optional: false, nullable: false,
default: 0,
},
requireSetup: {
type: 'boolean',
optional: false, nullable: false,
Expand Down Expand Up @@ -331,6 +336,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek,
})),
notesPerOneAd: instance.notesPerOneAd,
enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker,

Expand Down
4 changes: 4 additions & 0 deletions packages/frontend/src/components/MkInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -59,6 +61,8 @@ const props = defineProps<{
spellcheck?: boolean;
step?: any;
datalist?: string[];
min?: string;
max?: string;
inline?: boolean;
debounce?: boolean;
manualSave?: boolean;
Expand Down
9 changes: 9 additions & 0 deletions packages/frontend/src/components/MkTimeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand All @@ -38,7 +39,15 @@ provide('inChannel', computed(() => props.src === 'channel'));

const tlComponent: InstanceType<typeof MkNotes> = $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');
Expand Down
28 changes: 24 additions & 4 deletions packages/frontend/src/pages/admin/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
</div>
</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>
</FormSuspense>
</MkSpacer>
Expand All @@ -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';
Expand All @@ -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<void> {
const meta = await os.api('admin/meta');
Expand All @@ -171,10 +189,11 @@ async function init(): Promise<void> {
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,
Expand All @@ -191,9 +210,10 @@ function save(): void {
perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax,
}).then(() => {
fetchInstance();
notesPerOneAd,
});

fetchInstance();
}

const headerTabs = $computed(() => []);
Expand Down
3 changes: 2 additions & 1 deletion packages/misskey-js/etc/misskey-js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2448,6 +2448,7 @@ type LiteInstanceMetadata = {
url: string;
imageUrl: string;
}[];
notesPerOneAd: number;
translatorAvailable: boolean;
serverRules: string[];
};
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/misskey-js/src/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ export type LiteInstanceMetadata = {
url: string;
imageUrl: string;
}[];
notesPerOneAd: number;
translatorAvailable: boolean;
serverRules: string[];
};
Expand Down
Loading