feat(frontend): 今日誕生日のフォロー中のユーザーを一覧表示できるウィジェットを追加 (#12450)
* (add) 今日誕生日のフォロイー一覧表示 * Update Changelog * Update Changelog * 実装漏れ * create index * (fix) index
This commit is contained in:
parent
22d6fa1fdf
commit
b05d71fabf
|
@ -21,6 +21,7 @@
|
||||||
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
|
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- Feat: 今日誕生日のフォロー中のユーザーを一覧表示できるウィジェットを追加
|
||||||
- Enhance: 絵文字のオートコンプリート機能強化 #12364
|
- Enhance: 絵文字のオートコンプリート機能強化 #12364
|
||||||
- Enhance: ユーザーのRawデータを表示するページが復活
|
- Enhance: ユーザーのRawデータを表示するページが復活
|
||||||
- Enhance: リアクション選択時に音を鳴らせるように
|
- Enhance: リアクション選択時に音を鳴らせるように
|
||||||
|
|
1
locales/index.d.ts
vendored
1
locales/index.d.ts
vendored
|
@ -2110,6 +2110,7 @@ export interface Locale {
|
||||||
"chooseList": string;
|
"chooseList": string;
|
||||||
};
|
};
|
||||||
"clicker": string;
|
"clicker": string;
|
||||||
|
"birthdayFollowings": string;
|
||||||
};
|
};
|
||||||
"_cw": {
|
"_cw": {
|
||||||
"hide": string;
|
"hide": string;
|
||||||
|
|
|
@ -2014,6 +2014,7 @@ _widgets:
|
||||||
_userList:
|
_userList:
|
||||||
chooseList: "リストを選択"
|
chooseList: "リストを選択"
|
||||||
clicker: "クリッカー"
|
clicker: "クリッカー"
|
||||||
|
birthdayFollowings: "今日誕生日のユーザー"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
|
|
16
packages/backend/migration/1700902349231-add-bday-index.js
Normal file
16
packages/backend/migration/1700902349231-add-bday-index.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class AddBdayIndex1700902349231 {
|
||||||
|
name = 'AddBdayIndex1700902349231'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_de22cd2b445eee31ae51cdbe99" ON "user_profile" (SUBSTR("birthday", 6, 5))`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_de22cd2b445eee31ae51cdbe99"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ export class MiUserProfile {
|
||||||
})
|
})
|
||||||
public location: string | null;
|
public location: string | null;
|
||||||
|
|
||||||
|
@Index()
|
||||||
@Column('char', {
|
@Column('char', {
|
||||||
length: 10, nullable: true,
|
length: 10, nullable: true,
|
||||||
comment: 'The birthday (YYYY-MM-DD) of the User.',
|
comment: 'The birthday (YYYY-MM-DD) of the User.',
|
||||||
|
|
|
@ -42,6 +42,12 @@ export const meta = {
|
||||||
code: 'FORBIDDEN',
|
code: 'FORBIDDEN',
|
||||||
id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba',
|
id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
birthdayInvalid: {
|
||||||
|
message: 'Birthday date format is invalid.',
|
||||||
|
code: 'BIRTHDAY_DATE_FORMAT_INVALID',
|
||||||
|
id: 'a2b007b9-4782-4eba-abd3-93b05ed4130d',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -59,6 +65,8 @@ export const paramDef = {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'The local host is represented with `null`.',
|
description: 'The local host is represented with `null`.',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
birthday: { type: 'string', nullable: true },
|
||||||
},
|
},
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{ required: ['userId'] },
|
{ required: ['userId'] },
|
||||||
|
@ -117,6 +125,21 @@ 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');
|
||||||
|
|
||||||
|
if (ps.birthday) {
|
||||||
|
try {
|
||||||
|
const d = new Date(ps.birthday);
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
|
||||||
|
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() })`);
|
||||||
|
} catch (err) {
|
||||||
|
throw new ApiError(meta.errors.birthdayInvalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const followings = await query
|
const followings = await query
|
||||||
.limit(ps.limit)
|
.limit(ps.limit)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
127
packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
Normal file
127
packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkContainer :showHeader="widgetProps.showHeader" class="mkw-bdayfollowings">
|
||||||
|
<template #icon><i class="ti ti-cake"></i></template>
|
||||||
|
<template #header>{{ i18n.ts._widgets.birthdayFollowings }}</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>
|
||||||
|
</MkContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { 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 * as os from '@/os.js';
|
||||||
|
import { useInterval } from '@/scripts/use-interval.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
|
const name = i18n.ts._widgets.birthdayFollowings;
|
||||||
|
|
||||||
|
const widgetPropsDef = {
|
||||||
|
showHeader: {
|
||||||
|
type: 'boolean' as const,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
const props = defineProps<WidgetComponentProps<WidgetProps>>();
|
||||||
|
const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
|
||||||
|
|
||||||
|
const { widgetProps, configure } = useWidgetPropsManager(name,
|
||||||
|
widgetPropsDef,
|
||||||
|
props,
|
||||||
|
emit,
|
||||||
|
);
|
||||||
|
|
||||||
|
const users = ref<Misskey.entities.FollowingFolloweePopulated[]>([]);
|
||||||
|
const fetching = ref(true);
|
||||||
|
let lastFetchedAt = '1970-01-01';
|
||||||
|
|
||||||
|
const fetch = () => {
|
||||||
|
if (!$i) {
|
||||||
|
users.value = [];
|
||||||
|
fetching.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lfAtD = new Date(lastFetchedAt);
|
||||||
|
lfAtD.setHours(0, 0, 0, 0);
|
||||||
|
const now = new Date();
|
||||||
|
now.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (now > lfAtD) {
|
||||||
|
os.api('users/following', {
|
||||||
|
limit: 18,
|
||||||
|
birthday: now.toISOString(),
|
||||||
|
userId: $i.id,
|
||||||
|
}).then(res => {
|
||||||
|
users.value = res;
|
||||||
|
fetching.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
lastFetchedAt = now.toISOString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useInterval(fetch, 1000 * 60, {
|
||||||
|
immediate: true,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose<WidgetComponentExpose>({
|
||||||
|
name,
|
||||||
|
configure,
|
||||||
|
id: props.widget ? props.widget.id : null,
|
||||||
|
});
|
||||||
|
</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%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bdayFFallbackImage {
|
||||||
|
height: 96px;
|
||||||
|
width: auto;
|
||||||
|
max-width: 90%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -33,6 +33,7 @@ export default function(app: App) {
|
||||||
app.component('WidgetAichan', defineAsyncComponent(() => import('./WidgetAichan.vue')));
|
app.component('WidgetAichan', defineAsyncComponent(() => import('./WidgetAichan.vue')));
|
||||||
app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue')));
|
app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue')));
|
||||||
app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue')));
|
app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue')));
|
||||||
|
app.component('WidgetBirthdayFollowings', defineAsyncComponent(() => import('./WidgetBirthdayFollowings.vue')));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const widgets = [
|
export const widgets = [
|
||||||
|
@ -63,4 +64,5 @@ export const widgets = [
|
||||||
'aichan',
|
'aichan',
|
||||||
'userList',
|
'userList',
|
||||||
'clicker',
|
'clicker',
|
||||||
|
'birthdayFollowings',
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in a new issue