enhance(frontend): 「今日誕生日のフォロー中ユーザー」ウィジェットをリファクタリング
This commit is contained in:
parent
9723d01277
commit
24652b9364
18 changed files with 434 additions and 94 deletions
|
|
@ -29,6 +29,7 @@ const users = ref<Misskey.entities.UserLite[]>([]);
|
|||
onMounted(async () => {
|
||||
users.value = await misskeyApi('users/show', {
|
||||
userIds: props.userIds,
|
||||
detailed: false,
|
||||
}) as unknown as Misskey.entities.UserLite[];
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -4,43 +4,76 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkContainer :showHeader="widgetProps.showHeader" class="mkw-bdayfollowings">
|
||||
<MkContainer :style="`height: ${widgetProps.height}px;`" :showHeader="widgetProps.showHeader" :scrollable="true" class="mkw-bdayfollowings">
|
||||
<template #icon><i class="ti ti-cake"></i></template>
|
||||
<template #header>{{ i18n.ts._widgets.birthdayFollowings }}</template>
|
||||
<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="actualFetch()"><i class="ti ti-refresh"></i></button></template>
|
||||
<template v-if="widgetProps.period === 'today'" #header>{{ i18n.ts._widgets.birthdayFollowings }}</template>
|
||||
<template v-else #header>{{ i18n.ts._widgets.birthdaySoon }}</template>
|
||||
<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="fetch(true)"><i class="ti ti-refresh"></i></button></template>
|
||||
|
||||
<div :class="$style.bdayFRoot">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-else-if="users.length > 0" :class="$style.bdayFGrid">
|
||||
<MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar>
|
||||
</div>
|
||||
<div v-else :class="$style.bdayFFallback">
|
||||
<img :src="infoImageUrl" class="_ghost" :class="$style.bdayFFallbackImage"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkPagination ref="paginationEl" :pagination="birthdayUsersPagination">
|
||||
<template #empty>
|
||||
<div :class="$style.empty" :style="`height: ${widgetProps.showHeader ? widgetProps.height - 38 : widgetProps.height}px;`">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items: users }">
|
||||
<MkDateSeparatedList v-slot="{ item }" :items="toMisskeyEntity(users)" :noGap="true">
|
||||
<div v-if="item.user" :key="item.id" style="display: flex; gap: 8px; padding-right: 16px">
|
||||
<MkA :to="userPage(item.user)" style="flex-grow: 1;">
|
||||
<MkUserCardMini :user="item.user" :withChart="false" style="background: inherit; border-radius: unset;"/>
|
||||
</MkA>
|
||||
<button v-tooltip.noDelay="i18n.ts.note" class="_button" :class="$style.post" @click="os.post({initialText: `@${item.user.username}${item.user.host ? `@${item.user.host}` : ''} `})">
|
||||
<i class="ti-fw ti ti-confetti" :class="$style.postIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</MkDateSeparatedList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||
import { GetFormResultType } from '@/scripts/form.js';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import type { MisskeyEntity } from '@/types/date-separated-list.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { GetFormResultType } from '@/scripts/form.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
|
||||
const name = i18n.ts._widgets.birthdayFollowings;
|
||||
const name = i18n.ts._widgets.birthdaySoon;
|
||||
|
||||
const widgetPropsDef = {
|
||||
showHeader: {
|
||||
type: 'boolean' as const,
|
||||
default: true,
|
||||
},
|
||||
height: {
|
||||
type: 'number' as const,
|
||||
default: 300,
|
||||
},
|
||||
period: {
|
||||
type: 'radio' as const,
|
||||
default: 'today',
|
||||
options: [{
|
||||
value: 'today', label: i18n.ts.today,
|
||||
}, {
|
||||
value: '3day', label: i18n.tsx.dayX({ day: 3 }),
|
||||
}, {
|
||||
value: 'week', label: i18n.ts.oneWeek,
|
||||
}, {
|
||||
value: 'month', label: i18n.ts.oneMonth,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
|
@ -48,56 +81,73 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
|||
const props = defineProps<WidgetComponentProps<WidgetProps>>();
|
||||
const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
|
||||
|
||||
const { widgetProps, configure } = useWidgetPropsManager(name,
|
||||
const { widgetProps, configure } = useWidgetPropsManager(
|
||||
name,
|
||||
widgetPropsDef,
|
||||
props,
|
||||
emit,
|
||||
);
|
||||
|
||||
const users = ref<Misskey.Endpoints['users/following']['res']>([]);
|
||||
const fetching = ref(true);
|
||||
let lastFetchedAt = '1970-01-01';
|
||||
|
||||
const fetch = () => {
|
||||
if (!$i) {
|
||||
users.value = [];
|
||||
fetching.value = false;
|
||||
return;
|
||||
const begin = ref<Date>(new Date());
|
||||
const end = computed(() => {
|
||||
switch (widgetProps.period) {
|
||||
case '3day':
|
||||
return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 3);
|
||||
case 'week':
|
||||
return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 7);
|
||||
case 'month':
|
||||
return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 30);
|
||||
default:
|
||||
return begin.value;
|
||||
}
|
||||
});
|
||||
|
||||
const lfAtD = new Date(lastFetchedAt);
|
||||
lfAtD.setHours(0, 0, 0, 0);
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
if (now > lfAtD) {
|
||||
actualFetch();
|
||||
|
||||
lastFetchedAt = now.toISOString();
|
||||
}
|
||||
const paginationEl = ref<InstanceType<typeof MkPagination>>();
|
||||
const birthdayUsersPagination = {
|
||||
endpoint: 'users/get-following-birthday-users' as const,
|
||||
limit: 18,
|
||||
offsetMode: true,
|
||||
params: computed(() => {
|
||||
if (widgetProps.period === 'today') {
|
||||
return {
|
||||
birthday: {
|
||||
month: begin.value.getMonth() + 1,
|
||||
day: begin.value.getDate(),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
birthday: {
|
||||
begin: {
|
||||
month: begin.value.getMonth() + 1,
|
||||
day: begin.value.getDate(),
|
||||
},
|
||||
end: {
|
||||
month: end.value.getMonth() + 1,
|
||||
day: end.value.getDate(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
function actualFetch() {
|
||||
if ($i == null) {
|
||||
users.value = [];
|
||||
fetching.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
function fetch(force = false) {
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
fetching.value = true;
|
||||
misskeyApi('users/following', {
|
||||
limit: 18,
|
||||
birthday: `${now.getFullYear().toString().padStart(4, '0')}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`,
|
||||
userId: $i.id,
|
||||
}).then(res => {
|
||||
users.value = res;
|
||||
window.setTimeout(() => {
|
||||
// 早すぎるとチカチカする
|
||||
fetching.value = false;
|
||||
}, 100);
|
||||
});
|
||||
if (force || now.getDate() !== begin.value.getDate()) {
|
||||
// computed() で再評価されるので、paginationEl.value!.reload() は不要
|
||||
begin.value = now;
|
||||
}
|
||||
}
|
||||
|
||||
function toMisskeyEntity(items): MisskeyEntity[] {
|
||||
const r = items.map((item: { userId: string, birthday: string, user: Misskey.entities.UserLite }) => ({
|
||||
id: item.user.id,
|
||||
createdAt: item.birthday,
|
||||
user: item.user,
|
||||
}));
|
||||
|
||||
return [{ id: '_', createdAt: begin.value.toISOString() }, ...r];
|
||||
}
|
||||
|
||||
useInterval(fetch, 1000 * 60, {
|
||||
|
|
@ -113,32 +163,39 @@ defineExpose<WidgetComponentExpose>({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.bdayFRoot {
|
||||
overflow: hidden;
|
||||
min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--margin) * 2));
|
||||
}
|
||||
.bdayFGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 42px);
|
||||
grid-template-rows: repeat(3, 42px);
|
||||
place-content: center;
|
||||
gap: 8px;
|
||||
margin: var(--margin) auto;
|
||||
}
|
||||
|
||||
.bdayFFallback {
|
||||
height: 100%;
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> img {
|
||||
height: 96px;
|
||||
width: auto;
|
||||
max-width: 90%;
|
||||
margin-bottom: 8px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
}
|
||||
|
||||
.bdayFFallbackImage {
|
||||
height: 96px;
|
||||
width: auto;
|
||||
max-width: 90%;
|
||||
margin-bottom: 8px;
|
||||
border-radius: var(--radius);
|
||||
.post {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
margin: auto;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 100%;
|
||||
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||
|
||||
&:hover, &.active {
|
||||
&:before {
|
||||
background: var(--accentLighten);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.postIcon {
|
||||
color: var(--fgOnAccent);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue