mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-30 07:13:09 +02:00
Ad (#7495)
* wip * Update ad.vue * Update default.widgets.vue * wip * Create 1620019354680-ad.ts * wip * Update ads.vue * wip * Update ad.vue
This commit is contained in:
parent
71ebb068f7
commit
18e1efc7ec
24 changed files with 596 additions and 27 deletions
|
@ -748,6 +748,13 @@ gallery: "ギャラリー"
|
||||||
recentPosts: "最近の投稿"
|
recentPosts: "最近の投稿"
|
||||||
popularPosts: "人気の投稿"
|
popularPosts: "人気の投稿"
|
||||||
shareWithNote: "ノートで共有"
|
shareWithNote: "ノートで共有"
|
||||||
|
ads: "広告"
|
||||||
|
expiration: "期限"
|
||||||
|
memo: "メモ"
|
||||||
|
priority: "優先度"
|
||||||
|
high: "高"
|
||||||
|
middle: "中"
|
||||||
|
low: "低"
|
||||||
emailNotConfiguredWarning: "メールアドレスの設定がされていません。"
|
emailNotConfiguredWarning: "メールアドレスの設定がされていません。"
|
||||||
|
|
||||||
_forgotPassword:
|
_forgotPassword:
|
||||||
|
|
18
migration/1620019354680-ad.ts
Normal file
18
migration/1620019354680-ad.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class ad1620019354680 implements MigrationInterface {
|
||||||
|
name = 'ad1620019354680'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "ad" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "place" character varying(32) NOT NULL, "priority" character varying(32) NOT NULL, "url" character varying(1024) NOT NULL, "imageUrl" character varying(1024) NOT NULL, "memo" character varying(8192) NOT NULL, CONSTRAINT "PK_0193d5ef09746e88e9ea92c634d" PRIMARY KEY ("id")); COMMENT ON COLUMN "ad"."createdAt" IS 'The created date of the Ad.'; COMMENT ON COLUMN "ad"."expiresAt" IS 'The expired date of the Ad.'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_1129c2ef687fc272df040bafaa" ON "ad" ("createdAt") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_2da24ce20ad209f1d9dc032457" ON "ad" ("expiresAt") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_2da24ce20ad209f1d9dc032457"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_1129c2ef687fc272df040bafaa"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "ad"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -32,7 +32,6 @@
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"chokidar": "^3.3.1",
|
"chokidar": "^3.3.1",
|
||||||
"constantinople": "^4.0.1",
|
"constantinople": "^4.0.1",
|
||||||
"gulp/gulp-cli/yargs/yargs-parser": "5.0.0-security.0",
|
|
||||||
"jsonld/rdf-canonize/node-forge": "0.10.0",
|
"jsonld/rdf-canonize/node-forge": "0.10.0",
|
||||||
"lodash": "^4.17.20"
|
"lodash": "^4.17.20"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, h, TransitionGroup } from 'vue';
|
import { defineComponent, h, TransitionGroup } from 'vue';
|
||||||
|
import MkAd from '@client/components/global/ad.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
@ -22,6 +23,11 @@ export default defineComponent({
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
ad: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -58,11 +64,7 @@ export default defineComponent({
|
||||||
|
|
||||||
if (
|
if (
|
||||||
i != this.items.length - 1 &&
|
i != this.items.length - 1 &&
|
||||||
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() &&
|
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
|
||||||
!item._prId_ &&
|
|
||||||
!this.items[i + 1]._prId_ &&
|
|
||||||
!item._featuredId_ &&
|
|
||||||
!this.items[i + 1]._featuredId_
|
|
||||||
) {
|
) {
|
||||||
const separator = h('div', {
|
const separator = h('div', {
|
||||||
class: 'separator',
|
class: 'separator',
|
||||||
|
@ -85,9 +87,17 @@ export default defineComponent({
|
||||||
]));
|
]));
|
||||||
|
|
||||||
return [el, separator];
|
return [el, separator];
|
||||||
|
} else {
|
||||||
|
if (this.ad && item._shouldInsertAd_) {
|
||||||
|
return [h(MkAd, {
|
||||||
|
class: 'ad',
|
||||||
|
key: item.id + ':ad',
|
||||||
|
prefer: 'horizontal',
|
||||||
|
}), el];
|
||||||
} else {
|
} else {
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
142
src/client/components/global/ad.vue
Normal file
142
src/client/components/global/ad.vue
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
<template>
|
||||||
|
<div class="qiivuoyo" v-if="ad">
|
||||||
|
<div class="main" :class="ad.place" v-if="!showMenu">
|
||||||
|
<a :href="ad.url" target="_blank">
|
||||||
|
<img :src="ad.imageUrl">
|
||||||
|
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle"></span></button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="menu" v-else>
|
||||||
|
<div class="body">
|
||||||
|
<div>Ads by {{ host }}</div>
|
||||||
|
<!--<MkButton>{{ $ts.stopThisAd }}</MkButton>-->
|
||||||
|
<button class="_textButton" @click="toggleMenu">{{ $ts.close }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from 'vue';
|
||||||
|
import { instance } from '@client/instance';
|
||||||
|
import { host } from '@client/config';
|
||||||
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
MkButton
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
prefer: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
ad: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const showMenu = ref(false);
|
||||||
|
const toggleMenu = () => {
|
||||||
|
showMenu.value = !showMenu.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ad = null;
|
||||||
|
|
||||||
|
if (props.ad) {
|
||||||
|
ad = props.ad;
|
||||||
|
} else {
|
||||||
|
let ads = instance.ads.filter(ad => ad.place === props.prefer);
|
||||||
|
|
||||||
|
if (ads.length === 0) {
|
||||||
|
ads = instance.ads.filter(ad => ad.place === 'square');
|
||||||
|
}
|
||||||
|
|
||||||
|
const high = ads.filter(ad => ad.priority === 'high');
|
||||||
|
const middle = ads.filter(ad => ad.priority === 'middle');
|
||||||
|
const low = ads.filter(ad => ad.priority === 'low');
|
||||||
|
|
||||||
|
if (high.length > 0) {
|
||||||
|
ad = high[Math.floor(Math.random() * high.length)];
|
||||||
|
} else if (middle.length > 0) {
|
||||||
|
ad = middle[Math.floor(Math.random() * middle.length)];
|
||||||
|
} else if (low.length > 0) {
|
||||||
|
ad = low[Math.floor(Math.random() * low.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ad,
|
||||||
|
showMenu,
|
||||||
|
toggleMenu,
|
||||||
|
host,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.qiivuoyo {
|
||||||
|
background-size: auto auto;
|
||||||
|
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
|
||||||
|
|
||||||
|
> .main {
|
||||||
|
> a {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.square {
|
||||||
|
> a {
|
||||||
|
max-width: min(300px, 100%);
|
||||||
|
max-height: min(300px, 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.horizontal {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
> a {
|
||||||
|
max-width: min(600px, 100%);
|
||||||
|
max-height: min(100px, 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
> a {
|
||||||
|
max-width: min(100px, 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .menu {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> .body {
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 400px;
|
||||||
|
border: solid 1px var(--divider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -12,8 +12,10 @@ import url from './global/url.vue';
|
||||||
import i18n from './global/i18n';
|
import i18n from './global/i18n';
|
||||||
import loading from './global/loading.vue';
|
import loading from './global/loading.vue';
|
||||||
import error from './global/error.vue';
|
import error from './global/error.vue';
|
||||||
|
import ad from './global/ad.vue';
|
||||||
|
|
||||||
export default function(app: App) {
|
export default function(app: App) {
|
||||||
|
app.component('I18n', i18n);
|
||||||
app.component('Mfm', mfm);
|
app.component('Mfm', mfm);
|
||||||
app.component('MkA', a);
|
app.component('MkA', a);
|
||||||
app.component('MkAcct', acct);
|
app.component('MkAcct', acct);
|
||||||
|
@ -25,5 +27,5 @@ export default function(app: App) {
|
||||||
app.component('MkUrl', url);
|
app.component('MkUrl', url);
|
||||||
app.component('MkLoading', loading);
|
app.component('MkLoading', loading);
|
||||||
app.component('MkError', error);
|
app.component('MkError', error);
|
||||||
app.component('I18n', i18n);
|
app.component('MkAd', ad);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap">
|
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap" :ad="true">
|
||||||
<XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/>
|
<XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/>
|
||||||
</XList>
|
</XList>
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<MkAd prefer="horizontal"/>
|
||||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||||
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
|
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
|
||||||
<MkPagination :pagination="otherPostsPagination" #default="{items}">
|
<MkPagination :pagination="otherPostsPagination" #default="{items}">
|
||||||
|
|
125
src/client/pages/instance/ads.vue
Normal file
125
src/client/pages/instance/ads.vue
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
<template>
|
||||||
|
<div class="uqshojas">
|
||||||
|
<MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
|
||||||
|
<section class="_card _gap ads" v-for="ad in ads">
|
||||||
|
<div class="_content ad">
|
||||||
|
<MkAd v-if="ad.url" :ad="ad"/>
|
||||||
|
<MkInput v-model:value="ad.url" type="url">
|
||||||
|
<span>URL</span>
|
||||||
|
</MkInput>
|
||||||
|
<MkInput v-model:value="ad.imageUrl">
|
||||||
|
<span>{{ $ts.imageUrl }}</span>
|
||||||
|
</MkInput>
|
||||||
|
<div style="margin: 32px 0;">
|
||||||
|
<MkRadio v-model="ad.place" value="square">square</MkRadio>
|
||||||
|
<MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio>
|
||||||
|
</div>
|
||||||
|
<div style="margin: 32px 0;">
|
||||||
|
{{ $ts.priority }}
|
||||||
|
<MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
|
||||||
|
<MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
|
||||||
|
<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
|
||||||
|
</div>
|
||||||
|
<MkInput v-model:value="ad.expiresAt" type="date">
|
||||||
|
<span>{{ $ts.expiration }}</span>
|
||||||
|
</MkInput>
|
||||||
|
<MkTextarea v-model:value="ad.memo">
|
||||||
|
<span>{{ $ts.memo }}</span>
|
||||||
|
</MkTextarea>
|
||||||
|
<div class="buttons">
|
||||||
|
<MkButton class="button" inline @click="save(ad)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||||
|
<MkButton class="button" inline @click="remove(ad)" danger><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
|
import MkInput from '@client/components/ui/input.vue';
|
||||||
|
import MkTextarea from '@client/components/ui/textarea.vue';
|
||||||
|
import MkRadio from '@client/components/ui/radio.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
MkButton,
|
||||||
|
MkInput,
|
||||||
|
MkTextarea,
|
||||||
|
MkRadio,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.ads,
|
||||||
|
icon: 'fas fa-audio-description'
|
||||||
|
},
|
||||||
|
ads: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
os.api('admin/ad/list').then(ads => {
|
||||||
|
this.ads = ads;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
add() {
|
||||||
|
this.ads.unshift({
|
||||||
|
id: null,
|
||||||
|
memo: '',
|
||||||
|
place: 'square',
|
||||||
|
priority: 'middle',
|
||||||
|
url: '',
|
||||||
|
imageUrl: null,
|
||||||
|
expiresAt: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(ad) {
|
||||||
|
os.dialog({
|
||||||
|
type: 'warning',
|
||||||
|
text: this.$t('removeAreYouSure', { x: ad.url }),
|
||||||
|
showCancelButton: true
|
||||||
|
}).then(({ canceled }) => {
|
||||||
|
if (canceled) return;
|
||||||
|
this.ads = this.ads.filter(x => x != ad);
|
||||||
|
os.apiWithDialog('admin/ad/delete', {
|
||||||
|
id: ad.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
save(ad) {
|
||||||
|
if (ad.id == null) {
|
||||||
|
os.apiWithDialog('admin/ad/create', {
|
||||||
|
...ad,
|
||||||
|
expiresAt: new Date(ad.expiresAt).getTime()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
os.apiWithDialog('admin/ad/update', {
|
||||||
|
...ad,
|
||||||
|
expiresAt: new Date(ad.expiresAt).getTime()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.uqshojas {
|
||||||
|
margin: var(--margin);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -23,6 +23,7 @@
|
||||||
<FormLink :active="page === 'queue'" replace to="/instance/queue"><template #icon><i class="fas fa-clipboard-list"></i></template>{{ $ts.jobQueue }}</FormLink>
|
<FormLink :active="page === 'queue'" replace to="/instance/queue"><template #icon><i class="fas fa-clipboard-list"></i></template>{{ $ts.jobQueue }}</FormLink>
|
||||||
<FormLink :active="page === 'files'" replace to="/instance/files"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink>
|
<FormLink :active="page === 'files'" replace to="/instance/files"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink>
|
||||||
<FormLink :active="page === 'announcements'" replace to="/instance/announcements"><template #icon><i class="fas fa-broadcast-tower"></i></template>{{ $ts.announcements }}</FormLink>
|
<FormLink :active="page === 'announcements'" replace to="/instance/announcements"><template #icon><i class="fas fa-broadcast-tower"></i></template>{{ $ts.announcements }}</FormLink>
|
||||||
|
<FormLink :active="page === 'ads'" replace to="/instance/ads"><template #icon><i class="fas fa-audio-description"></i></template>{{ $ts.ads }}</FormLink>
|
||||||
<FormLink :active="page === 'abuses'" replace to="/instance/abuses"><template #icon><i class="fas fa-exclamation-circle"></i></template>{{ $ts.abuseReports }}</FormLink>
|
<FormLink :active="page === 'abuses'" replace to="/instance/abuses"><template #icon><i class="fas fa-exclamation-circle"></i></template>{{ $ts.abuseReports }}</FormLink>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
@ -102,6 +103,7 @@ export default defineComponent({
|
||||||
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
|
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
|
||||||
case 'files': return defineAsyncComponent(() => import('./files.vue'));
|
case 'files': return defineAsyncComponent(() => import('./files.vue'));
|
||||||
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
|
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
|
||||||
|
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
|
||||||
case 'database': return defineAsyncComponent(() => import('./database.vue'));
|
case 'database': return defineAsyncComponent(() => import('./database.vue'));
|
||||||
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
|
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
|
||||||
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
|
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
<div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
|
<div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
|
||||||
<div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
|
<div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
|
||||||
</div>
|
</div>
|
||||||
|
<MkAd prefer="horizontal"/>
|
||||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||||
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
|
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
|
||||||
<MkPagination :pagination="otherPostsPagination" #default="{items}">
|
<MkPagination :pagination="otherPostsPagination" #default="{items}">
|
||||||
|
|
|
@ -91,8 +91,10 @@ export default (opts) => ({
|
||||||
...params,
|
...params,
|
||||||
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
|
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
|
||||||
}).then(items => {
|
}).then(items => {
|
||||||
for (const item of items) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i];
|
||||||
markRaw(item);
|
markRaw(item);
|
||||||
|
if (i === 3) item._shouldInsertAd_ = true;
|
||||||
}
|
}
|
||||||
if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
|
if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
|
||||||
items.pop();
|
items.pop();
|
||||||
|
@ -128,8 +130,10 @@ export default (opts) => ({
|
||||||
untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
|
untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
|
||||||
}),
|
}),
|
||||||
}).then(items => {
|
}).then(items => {
|
||||||
for (const item of items) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i];
|
||||||
markRaw(item);
|
markRaw(item);
|
||||||
|
if (i === 10) item._shouldInsertAd_ = true;
|
||||||
}
|
}
|
||||||
if (items.length > SECOND_FETCH_LIMIT) {
|
if (items.length > SECOND_FETCH_LIMIT) {
|
||||||
items.pop();
|
items.pop();
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
--margin: var(--marginHalf);
|
--margin: var(--marginHalf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//--ad: rgb(255 169 0 / 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
|
|
|
@ -42,11 +42,7 @@ export default defineComponent({
|
||||||
|
|
||||||
if (
|
if (
|
||||||
i != this.items.length - 1 &&
|
i != this.items.length - 1 &&
|
||||||
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() &&
|
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
|
||||||
!item._prId_ &&
|
|
||||||
!this.items[i + 1]._prId_ &&
|
|
||||||
!item._featuredId_ &&
|
|
||||||
!this.items[i + 1]._featuredId_
|
|
||||||
) {
|
) {
|
||||||
const separator = h('div', {
|
const separator = h('div', {
|
||||||
class: 'separator',
|
class: 'separator',
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="efzpzdvf">
|
<div class="efzpzdvf">
|
||||||
<XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
|
<XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
|
||||||
|
<MkAd prefer="square"/>
|
||||||
|
|
||||||
<button v-if="editMode" @click="editMode = false" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button>
|
<button v-if="editMode" @click="editMode = false" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button>
|
||||||
<button v-else @click="editMode = true" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-pencil-alt"></i> {{ $ts.editWidgets }}</button>
|
<button v-else @click="editMode = true" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-pencil-alt"></i> {{ $ts.editWidgets }}</button>
|
||||||
|
|
|
@ -70,6 +70,7 @@ import { Channel } from '../models/entities/channel';
|
||||||
import { ChannelFollowing } from '../models/entities/channel-following';
|
import { ChannelFollowing } from '../models/entities/channel-following';
|
||||||
import { ChannelNotePining } from '../models/entities/channel-note-pining';
|
import { ChannelNotePining } from '../models/entities/channel-note-pining';
|
||||||
import { RegistryItem } from '../models/entities/registry-item';
|
import { RegistryItem } from '../models/entities/registry-item';
|
||||||
|
import { Ad } from '../models/entities/ad';
|
||||||
import { PasswordResetRequest } from '@/models/entities/password-reset-request';
|
import { PasswordResetRequest } from '@/models/entities/password-reset-request';
|
||||||
|
|
||||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||||
|
@ -170,6 +171,7 @@ export const entities = [
|
||||||
ChannelFollowing,
|
ChannelFollowing,
|
||||||
ChannelNotePining,
|
ChannelNotePining,
|
||||||
RegistryItem,
|
RegistryItem,
|
||||||
|
Ad,
|
||||||
PasswordResetRequest,
|
PasswordResetRequest,
|
||||||
...charts as any
|
...charts as any
|
||||||
];
|
];
|
||||||
|
|
53
src/models/entities/ad.ts
Normal file
53
src/models/entities/ad.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
|
||||||
|
import { id } from '../id';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Ad {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
comment: 'The created date of the Ad.'
|
||||||
|
})
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
comment: 'The expired date of the Ad.'
|
||||||
|
})
|
||||||
|
public expiresAt: Date;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 32, nullable: false
|
||||||
|
})
|
||||||
|
public place: string;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 32, nullable: false
|
||||||
|
})
|
||||||
|
public priority: string;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024, nullable: false
|
||||||
|
})
|
||||||
|
public url: string;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024, nullable: false
|
||||||
|
})
|
||||||
|
public imageUrl: string;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 8192, nullable: false
|
||||||
|
})
|
||||||
|
public memo: string;
|
||||||
|
|
||||||
|
constructor(data: Partial<Ad>) {
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(data)) {
|
||||||
|
(this as any)[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,6 +60,7 @@ import { MutedNote } from './entities/muted-note';
|
||||||
import { ChannelFollowing } from './entities/channel-following';
|
import { ChannelFollowing } from './entities/channel-following';
|
||||||
import { ChannelNotePining } from './entities/channel-note-pining';
|
import { ChannelNotePining } from './entities/channel-note-pining';
|
||||||
import { RegistryItem } from './entities/registry-item';
|
import { RegistryItem } from './entities/registry-item';
|
||||||
|
import { Ad } from './entities/ad';
|
||||||
import { PasswordResetRequest } from './entities/password-reset-request';
|
import { PasswordResetRequest } from './entities/password-reset-request';
|
||||||
|
|
||||||
export const Announcements = getRepository(Announcement);
|
export const Announcements = getRepository(Announcement);
|
||||||
|
@ -123,4 +124,5 @@ export const Channels = getCustomRepository(ChannelRepository);
|
||||||
export const ChannelFollowings = getRepository(ChannelFollowing);
|
export const ChannelFollowings = getRepository(ChannelFollowing);
|
||||||
export const ChannelNotePinings = getRepository(ChannelNotePining);
|
export const ChannelNotePinings = getRepository(ChannelNotePining);
|
||||||
export const RegistryItems = getRepository(RegistryItem);
|
export const RegistryItems = getRepository(RegistryItem);
|
||||||
|
export const Ads = getRepository(Ad);
|
||||||
export const PasswordResetRequests = getRepository(PasswordResetRequest);
|
export const PasswordResetRequests = getRepository(PasswordResetRequest);
|
||||||
|
|
|
@ -200,8 +200,6 @@ export class NoteRepository extends Repository<Note> {
|
||||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||||
uri: note.uri || undefined,
|
uri: note.uri || undefined,
|
||||||
url: note.url || undefined,
|
url: note.url || undefined,
|
||||||
_featuredId_: (note as any)._featuredId_ || undefined,
|
|
||||||
_prId_: (note as any)._prId_ || undefined,
|
|
||||||
|
|
||||||
...(opts.detail ? {
|
...(opts.detail ? {
|
||||||
reply: note.replyId ? this.pack(note.reply || note.replyId, me, {
|
reply: note.replyId ? this.pack(note.reply || note.replyId, me, {
|
||||||
|
@ -448,14 +446,7 @@ export const packedNoteSchema = {
|
||||||
optional: false as const, nullable: true as const,
|
optional: false as const, nullable: true as const,
|
||||||
description: 'The human readable url of a note. it will be null when the note is local.',
|
description: 'The human readable url of a note. it will be null when the note is local.',
|
||||||
},
|
},
|
||||||
_featuredId_: {
|
|
||||||
type: 'string' as const,
|
|
||||||
optional: false as const, nullable: true as const,
|
|
||||||
},
|
|
||||||
_prId_: {
|
|
||||||
type: 'string' as const,
|
|
||||||
optional: false as const, nullable: true as const,
|
|
||||||
},
|
|
||||||
myReaction: {
|
myReaction: {
|
||||||
type: 'object' as const,
|
type: 'object' as const,
|
||||||
optional: true as const, nullable: true as const,
|
optional: true as const, nullable: true as const,
|
||||||
|
|
45
src/server/api/endpoints/admin/ad/create.ts
Normal file
45
src/server/api/endpoints/admin/ad/create.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import define from '../../../define';
|
||||||
|
import { Ads } from '../../../../../models';
|
||||||
|
import { genId } from '@/misc/gen-id';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true as const,
|
||||||
|
requireModerator: true,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
url: {
|
||||||
|
validator: $.str.min(1)
|
||||||
|
},
|
||||||
|
memo: {
|
||||||
|
validator: $.str
|
||||||
|
},
|
||||||
|
place: {
|
||||||
|
validator: $.str
|
||||||
|
},
|
||||||
|
priority: {
|
||||||
|
validator: $.str
|
||||||
|
},
|
||||||
|
expiresAt: {
|
||||||
|
validator: $.num.int()
|
||||||
|
},
|
||||||
|
imageUrl: {
|
||||||
|
validator: $.str.min(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps) => {
|
||||||
|
await Ads.insert({
|
||||||
|
id: genId(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
expiresAt: new Date(ps.expiresAt),
|
||||||
|
url: ps.url,
|
||||||
|
imageUrl: ps.imageUrl,
|
||||||
|
priority: ps.priority,
|
||||||
|
place: ps.place,
|
||||||
|
memo: ps.memo,
|
||||||
|
});
|
||||||
|
});
|
34
src/server/api/endpoints/admin/ad/delete.ts
Normal file
34
src/server/api/endpoints/admin/ad/delete.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import define from '../../../define';
|
||||||
|
import { ID } from '@/misc/cafy-id';
|
||||||
|
import { Ads } from '../../../../../models';
|
||||||
|
import { ApiError } from '../../../error';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true as const,
|
||||||
|
requireModerator: true,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
id: {
|
||||||
|
validator: $.type(ID)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchAd: {
|
||||||
|
message: 'No such ad.',
|
||||||
|
code: 'NO_SUCH_AD',
|
||||||
|
id: 'ccac9863-3a03-416e-b899-8a64041118b1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps, me) => {
|
||||||
|
const ad = await Ads.findOne(ps.id);
|
||||||
|
|
||||||
|
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
|
||||||
|
|
||||||
|
await Ads.delete(ad.id);
|
||||||
|
});
|
36
src/server/api/endpoints/admin/ad/list.ts
Normal file
36
src/server/api/endpoints/admin/ad/list.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import { ID } from '@/misc/cafy-id';
|
||||||
|
import define from '../../../define';
|
||||||
|
import { Ads } from '../../../../../models';
|
||||||
|
import { makePaginationQuery } from '../../../common/make-pagination-query';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true as const,
|
||||||
|
requireModerator: true,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
limit: {
|
||||||
|
validator: $.optional.num.range(1, 100),
|
||||||
|
default: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
sinceId: {
|
||||||
|
validator: $.optional.type(ID),
|
||||||
|
},
|
||||||
|
|
||||||
|
untilId: {
|
||||||
|
validator: $.optional.type(ID),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps) => {
|
||||||
|
const query = makePaginationQuery(Ads.createQueryBuilder('ad'), ps.sinceId, ps.untilId)
|
||||||
|
.andWhere('ad.expiresAt > :now', { now: new Date() });
|
||||||
|
|
||||||
|
const ads = await query.take(ps.limit!).getMany();
|
||||||
|
|
||||||
|
return ads;
|
||||||
|
});
|
59
src/server/api/endpoints/admin/ad/update.ts
Normal file
59
src/server/api/endpoints/admin/ad/update.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import define from '../../../define';
|
||||||
|
import { ID } from '@/misc/cafy-id';
|
||||||
|
import { Ads } from '../../../../../models';
|
||||||
|
import { ApiError } from '../../../error';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true as const,
|
||||||
|
requireModerator: true,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
id: {
|
||||||
|
validator: $.type(ID)
|
||||||
|
},
|
||||||
|
memo: {
|
||||||
|
validator: $.str
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
validator: $.str.min(1)
|
||||||
|
},
|
||||||
|
imageUrl: {
|
||||||
|
validator: $.str.min(1)
|
||||||
|
},
|
||||||
|
place: {
|
||||||
|
validator: $.str
|
||||||
|
},
|
||||||
|
priority: {
|
||||||
|
validator: $.str
|
||||||
|
},
|
||||||
|
expiresAt: {
|
||||||
|
validator: $.num.int()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchAd: {
|
||||||
|
message: 'No such ad.',
|
||||||
|
code: 'NO_SUCH_AD',
|
||||||
|
id: 'b7aa1727-1354-47bc-a182-3a9c3973d300'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps, me) => {
|
||||||
|
const ad = await Ads.findOne(ps.id);
|
||||||
|
|
||||||
|
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
|
||||||
|
|
||||||
|
await Ads.update(ad.id, {
|
||||||
|
url: ps.url,
|
||||||
|
place: ps.place,
|
||||||
|
priority: ps.priority,
|
||||||
|
memo: ps.memo,
|
||||||
|
imageUrl: ps.imageUrl,
|
||||||
|
expiresAt: new Date(ps.expiresAt),
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,8 +2,9 @@ import $ from 'cafy';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import define from '../define';
|
import define from '../define';
|
||||||
import { fetchMeta } from '@/misc/fetch-meta';
|
import { fetchMeta } from '@/misc/fetch-meta';
|
||||||
import { Emojis, Users } from '../../../models';
|
import { Ads, Emojis, Users } from '../../../models';
|
||||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits';
|
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits';
|
||||||
|
import { MoreThan } from 'typeorm';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
|
@ -193,6 +194,30 @@ export const meta = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
ads: {
|
||||||
|
type: 'array' as const,
|
||||||
|
optional: false as const, nullable: false as const,
|
||||||
|
items: {
|
||||||
|
type: 'object' as const,
|
||||||
|
optional: false as const, nullable: false as const,
|
||||||
|
properties: {
|
||||||
|
place: {
|
||||||
|
type: 'string' as const,
|
||||||
|
optional: false as const, nullable: false as const
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: 'string' as const,
|
||||||
|
optional: false as const, nullable: false as const,
|
||||||
|
format: 'url'
|
||||||
|
},
|
||||||
|
imageUrl: {
|
||||||
|
type: 'string' as const,
|
||||||
|
optional: false as const, nullable: false as const,
|
||||||
|
format: 'url'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
requireSetup: {
|
requireSetup: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean' as const,
|
||||||
optional: false as const, nullable: false as const,
|
optional: false as const, nullable: false as const,
|
||||||
|
@ -443,6 +468,12 @@ export default define(meta, async (ps, me) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ads = await Ads.find({
|
||||||
|
where: {
|
||||||
|
expiresAt: MoreThan(new Date())
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const response: any = {
|
const response: any = {
|
||||||
maintainerName: instance.maintainerName,
|
maintainerName: instance.maintainerName,
|
||||||
maintainerEmail: instance.maintainerEmail,
|
maintainerEmail: instance.maintainerEmail,
|
||||||
|
@ -477,6 +508,12 @@ export default define(meta, async (ps, me) => {
|
||||||
logoImageUrl: instance.logoImageUrl,
|
logoImageUrl: instance.logoImageUrl,
|
||||||
maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH),
|
maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH),
|
||||||
emojis: await Emojis.packMany(emojis),
|
emojis: await Emojis.packMany(emojis),
|
||||||
|
ads: ads.map(ad => ({
|
||||||
|
url: ad.url,
|
||||||
|
place: ad.place,
|
||||||
|
priority: ad.priority,
|
||||||
|
imageUrl: ad.imageUrl,
|
||||||
|
})),
|
||||||
enableEmail: instance.enableEmail,
|
enableEmail: instance.enableEmail,
|
||||||
|
|
||||||
enableTwitterIntegration: instance.enableTwitterIntegration,
|
enableTwitterIntegration: instance.enableTwitterIntegration,
|
||||||
|
|
Loading…
Reference in a new issue