diff options
Diffstat (limited to 'packages/frontend/src/widgets')
| -rw-r--r-- | packages/frontend/src/widgets/WidgetBirthdayFollowings.user.vue | 86 | ||||
| -rw-r--r-- | packages/frontend/src/widgets/WidgetBirthdayFollowings.vue | 198 |
2 files changed, 212 insertions, 72 deletions
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.user.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.user.vue new file mode 100644 index 0000000000..dc8ffcc818 --- /dev/null +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.user.vue @@ -0,0 +1,86 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root"> + <MkA :to="userPage(item.user)" style="overflow: clip;"> + <MkUserCardMini :user="item.user" :withChart="false" style="text-overflow: ellipsis; background: inherit; border-radius: unset;"> + <template #sub> + <span>{{ countdownDate }}</span> + <span> / </span> + <span class="_monospace">@{{ acct(item.user) }}</span> + </template> + </MkUserCardMini> + </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> +</template> + +<script setup lang="ts"> +import { computed } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkUserCardMini from '@/components/MkUserCardMini.vue'; +import * as os from '@/os.js'; +import { i18n } from '@/i18n.js'; +import { useLowresTime } from '@/composables/use-lowres-time.js'; +import { userPage, acct } from '@/filters/user.js'; + +const props = defineProps<{ + item: Misskey.entities.UsersGetFollowingBirthdayUsersResponse[number]; +}>(); + +const now = useLowresTime(); +const nowDate = computed(() => { + const date = new Date(now.value); + date.setHours(0, 0, 0, 0); + return date; +}); +const birthdayDate = computed(() => { + const [year, month, day] = props.item.birthday.split('-').map((v) => parseInt(v, 10)); + return new Date(year, month - 1, day, 0, 0, 0, 0); +}); + +const countdownDate = computed(() => { + const days = Math.floor((birthdayDate.value.getTime() - nowDate.value.getTime()) / (1000 * 60 * 60 * 24)); + if (days === 0) { + return i18n.ts.today; + } else if (days > 0) { + return i18n.tsx._timeIn.days({ n: days }); + } else { + return i18n.tsx._ago.daysAgo({ n: Math.abs(days) }); + } +}); +</script> + +<style lang="scss" module> +.root { + box-sizing: border-box; + display: grid; + align-items: center; + grid-template-columns: auto 56px; +} + +.post { + display: flex; + justify-content: center; + align-items: center; + height: 40px; + width: 40px; + margin-right: 16px; + aspect-ratio: 1/1; + border-radius: 100%; + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); + + &:hover { + background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); + } +} + +.postIcon { + color: var(--MI_THEME-fgOnAccent); +} +</style> diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index d270f12eaf..ea577f3219 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -4,34 +4,43 @@ 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 #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="fetch"><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> + <MkPagination v-slot="{ items }" :paginator="birthdayUsersPaginator"> + <div> + <template v-for="(user, i) in items" :key="user.id"> + <div + v-if="i > 0 && isSeparatorNeeded(birthdayUsersPaginator.items.value[i - 1].birthday, user.birthday)" + > + <div :class="$style.date"> + <span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(birthdayUsersPaginator.items.value[i - 1].birthday, user.birthday)?.prevText }}</span> + <span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span> + <span>{{ getSeparatorInfo(birthdayUsersPaginator.items.value[i - 1].birthday, user.birthday)?.nextText }} <i class="ti ti-chevron-down"></i></span> + </div> + <XUser :class="$style.user" :item="user" /> + </div> + <XUser v-else :class="$style.user" :item="user" /> + </template> </div> - <div v-else :class="$style.bdayFFallback"> - <MkResult type="empty"/> - </div> - </div> + </MkPagination> </MkContainer> </template> <script lang="ts" setup> -import { ref } from 'vue'; -import * as Misskey from 'misskey-js'; -import { useInterval } from '@@/js/use-interval.js'; +import { computed, markRaw, ref, watch } from 'vue'; +import { useLowresTime } from '@/composables/use-lowres-time.js'; +import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; -import { misskeyApi } from '@/utility/misskey-api.js'; +import MkPagination from '@/components/MkPagination.vue'; +import XUser from './WidgetBirthdayFollowings.user.vue'; import { i18n } from '@/i18n.js'; -import { $i } from '@/i.js'; +import { Paginator } from '@/utility/paginator.js'; const name = 'birthdayFollowings'; @@ -41,6 +50,29 @@ const widgetPropsDef = { label: i18n.ts._widgetOptions.showHeader, default: true, }, + height: { + type: 'number' as const, + label: i18n.ts._widgetOptions.height, + default: 300, + }, + period: { + type: 'radio' as const, + label: i18n.ts._widgetOptions._birthdayFollowings.period, + default: '3day', + options: [{ + value: 'today' as const, + label: i18n.ts.today, + }, { + value: '3day' as const, + label: i18n.tsx.dayX({ day: 3 }), + }, { + value: 'week' as const, + label: i18n.ts.oneWeek, + }, { + value: 'month' as const, + label: i18n.ts.oneMonth, + }], + }, } satisfies FormWithDefault; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; @@ -48,62 +80,84 @@ 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 now = useLowresTime(); +const nextDay = new Date(); +nextDay.setHours(24, 0, 0, 0); +let nextDayMidnightTime = nextDay.getTime(); -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 birthdayUsersPaginator = markRaw(new Paginator('users/get-following-birthday-users', { + limit: 18, + offsetMode: true, + computedParams: 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(), + }, + }, + }; + } + }), +})); - const lfAtD = new Date(lastFetchedAt); - lfAtD.setHours(0, 0, 0, 0); +function fetch() { const now = new Date(); - now.setHours(0, 0, 0, 0); + begin.value = now; +} - if (now > lfAtD) { - actualFetch(); +const UPDATE_INTERVAL = 1000 * 60; +let nextDayTimer: number | null = null; - lastFetchedAt = now.toISOString(); - } -}; +watch(now, (to) => { + // 次回更新までに日付が変わる場合、日付が変わった直後に強制的に更新するタイマーをセットする + if (nextDayMidnightTime - to <= UPDATE_INTERVAL) { + if (nextDayTimer != null) { + window.clearTimeout(nextDayTimer); + nextDayTimer = null; + } -function actualFetch() { - if ($i == null) { - users.value = []; - fetching.value = false; - return; + nextDayTimer = window.setTimeout(() => { + fetch(); + nextDay.setHours(24, 0, 0, 0); + nextDayMidnightTime = nextDay.getTime(); + nextDayTimer = null; + }, nextDayMidnightTime - to); } - - 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); - }); -} - -useInterval(fetch, 1000 * 60, { - immediate: true, - afterMounted: true, -}); +}, { immediate: true }); defineExpose<WidgetComponentExpose>({ name, @@ -113,24 +167,24 @@ defineExpose<WidgetComponentExpose>({ </script> <style lang="scss" module> -.bdayFRoot { - overflow: hidden; - min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--MI-margin) * 2)); +.root { + container-type: inline-size; + background: var(--MI_THEME-panel); } -.bdayFGrid { - display: grid; - grid-template-columns: repeat(6, 42px); - grid-template-rows: repeat(3, 42px); - place-content: center; - gap: 8px; - margin: var(--MI-margin) auto; + +.user { + border-bottom: solid 0.5px var(--MI_THEME-divider); } -.bdayFFallback { - height: 100%; +.date { display: flex; - flex-direction: column; - justify-content: center; + font-size: 85%; align-items: center; + justify-content: center; + gap: 1em; + opacity: 0.75; + padding: 8px 8px; + margin: 0 auto; + border-bottom: solid 0.5px var(--MI_THEME-divider); } </style> |