Frontend: Pagination by time and removed ads
ci/woodpecker/push/ociImagePush Pipeline was successful Details

This commit is contained in:
Natty 2024-01-01 20:29:33 +01:00
parent 6b96f60b43
commit 6003f5e404
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
14 changed files with 41 additions and 461 deletions

View File

@ -947,9 +947,6 @@ export type Endpoints = {
// pinned-users // pinned-users
"pinned-users": { req: TODO; res: TODO }; "pinned-users": { req: TODO; res: TODO };
// promo
"promo/read": { req: TODO; res: TODO };
// request-reset-password // request-reset-password
"request-reset-password": { "request-reset-password": {
req: { username: string; email: string }; req: { username: string; email: string };

View File

@ -25,14 +25,6 @@
}" }"
> >
<div class="line"></div> <div class="line"></div>
<div v-if="appearNote._prId_" class="info">
<i class="ph-megaphone-simple-bold ph-lg"></i>
{{ i18n.ts.promotion
}}<button class="_textButton hide" @click.stop="readPromo()">
{{ i18n.ts.hideThisNote }}
<i class="ph-x ph-bold ph-lg"></i>
</button>
</div>
<div v-if="appearNote._featuredId_" class="info"> <div v-if="appearNote._featuredId_" class="info">
<i class="ph-lightning ph-bold ph-lg"></i> <i class="ph-lightning ph-bold ph-lg"></i>
{{ i18n.ts.featured }} {{ i18n.ts.featured }}
@ -517,13 +509,6 @@ function noteClick(e) {
} }
} }
function readPromo() {
os.api("promo/read", {
noteId: appearNote.id,
});
isDeleted.value = true;
}
let postIsExpanded = ref(false); let postIsExpanded = ref(false);
function setPostExpanded(val: boolean) { function setPostExpanded(val: boolean) {

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, h, PropType, TransitionGroup } from "vue"; import { defineComponent, h, PropType, TransitionGroup } from "vue";
import MkAd from "@/components/global/MkAd.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
@ -12,7 +11,6 @@ export default defineComponent({
id: string; id: string;
createdAt?: string; createdAt?: string;
created_at?: string; created_at?: string;
_shouldInsertAd_: boolean;
}[] }[]
>, >,
required: true, required: true,
@ -102,20 +100,9 @@ export default defineComponent({
); );
return [el, separator]; return [el, separator];
} else {
if (props.ad && item._shouldInsertAd_) {
return [
h(MkAd, {
class: "a", // advertise()
key: item.id + ":ad",
prefer: ["inline", "inline-big"],
}),
el,
];
} else {
return el;
}
} }
return el;
}); });
return () => return () =>

View File

@ -67,22 +67,21 @@ import {
computed, computed,
ComputedRef, ComputedRef,
isRef, isRef,
markRaw,
onActivated, onActivated,
onDeactivated, onDeactivated,
Ref,
ref, ref,
watch, watch,
} from "vue"; } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import * as os from "@/os"; import * as os from "@/os";
import { import {
onScrollTop,
isTopVisible,
getScrollPosition,
getScrollContainer, getScrollContainer,
getScrollPosition,
isTopVisible,
onScrollTop,
} from "@/scripts/scroll"; } from "@/scripts/scroll";
import MkButton from "@/components/MkButton.vue"; import MkButton from "@/components/MkButton.vue";
import { magTransProperty } from "@/scripts-mag/mag-util";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
export type Paging< export type Paging<
@ -90,9 +89,11 @@ export type Paging<
> = { > = {
endpoint: E; endpoint: E;
limit: number; limit: number;
params?: params?: (
| misskey.Endpoints[E]["req"] | misskey.Endpoints[E]["req"]
| ComputedRef<misskey.Endpoints[E]["req"]>; | ComputedRef<misskey.Endpoints[E]["req"]>
) &
Record<string, unknown>;
/** /**
* 検索APIのようなページング不可なエンドポイントを利用する場合 * 検索APIのようなページング不可なエンドポイントを利用する場合
@ -125,7 +126,7 @@ const emit = defineEmits<{
(ev: "queue", count: number): void; (ev: "queue", count: number): void;
}>(); }>();
type Item = { id: string; [another: string]: unknown }; type Item = { id: string; createdAt?: string; created_at?: string } & any;
const rootEl = ref<HTMLElement>(); const rootEl = ref<HTMLElement>();
const items = ref<Item[]>([]); const items = ref<Item[]>([]);
@ -156,14 +157,6 @@ const init = async (): Promise<void> => {
}) })
.then( .then(
(res) => { (res) => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (props.pagination.reversed) {
if (i === res.length - 2) item._shouldInsertAd_ = true;
} else {
if (i === 3) item._shouldInsertAd_ = true;
}
}
if ( if (
!props.pagination.noPaging && !props.pagination.noPaging &&
res.length > (props.pagination.limit || 10) res.length > (props.pagination.limit || 10)
@ -195,7 +188,7 @@ const reload = (): void => {
init(); init();
}; };
const refresh = async (): void => { const refresh = async (): Promise<void> => {
const params = props.pagination.params const params = props.pagination.params
? isRef(props.pagination.params) ? isRef(props.pagination.params)
? props.pagination.params.value ? props.pagination.params.value
@ -258,22 +251,28 @@ const fetchMore = async (): Promise<void> => {
} }
: props.pagination.reversed : props.pagination.reversed
? { ? {
sinceDate: new Date(
magTransProperty(
items.value[0],
"createdAt",
"created_at"
)
).getTime(),
sinceId: items.value[0].id, sinceId: items.value[0].id,
} }
: { : {
untilDate: new Date(
magTransProperty(
items.value[items.value.length - 1],
"createdAt",
"created_at"
)
).getTime(),
untilId: items.value[items.value.length - 1].id, untilId: items.value[items.value.length - 1].id,
}), }),
}) })
.then( .then(
(res) => { (res) => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (props.pagination.reversed) {
if (i === res.length - 9) item._shouldInsertAd_ = true;
} else {
if (i === 10) item._shouldInsertAd_ = true;
}
}
if (res.length > SECOND_FETCH_LIMIT) { if (res.length > SECOND_FETCH_LIMIT) {
res.pop(); res.pop();
items.value = props.pagination.reversed items.value = props.pagination.reversed
@ -319,10 +318,24 @@ const fetchMoreAhead = async (): Promise<void> => {
} }
: props.pagination.reversed : props.pagination.reversed
? { ? {
untilDate: new Date(
magTransProperty(
items.value[0],
"createdAt",
"created_at"
)
),
untilId: items.value[0].id, untilId: items.value[0].id,
} }
: { : {
sinceId: items.value[items.value.length - 1].id, sinceDate: new Date(
magTransProperty(
items.value[items.value.length - 1],
"createdAt",
"created_at"
)
),
sinceId: items.value[items.value.length - 1].createdAt,
}), }),
}) })
.then( .then(

View File

@ -1,222 +0,0 @@
<template>
<div
v-if="chosen && chosen.length > 0 && defaultStore.state.showAds"
v-for="chosenItem in chosen"
class="qiivuoyo"
>
<div v-if="!showMenu" class="main" :class="chosenItem.place">
<a :href="chosenItem.url" target="_blank">
<img :src="chosenItem.imageUrl" />
</a>
</div>
</div>
<div v-else-if="chosen && defaultStore.state.showAds" class="qiivuoyo">
<div v-if="!showMenu" class="main" :class="chosen.place">
<a :href="chosen.url" target="_blank">
<img :src="chosen.imageUrl" />
<button class="_button menu" @click.prevent.stop="toggleMenu">
<span class="ph-info ph-bold ph-lg info-circle"></span>
</button>
</a>
</div>
<div v-else class="menu">
<div class="body">
<div>Ads by {{ host }}</div>
<!--<MkButton class="button" primary>{{ i18n.ts._ad.like }}</MkButton>-->
<MkButton
v-if="chosen.ratio !== 0"
class="button"
@click="reduceFrequency"
>{{ i18n.ts._ad.reduceFrequencyOfThisAd }}</MkButton
>
<button class="_textButton" @click="toggleMenu">
{{ i18n.ts._ad.back }}
</button>
</div>
</div>
</div>
<div v-else></div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { instance } from "@/instance";
import { host } from "@/config";
import MkButton from "@/components/MkButton.vue";
import { defaultStore } from "@/store";
import * as os from "@/os";
import { i18n } from "@/i18n";
type Ad = (typeof instance)["ads"][number];
const props = defineProps<{
prefer: string[];
specify?: Ad;
}>();
const showMenu = ref(false);
const toggleMenu = (): void => {
showMenu.value = !showMenu.value;
};
const choseAd = (): Ad | null => {
if (props.specify) {
return props.specify;
}
const allAds = instance.ads.map((ad) =>
defaultStore.state.mutedAds.includes(ad.id)
? {
...ad,
ratio: 0,
}
: ad
);
let ads = allAds.filter((ad) => props.prefer.includes(ad.place));
if (ads.length === 0) {
ads = allAds.filter((ad) => ad.place === "square");
}
const lowPriorityAds = ads.filter((ad) => ad.ratio === 0);
const widgetAds = ads.filter((ad) => ad.place === "widget");
ads = ads.filter((ad) => ad.ratio !== 0);
if (widgetAds.length !== 0) {
return widgetAds;
} else if (ads.length === 0) {
if (lowPriorityAds.length !== 0) {
return lowPriorityAds[
Math.floor(Math.random() * lowPriorityAds.length)
];
} else {
return null;
}
}
const totalFactor = ads.reduce((a, b) => a + b.ratio, 0);
const r = Math.random() * totalFactor;
let stackedFactor = 0;
for (const ad of ads) {
if (r >= stackedFactor && r <= stackedFactor + ad.ratio) {
return ad;
} else {
stackedFactor += ad.ratio;
}
}
return null;
};
const chosen = ref(choseAd());
function reduceFrequency(): void {
if (chosen.value == null) return;
if (defaultStore.state.mutedAds.includes(chosen.value.id)) return;
defaultStore.push("mutedAds", chosen.value.id);
os.success();
chosen.value = choseAd();
showMenu.value = false;
}
</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 {
text-align: center;
> a {
display: inline-block;
position: relative;
vertical-align: bottom;
&:hover {
> img {
filter: contrast(120%);
}
}
> img {
display: block;
object-fit: contain;
margin: auto;
border-radius: 5px;
}
> .menu {
position: absolute;
top: 1px;
right: 1px;
> .info-circle {
border: 3px solid var(--panel);
border-radius: 50%;
background: var(--panel);
}
}
}
&.widget {
> a,
> a > img {
max-width: min(300px, 100%);
max-height: 300px;
}
}
&.inline {
padding: 8px;
> a,
> a > img {
max-width: min(600px, 100%);
max-height: 80px;
}
}
&.inline-big {
padding: 8px;
> a,
> a > img {
max-width: min(600px, 100%);
max-height: 250px;
}
}
&.vertical {
> a,
> a > img {
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);
> .button {
margin: 8px auto;
}
}
}
}
</style>

View File

@ -57,7 +57,6 @@ import I18n from "./components/global/i18n";
import RouterView from "./components/global/RouterView.vue"; import RouterView from "./components/global/RouterView.vue";
import MkLoading from "./components/global/MkLoading.vue"; import MkLoading from "./components/global/MkLoading.vue";
import MkError from "./components/global/MkError.vue"; import MkError from "./components/global/MkError.vue";
import MkAd from "./components/global/MkAd.vue";
import MkPageHeader from "./components/global/MkPageHeader.vue"; import MkPageHeader from "./components/global/MkPageHeader.vue";
import MkSpacer from "./components/global/MkSpacer.vue"; import MkSpacer from "./components/global/MkSpacer.vue";
import MkStickyContainer from "./components/global/MkStickyContainer.vue"; import MkStickyContainer from "./components/global/MkStickyContainer.vue";
@ -76,7 +75,6 @@ function globalComponents(app: App) {
app.component("MkUrl", MkUrl); app.component("MkUrl", MkUrl);
app.component("MkLoading", MkLoading); app.component("MkLoading", MkLoading);
app.component("MkError", MkError); app.component("MkError", MkError);
app.component("MkAd", MkAd);
app.component("MkPageHeader", MkPageHeader); app.component("MkPageHeader", MkPageHeader);
app.component("MkSpacer", MkSpacer); app.component("MkSpacer", MkSpacer);
app.component("MkStickyContainer", MkStickyContainer); app.component("MkStickyContainer", MkStickyContainer);
@ -98,7 +96,6 @@ declare module "@vue/runtime-core" {
MkUrl: typeof MkUrl; MkUrl: typeof MkUrl;
MkLoading: typeof MkLoading; MkLoading: typeof MkLoading;
MkError: typeof MkError; MkError: typeof MkError;
MkAd: typeof MkAd;
MkPageHeader: typeof MkPageHeader; MkPageHeader: typeof MkPageHeader;
MkSpacer: typeof MkSpacer; MkSpacer: typeof MkSpacer;
MkStickyContainer: typeof MkStickyContainer; MkStickyContainer: typeof MkStickyContainer;

View File

@ -185,12 +185,6 @@ const menuDef = $computed(() => [
to: "/admin/announcements", to: "/admin/announcements",
active: currentPage?.route.name === "announcements", active: currentPage?.route.name === "announcements",
}, },
{
icon: "ph-money ph-bold ph-lg",
text: i18n.ts.ads,
to: "/admin/ads",
active: currentPage?.route.name === "ads",
},
{ {
icon: "ph-warning-circle ph-bold ph-lg", icon: "ph-warning-circle ph-bold ph-lg",
text: i18n.ts.abuseReports, text: i18n.ts.abuseReports,

View File

@ -1,158 +0,0 @@
<template>
<MkStickyContainer>
<template #header
><MkPageHeader
:actions="headerActions"
:tabs="headerTabs"
:display-back-button="true"
/></template>
<MkSpacer :content-max="900">
<div class="uqshojas">
<div v-for="ad in ads" class="_panel _formRoot ad">
<MkAd v-if="ad.url" :specify="ad" />
<MkInput v-model="ad.url" type="url" class="_formBlock">
<template #label>URL</template>
</MkInput>
<MkInput v-model="ad.imageUrl" class="_formBlock">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<FormRadios v-model="ad.place" class="_formBlock">
<template #label>Form</template>
<option value="widget">widget</option>
<option value="inline">inline</option>
<option value="inline-big">inline-big</option>
</FormRadios>
<FormSplit>
<MkInput
:disabled="ad.place === 'widget'"
v-model="ad.ratio"
type="number"
>
<template #label>{{ i18n.ts.ratio }}</template>
</MkInput>
<MkInput v-model="ad.expiresAt" type="date">
<template #label>{{ i18n.ts.expiration }}</template>
</MkInput>
</FormSplit>
<MkTextarea v-model="ad.memo" class="_formBlock">
<template #label>{{ i18n.ts.memo }}</template>
</MkTextarea>
<div class="buttons _formBlock">
<MkButton
class="button"
inline
primary
style="margin-right: 12px"
@click="save(ad)"
><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
{{ i18n.ts.save }}</MkButton
>
<MkButton
class="button"
inline
danger
@click="remove(ad)"
><i class="ph-trash ph-bold ph-lg"></i>
{{ i18n.ts.remove }}</MkButton
>
</div>
</div>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import {} from "vue";
import MkButton from "@/components/MkButton.vue";
import MkInput from "@/components/form/input.vue";
import MkTextarea from "@/components/form/textarea.vue";
import FormRadios from "@/components/form/radios.vue";
import FormSplit from "@/components/form/split.vue";
import * as os from "@/os";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
import { formatDateTimeString } from "@/scripts/format-time-string";
let ads: any[] = $ref([]);
os.api("admin/ad/list").then((adsResponse) => {
ads = adsResponse;
// The date format should be changed to yyyy-MM-dd in order to be properly displayed
for (let i in ads) {
ads[i].expiresAt = ads[i].expiresAt.substr(0, 10);
}
});
function add() {
const tomorrow = formatDateTimeString(
new Date(new Date().setDate(new Date().getDate() + 1)),
"yyyy-MM-dd"
);
ads.unshift({
id: null,
memo: "",
place: "widget",
priority: "middle",
ratio: 1,
url: "",
imageUrl: null,
expiresAt: tomorrow,
});
}
function remove(ad) {
os.confirm({
type: "warning",
text: i18n.t("removeAreYouSure", { x: ad.url }),
}).then(({ canceled }) => {
if (canceled) return;
ads = ads.filter((x) => x !== ad);
os.apiWithDialog("admin/ad/delete", {
id: ad.id,
});
});
}
function 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(),
});
}
}
const headerActions = $computed(() => [
{
asFullButton: true,
icon: "ph-plus ph-bold ph-lg",
text: i18n.ts.add,
handler: add,
},
]);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.ads,
icon: "ph-money ph-bold ph-lg",
});
</script>
<style lang="scss" scoped>
.uqshojas {
> .ad {
padding: 32px;
&:not(:last-child) {
margin-bottom: var(--margin);
}
}
}
</style>

View File

@ -114,7 +114,6 @@
/> />
</div> </div>
</div> </div>
<MkAd :prefer="['inline', 'inline-big']" />
<MkContainer <MkContainer
:max-height="300" :max-height="300"
:foldable="true" :foldable="true"

View File

@ -171,7 +171,6 @@
</template> </template>
</div> --> </div> -->
</div> </div>
<MkAd :prefer="['inline', 'inline-big']" />
<MkContainer <MkContainer
:max-height="300" :max-height="300"
:foldable="true" :foldable="true"

View File

@ -146,9 +146,6 @@
<FormSection> <FormSection>
<template #label>{{ i18n.ts.appearance }}</template> <template #label>{{ i18n.ts.appearance }}</template>
<FormSwitch v-model="showAds" class="_formBlock">{{
i18n.ts.showAds
}}</FormSwitch>
<FormSwitch v-model="useBlurEffect" class="_formBlock">{{ <FormSwitch v-model="useBlurEffect" class="_formBlock">{{
i18n.ts.useBlurEffect i18n.ts.useBlurEffect
}}</FormSwitch> }}</FormSwitch>

View File

@ -491,11 +491,6 @@ export const routes = [
() => import("./pages/admin/announcements.vue") () => import("./pages/admin/announcements.vue")
), ),
}, },
{
path: "/ads",
name: "ads",
component: page(() => import("./pages/admin/promotions.vue")),
},
{ {
path: "/database", path: "/database",
name: "database", name: "database",

View File

@ -11,7 +11,6 @@
>{{ column.name }}</template >{{ column.name }}</template
> >
<div class="wtdtxvec"> <div class="wtdtxvec">
<MkAd class="a" :prefer="['widget']" />
<div <div
v-if="!(column.widgets && column.widgets.length > 0) && !edit" v-if="!(column.widgets && column.widgets.length > 0) && !edit"
class="intro" class="intro"
@ -32,7 +31,6 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue";
import XColumn from "./column.vue"; import XColumn from "./column.vue";
import { import {
addColumnWidget, addColumnWidget,

View File

@ -1,6 +1,5 @@
<template> <template>
<aside class="widgets" :aria-label="i18n.ts._deck._columns.widgets"> <aside class="widgets" :aria-label="i18n.ts._deck._columns.widgets">
<MkAd class="a" :prefer="['widget']" />
<XWidgets <XWidgets
:edit="editMode" :edit="editMode"
:widgets="defaultStore.reactiveState.widgets.value" :widgets="defaultStore.reactiveState.widgets.value"