1
0
mirror of https://github.com/MisskeyIO/misskey synced 2024-11-27 14:28:49 +09:00

enhance(frontend): 「今日誕生日のフォロー中ユーザー」ウィジェットをリファクタリング

This commit is contained in:
まっちゃとーにゅ 2024-03-29 02:28:22 +09:00
parent 9723d01277
commit 24652b9364
No known key found for this signature in database
GPG Key ID: 6AFBBF529601C1DB
18 changed files with 434 additions and 94 deletions

View File

@ -2153,6 +2153,7 @@ _widgets:
chooseList: "Select a list" chooseList: "Select a list"
clicker: "Clicker" clicker: "Clicker"
birthdayFollowings: "Users who celebrate their birthday today" birthdayFollowings: "Users who celebrate their birthday today"
birthdaySoon: "Users who will celebrate their birthday soon"
_cw: _cw:
hide: "Hide" hide: "Hide"
show: "Show content" show: "Show content"

4
locales/index.d.ts vendored
View File

@ -8396,6 +8396,10 @@ export interface Locale extends ILocale {
* *
*/ */
"birthdayFollowings": string; "birthdayFollowings": string;
/**
*
*/
"birthdaySoon": string;
}; };
"_cw": { "_cw": {
/** /**

View File

@ -2207,6 +2207,7 @@ _widgets:
chooseList: "リストを選択" chooseList: "リストを選択"
clicker: "クリッカー" clicker: "クリッカー"
birthdayFollowings: "今日誕生日のユーザー" birthdayFollowings: "今日誕生日のユーザー"
birthdaySoon: "もうすぐ誕生日のユーザー"
_cw: _cw:
hide: "隠す" hide: "隠す"

View File

@ -2131,6 +2131,7 @@ _widgets:
chooseList: "리스트 선택" chooseList: "리스트 선택"
clicker: "클리커" clicker: "클리커"
birthdayFollowings: "오늘이 생일인 사용자" birthdayFollowings: "오늘이 생일인 사용자"
birthdaySoon: "곧 생일인 사용자"
_cw: _cw:
hide: "숨기기" hide: "숨기기"
show: "더 보기" show: "더 보기"

View File

@ -0,0 +1,15 @@
export class BirthdayIndex1711478468155 {
name = 'BirthdayIndex1711478468155'
async up(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_de22cd2b445eee31ae51cdbe99"`);
await queryRunner.query(`CREATE OR REPLACE FUNCTION get_birthday_date(birthday TEXT) RETURNS SMALLINT AS $$ BEGIN RETURN CAST((SUBSTR(birthday, 6, 2) || SUBSTR(birthday, 9, 2)) AS SMALLINT); END; $$ LANGUAGE plpgsql IMMUTABLE;`);
await queryRunner.query(`CREATE INDEX "IDX_USERPROFILE_BIRTHDAY_DATE" ON "user_profile" (get_birthday_date("birthday"))`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE INDEX "IDX_de22cd2b445eee31ae51cdbe99" ON "user_profile" (substr("birthday", 6, 5))`);
await queryRunner.query(`DROP INDEX "public"."IDX_USERPROFILE_BIRTHDAY_DATE"`);
await queryRunner.query(`DROP FUNCTION IF EXISTS get_birthday_date(birthday TEXT)`);
}
}

View File

@ -642,8 +642,8 @@ export class UserEntityService implements OnModuleInit {
// -- 特に前提条件のない値群を取得 // -- 特に前提条件のない値群を取得
const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) }) const profilesMap = (options?.schema !== 'UserLite') ? await this.userProfilesRepository.findBy({ userId: In(_userIds) })
.then(profiles => new Map(profiles.map(p => [p.userId, p]))); .then(profiles => new Map(profiles.map(p => [p.userId, p]))) : undefined;
// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得 // -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
@ -680,7 +680,7 @@ export class UserEntityService implements OnModuleInit {
} }
} }
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes })))) return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap?.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
.filter(result => result.status === 'fulfilled') .filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<Packed<S>>).value); .map(result => (result as PromiseFulfilledResult<Packed<S>>).value);
} }

View File

@ -348,6 +348,7 @@ import * as ep___users_clips from './endpoints/users/clips.js';
import * as ep___users_followers from './endpoints/users/followers.js'; import * as ep___users_followers from './endpoints/users/followers.js';
import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_following from './endpoints/users/following.js';
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
import * as ep___users_getFollowingBirthdayUsers from './endpoints/users/get-following-birthday-users.js';
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js'; import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js';
@ -733,6 +734,7 @@ const $users_clips: Provider = { provide: 'ep:users/clips', useClass: ep___users
const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep___users_followers.default }; const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep___users_followers.default };
const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default }; const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default };
const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default }; const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default };
const $users_getFollowingBirthdayUsers: Provider = { provide: 'ep:users/get-following-birthday-users', useClass: ep___users_getFollowingBirthdayUsers.default };
const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default }; const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default };
const $users_featuredNotes: Provider = { provide: 'ep:users/featured-notes', useClass: ep___users_featuredNotes.default }; const $users_featuredNotes: Provider = { provide: 'ep:users/featured-notes', useClass: ep___users_featuredNotes.default };
const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default }; const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default };
@ -1122,6 +1124,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$users_followers, $users_followers,
$users_following, $users_following,
$users_gallery_posts, $users_gallery_posts,
$users_getFollowingBirthdayUsers,
$users_getFrequentlyRepliedUsers, $users_getFrequentlyRepliedUsers,
$users_featuredNotes, $users_featuredNotes,
$users_lists_create, $users_lists_create,
@ -1503,6 +1506,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$users_followers, $users_followers,
$users_following, $users_following,
$users_gallery_posts, $users_gallery_posts,
$users_getFollowingBirthdayUsers,
$users_getFrequentlyRepliedUsers, $users_getFrequentlyRepliedUsers,
$users_featuredNotes, $users_featuredNotes,
$users_lists_create, $users_lists_create,

View File

@ -348,6 +348,7 @@ import * as ep___users_clips from './endpoints/users/clips.js';
import * as ep___users_followers from './endpoints/users/followers.js'; import * as ep___users_followers from './endpoints/users/followers.js';
import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_following from './endpoints/users/following.js';
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
import * as ep___users_getFollowingBirthdayUsers from './endpoints/users/get-following-birthday-users.js';
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js'; import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js';
@ -731,6 +732,7 @@ const eps = [
['users/followers', ep___users_followers], ['users/followers', ep___users_followers],
['users/following', ep___users_following], ['users/following', ep___users_following],
['users/gallery/posts', ep___users_gallery_posts], ['users/gallery/posts', ep___users_gallery_posts],
['users/get-following-birthday-users', ep___users_getFollowingBirthdayUsers],
['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers], ['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers],
['users/featured-notes', ep___users_featuredNotes], ['users/featured-notes', ep___users_featuredNotes],
['users/lists/create', ep___users_lists_create], ['users/lists/create', ep___users_lists_create],

View File

@ -67,7 +67,10 @@ export const paramDef = {
description: 'The local host is represented with `null`.', description: 'The local host is represented with `null`.',
}, },
birthday: { ...birthdaySchema, nullable: true }, birthday: {
...birthdaySchema, nullable: true,
description: '@deprecated use get-following-birthday-users instead.',
},
}, },
anyOf: [ anyOf: [
{ required: ['userId'] }, { required: ['userId'] },
@ -126,14 +129,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.andWhere('following.followerId = :userId', { userId: user.id }) .andWhere('following.followerId = :userId', { userId: user.id })
.innerJoinAndSelect('following.followee', 'followee'); .innerJoinAndSelect('following.followee', 'followee');
// @deprecated use get-following-birthday-users instead.
if (ps.birthday) { if (ps.birthday) {
try { query.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
const birthday = ps.birthday.substring(5, 10);
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
birthdayUserQuery.select('user_profile.userId')
.where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);
query.andWhere(`following.followeeId IN (${ birthdayUserQuery.getQuery() })`); try {
const birthday = ps.birthday.split('-');
birthday.shift(); // 年の部分を削除
// なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応
query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: parseInt(birthday.join('')) });
} catch (err) { } catch (err) {
throw new ApiError(meta.errors.birthdayInvalid); throw new ApiError(meta.errors.birthdayInvalid);
} }

View File

@ -0,0 +1,130 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type {
FollowingsRepository,
UserProfilesRepository,
} from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { Packed } from '@/misc/json-schema.js';
export const meta = {
tags: ['users'],
requireCredential: true,
kind: 'read:account',
description: 'Find users who have a birthday on the specified range.',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
birthday: {
type: 'string', format: 'date-time',
optional: false, nullable: false,
},
user: {
type: 'object',
optional: false, nullable: false,
ref: 'UserLite',
},
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
birthday: {
type: 'object',
properties: {
month: { type: 'integer', minimum: 1, maximum: 12 },
day: { type: 'integer', minimum: 1, maximum: 31 },
begin: {
type: 'object',
properties: {
month: { type: 'integer', minimum: 1, maximum: 12 },
day: { type: 'integer', minimum: 1, maximum: 31 },
},
required: ['month', 'day'],
},
end: {
type: 'object',
properties: {
month: { type: 'integer', minimum: 1, maximum: 12 },
day: { type: 'integer', minimum: 1, maximum: 31 },
},
required: ['month', 'day'],
},
},
anyOf: [
{ required: ['month', 'day'] },
{ required: ['begin', 'end'] },
],
},
},
required: ['birthday'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.followingsRepository
.createQueryBuilder('following')
.andWhere('following.followerId = :userId', { userId: me.id })
.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
if (Object.hasOwn(ps.birthday, 'begin') && Object.hasOwn(ps.birthday, 'end')) {
const { begin, end } = ps.birthday as { begin: { month: number; day: number }; end: { month: number; day: number }; };
query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND :end', { begin: begin.month * 100 + begin.day, end: end.month * 100 + end.day });
} else {
const { month, day } = ps.birthday as { month: number; day: number };
// なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応
query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: month * 100 + day });
}
query.select('following.followeeId', 'user_id');
query.addSelect('get_birthday_date(followeeProfile.birthday)', 'birthday_date');
query.orderBy('birthday_date', 'ASC');
const birthdayUsers = await query
.offset(ps.offset).limit(ps.limit)
.getRawMany<{ birthday_date: number; user_id: string }>();
const users = new Map<string, Packed<'UserLite'>>((
await this.userEntityService.packMany(
birthdayUsers.map(u => u.user_id),
me,
{ schema: 'UserLite' },
)
).map(u => [u.id, u]));
return birthdayUsers
.map(item => {
const birthday = new Date();
birthday.setMonth(Math.floor(item.birthday_date / 100) - 1);
birthday.setDate(item.birthday_date % 100);
birthday.setHours(0, 0, 0, 0);
if (birthday.getTime() < Date.now()) birthday.setFullYear(new Date().getFullYear() + 1);
return { birthday: birthday.toISOString(), user: users.get(item.user_id) };
})
.filter(item => item.user !== undefined)
.map(item => item as { birthday: string; user: Packed<'UserLite'> });
});
}
}

View File

@ -27,10 +27,21 @@ export const meta = {
res: { res: {
optional: false, nullable: false, optional: false, nullable: false,
oneOf: [ oneOf: [
{
type: 'object',
ref: 'UserLite',
},
{ {
type: 'object', type: 'object',
ref: 'UserDetailed', ref: 'UserDetailed',
}, },
{
type: 'array',
items: {
type: 'object',
ref: 'UserLite',
},
},
{ {
type: 'array', type: 'array',
items: { items: {
@ -71,6 +82,7 @@ export const paramDef = {
nullable: true, nullable: true,
description: 'The local host is represented with `null`.', description: 'The local host is represented with `null`.',
}, },
detailed: { type: 'boolean', default: true },
}, },
anyOf: [ anyOf: [
{ required: ['userId'] }, { required: ['userId'] },
@ -117,7 +129,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
return await this.userEntityService.packMany(_users, me, { return await this.userEntityService.packMany(_users, me, {
schema: 'UserDetailed', schema: ps.detailed ? 'UserDetailed' : 'UserLite',
}); });
} else { } else {
// Lookup user // Lookup user
@ -147,7 +159,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
return await this.userEntityService.pack(user, me, { return await this.userEntityService.pack(user, me, {
schema: 'UserDetailed', schema: ps.detailed ? 'UserDetailed' : 'UserLite',
}); });
} }
}); });

View File

@ -29,6 +29,7 @@ const users = ref<Misskey.entities.UserLite[]>([]);
onMounted(async () => { onMounted(async () => {
users.value = await misskeyApi('users/show', { users.value = await misskeyApi('users/show', {
userIds: props.userIds, userIds: props.userIds,
detailed: false,
}) as unknown as Misskey.entities.UserLite[]; }) as unknown as Misskey.entities.UserLite[];
}); });
</script> </script>

View File

@ -4,43 +4,76 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <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 #icon><i class="ti ti-cake"></i></template>
<template #header>{{ i18n.ts._widgets.birthdayFollowings }}</template> <template v-if="widgetProps.period === 'today'" #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-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"> <MkPagination ref="paginationEl" :pagination="birthdayUsersPagination">
<MkLoading v-if="fetching"/> <template #empty>
<div v-else-if="users.length > 0" :class="$style.bdayFGrid"> <div :class="$style.empty" :style="`height: ${widgetProps.showHeader ? widgetProps.height - 38 : widgetProps.height}px;`">
<MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar> <img :src="infoImageUrl" class="_ghost"/>
</div> <div>{{ i18n.ts.nothing }}</div>
<div v-else :class="$style.bdayFFallback"> </div>
<img :src="infoImageUrl" class="_ghost" :class="$style.bdayFFallbackImage"/> </template>
<div>{{ i18n.ts.nothing }}</div>
</div> <template #default="{ items: users }">
</div> <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> </MkContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js'; import type { MisskeyEntity } from '@/types/date-separated-list.js';
import MkContainer from '@/components/MkContainer.vue'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { useInterval } from '@/scripts/use-interval.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { userPage } from '@/filters/user.js';
import { infoImageUrl } from '@/instance.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 = { const widgetPropsDef = {
showHeader: { showHeader: {
type: 'boolean' as const, type: 'boolean' as const,
default: true, 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>; type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
@ -48,56 +81,73 @@ type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
const props = defineProps<WidgetComponentProps<WidgetProps>>(); const props = defineProps<WidgetComponentProps<WidgetProps>>();
const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const { widgetProps, configure } = useWidgetPropsManager(name, const { widgetProps, configure } = useWidgetPropsManager(
name,
widgetPropsDef, widgetPropsDef,
props, props,
emit, emit,
); );
const users = ref<Misskey.Endpoints['users/following']['res']>([]); const begin = ref<Date>(new Date());
const fetching = ref(true); const end = computed(() => {
let lastFetchedAt = '1970-01-01'; switch (widgetProps.period) {
case '3day':
const fetch = () => { return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 3);
if (!$i) { case 'week':
users.value = []; return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 7);
fetching.value = false; case 'month':
return; return new Date(begin.value.getTime() + 1000 * 60 * 60 * 24 * 30);
default:
return begin.value;
} }
});
const lfAtD = new Date(lastFetchedAt); const paginationEl = ref<InstanceType<typeof MkPagination>>();
lfAtD.setHours(0, 0, 0, 0); const birthdayUsersPagination = {
const now = new Date(); endpoint: 'users/get-following-birthday-users' as const,
now.setHours(0, 0, 0, 0); limit: 18,
offsetMode: true,
if (now > lfAtD) { params: computed(() => {
actualFetch(); if (widgetProps.period === 'today') {
return {
lastFetchedAt = now.toISOString(); 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() { function fetch(force = false) {
if ($i == null) {
users.value = [];
fetching.value = false;
return;
}
const now = new Date(); const now = new Date();
now.setHours(0, 0, 0, 0); if (force || now.getDate() !== begin.value.getDate()) {
fetching.value = true; // computed() paginationEl.value!.reload()
misskeyApi('users/following', { begin.value = now;
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 => { function toMisskeyEntity(items): MisskeyEntity[] {
users.value = res; const r = items.map((item: { userId: string, birthday: string, user: Misskey.entities.UserLite }) => ({
window.setTimeout(() => { id: item.user.id,
// createdAt: item.birthday,
fetching.value = false; user: item.user,
}, 100); }));
});
return [{ id: '_', createdAt: begin.value.toISOString() }, ...r];
} }
useInterval(fetch, 1000 * 60, { useInterval(fetch, 1000 * 60, {
@ -113,32 +163,39 @@ defineExpose<WidgetComponentExpose>({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.bdayFRoot { .empty {
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%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
> img {
height: 96px;
width: auto;
max-width: 90%;
margin-bottom: 8px;
border-radius: var(--radius);
}
} }
.bdayFFallbackImage { .post {
height: 96px; display: flex;
width: auto; justify-content: center;
max-width: 90%; align-items: center;
margin-bottom: 8px; height: 40px;
border-radius: var(--radius); 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> </style>

View File

@ -1686,6 +1686,8 @@ declare namespace entities {
UsersFollowingResponse, UsersFollowingResponse,
UsersGalleryPostsRequest, UsersGalleryPostsRequest,
UsersGalleryPostsResponse, UsersGalleryPostsResponse,
UsersGetFollowingBirthdayUsersRequest,
UsersGetFollowingBirthdayUsersResponse,
UsersGetFrequentlyRepliedUsersRequest, UsersGetFrequentlyRepliedUsersRequest,
UsersGetFrequentlyRepliedUsersResponse, UsersGetFrequentlyRepliedUsersResponse,
UsersFeaturedNotesRequest, UsersFeaturedNotesRequest,
@ -3091,6 +3093,12 @@ type UsersGalleryPostsRequest = operations['users___gallery___posts']['requestBo
// @public (undocumented) // @public (undocumented)
type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json']; type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json'];
// @public (undocumented)
type UsersGetFollowingBirthdayUsersRequest = operations['users___get-following-birthday-users']['requestBody']['content']['application/json'];
// @public (undocumented)
type UsersGetFollowingBirthdayUsersResponse = operations['users___get-following-birthday-users']['responses']['200']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json']; type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json'];

View File

@ -3804,6 +3804,17 @@ declare module '../api.js' {
credential?: string | null, credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>; ): Promise<SwitchCaseResponseType<E, P>>;
/**
* Find users who have a birthday on the specified range.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
request<E extends 'users/get-following-birthday-users', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/** /**
* Get a list of other users that the specified user frequently replies to. * Get a list of other users that the specified user frequently replies to.
* *

View File

@ -506,6 +506,8 @@ import type {
UsersFollowingResponse, UsersFollowingResponse,
UsersGalleryPostsRequest, UsersGalleryPostsRequest,
UsersGalleryPostsResponse, UsersGalleryPostsResponse,
UsersGetFollowingBirthdayUsersRequest,
UsersGetFollowingBirthdayUsersResponse,
UsersGetFrequentlyRepliedUsersRequest, UsersGetFrequentlyRepliedUsersRequest,
UsersGetFrequentlyRepliedUsersResponse, UsersGetFrequentlyRepliedUsersResponse,
UsersFeaturedNotesRequest, UsersFeaturedNotesRequest,
@ -916,6 +918,7 @@ export type Endpoints = {
'users/followers': { req: UsersFollowersRequest; res: UsersFollowersResponse }; 'users/followers': { req: UsersFollowersRequest; res: UsersFollowersResponse };
'users/following': { req: UsersFollowingRequest; res: UsersFollowingResponse }; 'users/following': { req: UsersFollowingRequest; res: UsersFollowingResponse };
'users/gallery/posts': { req: UsersGalleryPostsRequest; res: UsersGalleryPostsResponse }; 'users/gallery/posts': { req: UsersGalleryPostsRequest; res: UsersGalleryPostsResponse };
'users/get-following-birthday-users': { req: UsersGetFollowingBirthdayUsersRequest; res: UsersGetFollowingBirthdayUsersResponse };
'users/get-frequently-replied-users': { req: UsersGetFrequentlyRepliedUsersRequest; res: UsersGetFrequentlyRepliedUsersResponse }; 'users/get-frequently-replied-users': { req: UsersGetFrequentlyRepliedUsersRequest; res: UsersGetFrequentlyRepliedUsersResponse };
'users/featured-notes': { req: UsersFeaturedNotesRequest; res: UsersFeaturedNotesResponse }; 'users/featured-notes': { req: UsersFeaturedNotesRequest; res: UsersFeaturedNotesResponse };
'users/lists/create': { req: UsersListsCreateRequest; res: UsersListsCreateResponse }; 'users/lists/create': { req: UsersListsCreateRequest; res: UsersListsCreateResponse };

View File

@ -509,6 +509,8 @@ export type UsersFollowingRequest = operations['users___following']['requestBody
export type UsersFollowingResponse = operations['users___following']['responses']['200']['content']['application/json']; export type UsersFollowingResponse = operations['users___following']['responses']['200']['content']['application/json'];
export type UsersGalleryPostsRequest = operations['users___gallery___posts']['requestBody']['content']['application/json']; export type UsersGalleryPostsRequest = operations['users___gallery___posts']['requestBody']['content']['application/json'];
export type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json']; export type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json'];
export type UsersGetFollowingBirthdayUsersRequest = operations['users___get-following-birthday-users']['requestBody']['content']['application/json'];
export type UsersGetFollowingBirthdayUsersResponse = operations['users___get-following-birthday-users']['responses']['200']['content']['application/json'];
export type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json']; export type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json'];
export type UsersGetFrequentlyRepliedUsersResponse = operations['users___get-frequently-replied-users']['responses']['200']['content']['application/json']; export type UsersGetFrequentlyRepliedUsersResponse = operations['users___get-frequently-replied-users']['responses']['200']['content']['application/json'];
export type UsersFeaturedNotesRequest = operations['users___featured-notes']['requestBody']['content']['application/json']; export type UsersFeaturedNotesRequest = operations['users___featured-notes']['requestBody']['content']['application/json'];

View File

@ -3276,6 +3276,15 @@ export type paths = {
*/ */
post: operations['users___gallery___posts']; post: operations['users___gallery___posts'];
}; };
'/users/get-following-birthday-users': {
/**
* users/get-following-birthday-users
* @description Find users who have a birthday on the specified range.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
post: operations['users___get-following-birthday-users'];
};
'/users/get-frequently-replied-users': { '/users/get-frequently-replied-users': {
/** /**
* users/get-frequently-replied-users * users/get-frequently-replied-users
@ -25425,6 +25434,7 @@ export type operations = {
username?: string; username?: string;
/** @description The local host is represented with `null`. */ /** @description The local host is represented with `null`. */
host?: string | null; host?: string | null;
/** @description @deprecated use get-following-birthday-users instead. */
birthday?: string | null; birthday?: string | null;
}; };
}; };
@ -25528,6 +25538,78 @@ export type operations = {
}; };
}; };
}; };
/**
* users/get-following-birthday-users
* @description Find users who have a birthday on the specified range.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
'users___get-following-birthday-users': {
requestBody: {
content: {
'application/json': {
/** @default 10 */
limit?: number;
/** @default 0 */
offset?: number;
birthday: {
month?: number;
day?: number;
begin?: {
month: number;
day: number;
};
end?: {
month: number;
day: number;
};
};
};
};
};
responses: {
/** @description OK (with results) */
200: {
content: {
'application/json': {
/** Format: date-time */
birthday: string;
user: components['schemas']['UserLite'];
}[];
};
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/** /**
* users/get-frequently-replied-users * users/get-frequently-replied-users
* @description Get a list of other users that the specified user frequently replies to. * @description Get a list of other users that the specified user frequently replies to.
@ -26896,6 +26978,8 @@ export type operations = {
username?: string; username?: string;
/** @description The local host is represented with `null`. */ /** @description The local host is represented with `null`. */
host?: string | null; host?: string | null;
/** @default true */
detailed?: boolean;
}; };
}; };
}; };
@ -26903,7 +26987,7 @@ export type operations = {
/** @description OK (with results) */ /** @description OK (with results) */
200: { 200: {
content: { content: {
'application/json': components['schemas']['UserDetailed'] | components['schemas']['UserDetailed'][]; 'application/json': components['schemas']['UserLite'] | components['schemas']['UserDetailed'] | components['schemas']['UserLite'][] | components['schemas']['UserDetailed'][];
}; };
}; };
/** @description Client error */ /** @description Client error */