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:
parent
9240db35f3
commit
4bbfc98883
13 changed files with 88 additions and 5 deletions
|
@ -28,6 +28,9 @@
|
||||||
- Feat: ユーザーごとのハイライト
|
- Feat: ユーザーごとのハイライト
|
||||||
- Feat: プライバシーポリシー・運営者情報(Impressum)の指定が可能になりました
|
- Feat: プライバシーポリシー・運営者情報(Impressum)の指定が可能になりました
|
||||||
- プライバシーポリシーはサーバー登録時に同意確認が入ります
|
- プライバシーポリシーはサーバー登録時に同意確認が入ります
|
||||||
|
- Feat: タイムラインがリアルタイム更新中に広告を挿入できるようになりました
|
||||||
|
- デフォルトは無効
|
||||||
|
- 頻度はコントロールパネルから設定できます。運営中のサーバーのTLの流速を見て、最適な値を指定してください。
|
||||||
- Enhance: ソフトワードミュートとハードワードミュートは統合されました
|
- Enhance: ソフトワードミュートとハードワードミュートは統合されました
|
||||||
- Enhance: モデレーションログ機能の強化
|
- Enhance: モデレーションログ機能の強化
|
||||||
- Enhance: ローカリゼーションの更新
|
- Enhance: ローカリゼーションの更新
|
||||||
|
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
@ -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;
|
||||||
|
|
|
@ -1546,6 +1546,10 @@ _ad:
|
||||||
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
|
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
|
||||||
hide: "表示しない"
|
hide: "表示しない"
|
||||||
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。"
|
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。"
|
||||||
|
adsSettings: "広告配信設定"
|
||||||
|
notesPerOneAd: "リアルタイム更新中に広告を配信する間隔(ノートの個数)"
|
||||||
|
setZeroToDisable: "0でリアルタイム更新時の広告配信を無効"
|
||||||
|
adsTooClose: "広告の配信間隔が極めて短いため、ユーザー体験が著しく損われる可能性があります。"
|
||||||
|
|
||||||
_forgotPassword:
|
_forgotPassword:
|
||||||
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
|
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
|
||||||
|
|
16
packages/backend/migration/1696743032098-AdsOnStream.js
Normal file
16
packages/backend/migration/1696743032098-AdsOnStream.js
Normal 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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -503,4 +503,9 @@ export class MiMeta {
|
||||||
default: 300,
|
default: 300,
|
||||||
})
|
})
|
||||||
public perUserListTimelineCacheMax: number;
|
public perUserListTimelineCacheMax: number;
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 0,
|
||||||
|
})
|
||||||
|
public notesPerOneAd: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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(() => []);
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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[];
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue