update
This commit is contained in:
parent
5e140a7ff5
commit
509181a6d0
|
@ -436,6 +436,10 @@ export interface Locale extends ILocale {
|
||||||
* フォロワー
|
* フォロワー
|
||||||
*/
|
*/
|
||||||
"followers": string;
|
"followers": string;
|
||||||
|
/**
|
||||||
|
* プリズム
|
||||||
|
*/
|
||||||
|
"points": string;
|
||||||
/**
|
/**
|
||||||
* フォローされています
|
* フォローされています
|
||||||
*/
|
*/
|
||||||
|
@ -9291,6 +9295,10 @@ export interface Locale extends ILocale {
|
||||||
* 実績を獲得
|
* 実績を獲得
|
||||||
*/
|
*/
|
||||||
"achievementEarned": string;
|
"achievementEarned": string;
|
||||||
|
/**
|
||||||
|
* ログインボーナス
|
||||||
|
*/
|
||||||
|
"loginbonus": string;
|
||||||
/**
|
/**
|
||||||
* 通知テスト
|
* 通知テスト
|
||||||
*/
|
*/
|
||||||
|
@ -9380,6 +9388,10 @@ export interface Locale extends ILocale {
|
||||||
* 実績の獲得
|
* 実績の獲得
|
||||||
*/
|
*/
|
||||||
"achievementEarned": string;
|
"achievementEarned": string;
|
||||||
|
/**
|
||||||
|
* ログインボーナス
|
||||||
|
*/
|
||||||
|
"loginbonus": string;
|
||||||
/**
|
/**
|
||||||
* 連携アプリからの通知
|
* 連携アプリからの通知
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -105,6 +105,7 @@ note: "ノート"
|
||||||
notes: "ノート"
|
notes: "ノート"
|
||||||
following: "フォロー"
|
following: "フォロー"
|
||||||
followers: "フォロワー"
|
followers: "フォロワー"
|
||||||
|
points: "プリズム"
|
||||||
followsYou: "フォローされています"
|
followsYou: "フォローされています"
|
||||||
createList: "リスト作成"
|
createList: "リスト作成"
|
||||||
manageLists: "リストの管理"
|
manageLists: "リストの管理"
|
||||||
|
@ -2452,6 +2453,7 @@ _notification:
|
||||||
roleAssigned: "ロールが付与されました"
|
roleAssigned: "ロールが付与されました"
|
||||||
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
||||||
achievementEarned: "実績を獲得"
|
achievementEarned: "実績を獲得"
|
||||||
|
loginbonus: "ログインボーナス"
|
||||||
testNotification: "通知テスト"
|
testNotification: "通知テスト"
|
||||||
checkNotificationBehavior: "通知の表示を確かめる"
|
checkNotificationBehavior: "通知の表示を確かめる"
|
||||||
sendTestNotification: "テスト通知を送信する"
|
sendTestNotification: "テスト通知を送信する"
|
||||||
|
@ -2476,6 +2478,7 @@ _notification:
|
||||||
followRequestAccepted: "フォローが受理された"
|
followRequestAccepted: "フォローが受理された"
|
||||||
roleAssigned: "ロールが付与された"
|
roleAssigned: "ロールが付与された"
|
||||||
achievementEarned: "実績の獲得"
|
achievementEarned: "実績の獲得"
|
||||||
|
loginbonus: "ログインボーナス"
|
||||||
app: "連携アプリからの通知"
|
app: "連携アプリからの通知"
|
||||||
|
|
||||||
_actions:
|
_actions:
|
||||||
|
|
|
@ -163,6 +163,9 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
...(notification.type === 'achievementEarned' ? {
|
...(notification.type === 'achievementEarned' ? {
|
||||||
achievement: notification.achievement,
|
achievement: notification.achievement,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
...(notification.type === 'loginbonus' ? {
|
||||||
|
loginbonus: notification.loginbonus,
|
||||||
|
} : {}),
|
||||||
...(notification.type === 'app' ? {
|
...(notification.type === 'app' ? {
|
||||||
body: notification.customBody,
|
body: notification.customBody,
|
||||||
header: notification.customHeader,
|
header: notification.customHeader,
|
||||||
|
|
|
@ -404,6 +404,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
userRelations?: Map<MiUser['id'], UserRelation>,
|
userRelations?: Map<MiUser['id'], UserRelation>,
|
||||||
userMemos?: Map<MiUser['id'], string | null>,
|
userMemos?: Map<MiUser['id'], string | null>,
|
||||||
pinNotes?: Map<MiUser['id'], MiUserNotePining[]>,
|
pinNotes?: Map<MiUser['id'], MiUserNotePining[]>,
|
||||||
|
todayGetPoints?: number,
|
||||||
},
|
},
|
||||||
): Promise<Packed<S>> {
|
): Promise<Packed<S>> {
|
||||||
const opts = Object.assign({
|
const opts = Object.assign({
|
||||||
|
@ -507,7 +508,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
iconUrl: r.iconUrl,
|
iconUrl: r.iconUrl,
|
||||||
displayOrder: r.displayOrder,
|
displayOrder: r.displayOrder,
|
||||||
}))) : undefined,
|
}))) : undefined,
|
||||||
|
...(user.host == null ? { getPoints: profile!.getPoints } : {}),
|
||||||
...(isDetailed ? {
|
...(isDetailed ? {
|
||||||
url: profile!.url,
|
url: profile!.url,
|
||||||
uri: user.uri,
|
uri: user.uri,
|
||||||
|
@ -602,6 +603,9 @@ export class UserEntityService implements OnModuleInit {
|
||||||
achievements: profile!.achievements,
|
achievements: profile!.achievements,
|
||||||
loggedInDays: profile!.loggedInDates.length,
|
loggedInDays: profile!.loggedInDates.length,
|
||||||
policies: this.roleService.getUserPolicies(user.id),
|
policies: this.roleService.getUserPolicies(user.id),
|
||||||
|
...(opts.todayGetPoints ? {
|
||||||
|
todayGetPoints: opts.todayGetPoints,
|
||||||
|
} : {}),
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
...(opts.includeSecrets ? {
|
...(opts.includeSecrets ? {
|
||||||
|
|
|
@ -261,6 +261,10 @@ export class MiUserProfile {
|
||||||
length: 32, array: true, default: '{}',
|
length: 32, array: true, default: '{}',
|
||||||
})
|
})
|
||||||
public loggedInDates: string[];
|
public loggedInDates: string[];
|
||||||
|
@Column('integer', {
|
||||||
|
default: '0',
|
||||||
|
})
|
||||||
|
public getPoints: number;
|
||||||
|
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: [],
|
default: [],
|
||||||
|
|
|
@ -8,13 +8,14 @@ import type { UserProfilesRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { ApiError } from '../error.js';
|
import { ApiError } from '../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['account'],
|
tags: ['account'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
kind: "read:account",
|
kind: 'read:account',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -43,7 +44,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
private notificationService: NotificationService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, user, token) => {
|
super(meta, paramDef, async (ps, user, token) => {
|
||||||
|
@ -51,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`;
|
const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`;
|
||||||
|
let todayGetPoints = 0;
|
||||||
// 渡ってきている user はキャッシュされていて古い可能性があるので改めて取得
|
// 渡ってきている user はキャッシュされていて古い可能性があるので改めて取得
|
||||||
const userProfile = await this.userProfilesRepository.findOne({
|
const userProfile = await this.userProfilesRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
@ -65,9 +66,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userProfile.loggedInDates.includes(today)) {
|
if (!userProfile.loggedInDates.includes(today)) {
|
||||||
|
todayGetPoints = Math.floor(Math.random() * 5) + 1;
|
||||||
this.userProfilesRepository.update({ userId: user.id }, {
|
this.userProfilesRepository.update({ userId: user.id }, {
|
||||||
loggedInDates: [...userProfile.loggedInDates, today],
|
loggedInDates: [...userProfile.loggedInDates, today],
|
||||||
});
|
});
|
||||||
|
this.userProfilesRepository.update({ userId: user.id }, {
|
||||||
|
getPoints: userProfile.getPoints + todayGetPoints,
|
||||||
|
});
|
||||||
|
this.notificationService.createNotification(user.id, 'loginbonus', {
|
||||||
|
loginbonus: todayGetPoints,
|
||||||
|
});
|
||||||
userProfile.loggedInDates = [...userProfile.loggedInDates, today];
|
userProfile.loggedInDates = [...userProfile.loggedInDates, today];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
schema: 'MeDetailed',
|
schema: 'MeDetailed',
|
||||||
includeSecrets: isSecure,
|
includeSecrets: isSecure,
|
||||||
userProfile,
|
userProfile,
|
||||||
|
...(todayGetPoints && { todayGetPoints }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ export const notificationTypes = [
|
||||||
'achievementEarned',
|
'achievementEarned',
|
||||||
'app',
|
'app',
|
||||||
'test',
|
'test',
|
||||||
|
'loginbonus',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const groupedNotificationTypes = [
|
export const groupedNotificationTypes = [
|
||||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div :class="$style.head">
|
<div :class="$style.head">
|
||||||
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'loginbonus'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||||
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
||||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
||||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||||
|
@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
[$style.t_quote]: notification.type === 'quote',
|
[$style.t_quote]: notification.type === 'quote',
|
||||||
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
||||||
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
||||||
|
[$style.t_achievementEarned]: notification.type === 'loginbonus',
|
||||||
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
|
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
|
||||||
}]"
|
}]"
|
||||||
>
|
>
|
||||||
|
@ -37,6 +38,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||||
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
||||||
|
<i v-else-if="notification.type === 'loginbonus'" class="ti ti-medal"></i>
|
||||||
|
|
||||||
<template v-else-if="notification.type === 'roleAssigned'">
|
<template v-else-if="notification.type === 'roleAssigned'">
|
||||||
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
||||||
<i v-else class="ti ti-badges"></i>
|
<i v-else class="ti ti-badges"></i>
|
||||||
|
@ -56,6 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
|
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
|
||||||
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
||||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||||
|
<span v-else-if="notification.type === 'loginbonus'">{{ i18n.ts._notification.loginbonus }}</span>
|
||||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||||
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||||
|
@ -94,10 +98,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkA>
|
</MkA>
|
||||||
<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
|
<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
|
||||||
{{ notification.role.name }}
|
{{ notification.role.name }}
|
||||||
|
</div> <div v-else-if="notification.type === 'loginbonus'" :class="$style.text">
|
||||||
|
{{ notification.loginbonus }}プリズム入手しました!
|
||||||
</div>
|
</div>
|
||||||
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
||||||
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
|
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
|
||||||
</MkA>
|
</MkA>
|
||||||
|
|
||||||
<template v-else-if="notification.type === 'follow'">
|
<template v-else-if="notification.type === 'follow'">
|
||||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -11,159 +11,175 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<!-- <div class="punished" v-if="user.isSuspended"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSuspended }}</div> -->
|
<!-- <div class="punished" v-if="user.isSuspended"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSuspended }}</div> -->
|
||||||
<!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
|
<!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
|
||||||
|
|
||||||
<div class="profile _gaps">
|
<div class="profile _gaps">
|
||||||
<MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
|
<MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
|
||||||
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/>
|
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/>
|
||||||
<MkRemoteInfoUpdate v-if="user.host != null" :UserId="user.id" class="warn"/>
|
<MkRemoteInfoUpdate v-if="user.host != null" :UserId="user.id" class="warn"/>
|
||||||
<div :key="user.id" class="main _panel">
|
<div :key="user.id" class="main _panel">
|
||||||
<div class="banner-container" :style="style">
|
<div class="banner-container" :style="style">
|
||||||
<div ref="bannerEl" class="banner" :style="style"></div>
|
<div ref="bannerEl" class="banner" :style="style"></div>
|
||||||
<div class="fade"></div>
|
<div class="fade"></div>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<MkUserName class="name" :user="user" :nowrap="true"/>
|
<MkUserName class="name" :user="user" :nowrap="true"/>
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<span class="username"><MkAcct :user="user" :detail="true"/></span>
|
<span class="username"><MkAcct :user="user" :detail="true"/></span>
|
||||||
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i
|
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i
|
||||||
class="ti ti-shield"></i></span>
|
class="ti ti-shield"
|
||||||
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
|
></i></span>
|
||||||
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
|
||||||
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
|
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
||||||
<i class="ti ti-edit"/> {{ i18n.ts.addMemo }}
|
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
|
||||||
</button>
|
<i class="ti ti-edit"/> {{ i18n.ts.addMemo }}
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
</div>
|
||||||
<div v-if="$i" class="actions">
|
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
||||||
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
|
<div v-if="$i" class="actions">
|
||||||
<MkNotifyButton v-if="$i.id != user.id " :user="user"></MkNotifyButton>
|
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
|
||||||
<MkFollowButton v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true"
|
<MkNotifyButton v-if="$i.id != user.id " :user="user"></MkNotifyButton>
|
||||||
class="koudoku"/>
|
<MkFollowButton
|
||||||
</div>
|
v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true"
|
||||||
</div>
|
class="koudoku"
|
||||||
<MkAvatar class="avatar" :user="user" indicator/>
|
/>
|
||||||
<div class="title">
|
</div>
|
||||||
<MkUserName :user="user" :nowrap="false" class="name"/>
|
</div>
|
||||||
<div class="bottom">
|
<MkAvatar class="avatar" :user="user" indicator/>
|
||||||
<span class="username"><MkAcct :user="user" :detail="true"/></span>
|
<div class="title">
|
||||||
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i
|
<MkUserName :user="user" :nowrap="false" class="name"/>
|
||||||
class="ti ti-shield"></i></span>
|
<div class="bottom">
|
||||||
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
|
<span class="username"><MkAcct :user="user" :detail="true"/></span>
|
||||||
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i
|
||||||
</div>
|
class="ti ti-shield"
|
||||||
</div>
|
></i></span>
|
||||||
<div v-if="user.roles.length > 0" class="roles">
|
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
|
||||||
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role"
|
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
||||||
:style="{ '--color': role.color }">
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="user.roles.length > 0" class="roles">
|
||||||
|
<span
|
||||||
|
v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role"
|
||||||
|
:style="{ '--color': role.color }"
|
||||||
|
>
|
||||||
<MkA v-adaptive-bg :to="`/roles/${role.id}`">
|
<MkA v-adaptive-bg :to="`/roles/${role.id}`">
|
||||||
<img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
|
<img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
|
||||||
{{ role.name }}
|
{{ role.name }}
|
||||||
</MkA>
|
</MkA>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="iAmModerator" class="moderationNote">
|
<div v-if="iAmModerator" class="moderationNote">
|
||||||
<MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')"
|
<MkTextarea
|
||||||
v-model="moderationNote" manualSave>
|
v-if="editModerationNote || (moderationNote != null && moderationNote !== '')"
|
||||||
<template #label>{{ i18n.ts.moderationNote }}</template>
|
v-model="moderationNote" manualSave
|
||||||
</MkTextarea>
|
>
|
||||||
<div v-else>
|
<template #label>{{ i18n.ts.moderationNote }}</template>
|
||||||
<MkButton small @click="editModerationNote = true">{{ i18n.ts.addModerationNote }}</MkButton>
|
</MkTextarea>
|
||||||
</div>
|
<div v-else>
|
||||||
</div>
|
<MkButton small @click="editModerationNote = true">{{ i18n.ts.addModerationNote }}</MkButton>
|
||||||
<div v-if="isEditingMemo || memoDraft" class="memo" :class="{'no-memo': !memoDraft}">
|
</div>
|
||||||
<div class="heading" v-text="i18n.ts.memo"/>
|
</div>
|
||||||
<textarea
|
<div v-if="isEditingMemo || memoDraft" class="memo" :class="{'no-memo': !memoDraft}">
|
||||||
ref="memoTextareaEl"
|
<div class="heading" v-text="i18n.ts.memo"/>
|
||||||
v-model="memoDraft"
|
<textarea
|
||||||
rows="1"
|
ref="memoTextareaEl"
|
||||||
@focus="isEditingMemo = true"
|
v-model="memoDraft"
|
||||||
@blur="updateMemo"
|
rows="1"
|
||||||
@input="adjustMemoTextarea"
|
@focus="isEditingMemo = true"
|
||||||
/>
|
@blur="updateMemo"
|
||||||
</div>
|
@input="adjustMemoTextarea"
|
||||||
<div class="description">
|
/>
|
||||||
<MkOmit>
|
</div>
|
||||||
<Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user"/>
|
<div class="description">
|
||||||
<p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p>
|
<MkOmit>
|
||||||
</MkOmit>
|
<Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user"/>
|
||||||
</div>
|
<p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p>
|
||||||
<div class="fields system">
|
</MkOmit>
|
||||||
<dl v-if="user.location" class="field">
|
</div>
|
||||||
<dt class="name"><i class="ti ti-map-pin ti-fw"></i> {{ i18n.ts.location }}</dt>
|
<div class="fields system">
|
||||||
<dd class="value">{{ user.location }}</dd>
|
<dl v-if="user.location" class="field">
|
||||||
</dl>
|
<dt class="name"><i class="ti ti-map-pin ti-fw"></i> {{ i18n.ts.location }}</dt>
|
||||||
<dl v-if="user.birthday" class="field">
|
<dd class="value">{{ user.location }}</dd>
|
||||||
<dt class="name"><i class="ti ti-cake ti-fw"></i> {{ i18n.ts.birthday }}</dt>
|
</dl>
|
||||||
<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{
|
<dl v-if="user.birthday" class="field">
|
||||||
i18n.tsx.yearsOld({age})
|
<dt class="name"><i class="ti ti-cake ti-fw"></i> {{ i18n.ts.birthday }}</dt>
|
||||||
}})
|
<dd class="value">
|
||||||
</dd>
|
{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{
|
||||||
</dl>
|
i18n.tsx.yearsOld({age})
|
||||||
<dl class="field">
|
}})
|
||||||
<dt class="name"><i class="ti ti-calendar ti-fw"></i> {{ i18n.ts.registeredDate }}</dt>
|
</dd>
|
||||||
<dd class="value">{{ dateString(user.createdAt) }} (
|
</dl>
|
||||||
<MkTime :time="user.createdAt"/>
|
<dl class="field">
|
||||||
)
|
<dt class="name"><i class="ti ti-calendar ti-fw"></i> {{ i18n.ts.registeredDate }}</dt>
|
||||||
</dd>
|
<dd class="value">
|
||||||
</dl>
|
{{ dateString(user.createdAt) }} (
|
||||||
</div>
|
<MkTime :time="user.createdAt"/>
|
||||||
<div v-if="user.fields.length > 0" class="fields">
|
)
|
||||||
<dl v-for="(field, i) in user.fields" :key="i" class="field">
|
</dd>
|
||||||
<dt class="name">
|
</dl>
|
||||||
<Mfm :text="field.name" :plain="true" :colored="false"/>
|
</div>
|
||||||
</dt>
|
<div v-if="user.fields.length > 0" class="fields">
|
||||||
<dd class="value">
|
<dl v-for="(field, i) in user.fields" :key="i" class="field">
|
||||||
<Mfm :text="field.value" :author="user" :colored="false"/>
|
<dt class="name">
|
||||||
<i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink"
|
<Mfm :text="field.name" :plain="true" :colored="false"/>
|
||||||
class="ti ti-circle-check" :class="$style.verifiedLink"></i>
|
</dt>
|
||||||
</dd>
|
<dd class="value">
|
||||||
</dl>
|
<Mfm :text="field.value" :author="user" :colored="false"/>
|
||||||
</div>
|
<i
|
||||||
<div class="status">
|
v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink"
|
||||||
<MkA :to="userPage(user)">
|
class="ti ti-circle-check" :class="$style.verifiedLink"
|
||||||
<b>{{ number(user.notesCount) }}</b>
|
></i>
|
||||||
<span>{{ i18n.ts.notes }}</span>
|
</dd>
|
||||||
</MkA>
|
</dl>
|
||||||
<MkA v-if="isFollowingVisibleForMe(user)" :to="userPage(user, 'following')">
|
</div>
|
||||||
<b>{{ number(user.followingCount) }}</b>
|
<div class="status">
|
||||||
<span>{{ i18n.ts.following }}</span>
|
<MkA :to="userPage(user)">
|
||||||
</MkA>
|
<b>{{ number(user.notesCount) }}</b>
|
||||||
<MkA v-if="isFollowersVisibleForMe(user)" :to="userPage(user, 'followers')">
|
<span>{{ i18n.ts.notes }}</span>
|
||||||
<b>{{ number(user.followersCount) }}</b>
|
</MkA>
|
||||||
<span>{{ i18n.ts.followers }}</span>
|
<MkA v-if="isFollowingVisibleForMe(user)" :to="userPage(user, 'following')">
|
||||||
</MkA>
|
<b>{{ number(user.followingCount) }}</b>
|
||||||
</div>
|
<span>{{ i18n.ts.following }}</span>
|
||||||
</div>
|
</MkA>
|
||||||
</div>
|
<MkA v-if="isFollowersVisibleForMe(user)" :to="userPage(user, 'followers')">
|
||||||
|
<b>{{ number(user.followersCount) }}</b>
|
||||||
|
<span>{{ i18n.ts.followers }}</span>
|
||||||
|
</MkA>
|
||||||
|
<MkA>
|
||||||
|
<b> {{ number(user.getPoints) }}</b>
|
||||||
|
<span>{{ i18n.ts.points }}</span>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="contents _gaps">
|
<div class="contents _gaps">
|
||||||
<div v-if="user.pinnedNotes.length > 0" class="_gaps">
|
<div v-if="user.pinnedNotes.length > 0" class="_gaps">
|
||||||
<MkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true"/>
|
<MkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true"/>
|
||||||
</div>
|
</div>
|
||||||
<MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo>
|
<MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo>
|
||||||
<template v-if="narrow">
|
<template v-if="narrow">
|
||||||
<MkLazy>
|
<MkLazy>
|
||||||
<XFiles :key="user.id" :user="user"/>
|
<XFiles :key="user.id" :user="user"/>
|
||||||
</MkLazy>
|
</MkLazy>
|
||||||
<MkLazy>
|
<MkLazy>
|
||||||
<XActivity :key="user.id" :user="user"/>
|
<XActivity :key="user.id" :user="user"/>
|
||||||
</MkLazy>
|
</MkLazy>
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<div style="margin-bottom: 8px;">{{ i18n.ts._sfx.note }}</div>
|
<div style="margin-bottom: 8px;">{{ i18n.ts._sfx.note }}</div>
|
||||||
<MkNotes :class="$style.tl" :noGap="true" :pagination="Notes"/>
|
<MkNotes :class="$style.tl" :noGap="true" :pagination="Notes"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
|
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
|
||||||
<XFiles :key="user.id" :user="user"/>
|
<XFiles :key="user.id" :user="user"/>
|
||||||
<XActivity :key="user.id" :user="user"/>
|
<XActivity :key="user.id" :user="user"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
|
import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkNote from '@/components/MkNote.vue';
|
import MkNote from '@/components/MkNote.vue';
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||||
|
@ -173,36 +189,36 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkOmit from '@/components/MkOmit.vue';
|
import MkOmit from '@/components/MkOmit.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import {getScrollPosition} from '@/scripts/scroll.js';
|
import { getScrollPosition } from '@/scripts/scroll.js';
|
||||||
import {getUserMenu} from '@/scripts/get-user-menu.js';
|
import { getUserMenu } from '@/scripts/get-user-menu.js';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
import {userPage} from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { useRouter } from '@/router/supplier.js';
|
import { useRouter } from '@/router/supplier.js';
|
||||||
import {i18n} from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import {$i, iAmModerator} from '@/account.js';
|
import { $i, iAmModerator } from '@/account.js';
|
||||||
import {dateString} from '@/filters/date.js';
|
import { dateString } from '@/filters/date.js';
|
||||||
import {confetti} from '@/scripts/confetti.js';
|
import { confetti } from '@/scripts/confetti.js';
|
||||||
import {misskeyApi} from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import {isFollowingVisibleForMe, isFollowersVisibleForMe} from '@/scripts/isFfVisibleForMe.js';
|
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
||||||
import MkNotifyButton from "@/components/MkNotifyButton.vue";
|
import MkNotifyButton from '@/components/MkNotifyButton.vue';
|
||||||
import MkRemoteInfoUpdate from "@/components/MkRemoteInfoUpdate.vue";
|
import MkRemoteInfoUpdate from '@/components/MkRemoteInfoUpdate.vue';
|
||||||
import MkNotes from "@/components/MkNotes.vue";
|
import MkNotes from '@/components/MkNotes.vue';
|
||||||
import MkLazy from "@/components/global/MkLazy.vue";
|
import MkLazy from '@/components/global/MkLazy.vue';
|
||||||
|
|
||||||
function calcAge(birthdate: string): number {
|
function calcAge(birthdate: string): number {
|
||||||
const date = new Date(birthdate);
|
const date = new Date(birthdate);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
let yearDiff = now.getFullYear() - date.getFullYear();
|
let yearDiff = now.getFullYear() - date.getFullYear();
|
||||||
const monthDiff = now.getMonth() - date.getMonth();
|
const monthDiff = now.getMonth() - date.getMonth();
|
||||||
const pastDate = now.getDate() < date.getDate();
|
const pastDate = now.getDate() < date.getDate();
|
||||||
|
|
||||||
if (monthDiff < 0 || (monthDiff === 0 && pastDate)) {
|
if (monthDiff < 0 || (monthDiff === 0 && pastDate)) {
|
||||||
yearDiff--;
|
yearDiff--;
|
||||||
}
|
}
|
||||||
|
|
||||||
return yearDiff;
|
return yearDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
const XFiles = defineAsyncComponent(() => import('./index.files.vue'));
|
const XFiles = defineAsyncComponent(() => import('./index.files.vue'));
|
||||||
|
@ -214,7 +230,7 @@ const props = withDefaults(defineProps<{
|
||||||
/** Test only; MkNotes currently causes problems in vitest */
|
/** Test only; MkNotes currently causes problems in vitest */
|
||||||
disableNotes: boolean;
|
disableNotes: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
disableNotes: false,
|
disableNotes: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -231,107 +247,107 @@ const moderationNote = ref(props.user.moderationNote);
|
||||||
const editModerationNote = ref(false);
|
const editModerationNote = ref(false);
|
||||||
|
|
||||||
watch(moderationNote, async () => {
|
watch(moderationNote, async () => {
|
||||||
await misskeyApi('admin/update-user-note', {userId: props.user.id, text: moderationNote.value });
|
await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
|
||||||
});
|
});
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
endpoint: 'users/featured-notes' as const,
|
endpoint: 'users/featured-notes' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: computed(() => ({
|
params: computed(() => ({
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
})),
|
})),
|
||||||
|
};
|
||||||
|
const Notes = {
|
||||||
|
endpoint: 'users/notes' as const,
|
||||||
|
limit: 10,
|
||||||
|
params: computed(() => ({
|
||||||
|
userId: props.user.id,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
const Notes ={
|
|
||||||
endpoint: 'users/notes' as const,
|
|
||||||
limit: 10,
|
|
||||||
params: computed(() => ({
|
|
||||||
userId: props.user.id,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = computed(() => {
|
const style = computed(() => {
|
||||||
if (props.user.bannerUrl == null) return {};
|
if (props.user.bannerUrl == null) return {};
|
||||||
return {
|
return {
|
||||||
backgroundImage: `url(${props.user.bannerUrl})`,
|
backgroundImage: `url(${props.user.bannerUrl})`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const age = computed(() => {
|
const age = computed(() => {
|
||||||
return calcAge(props.user.birthday);
|
return calcAge(props.user.birthday);
|
||||||
});
|
});
|
||||||
|
|
||||||
function menu(ev: MouseEvent) {
|
function menu(ev: MouseEvent) {
|
||||||
const {menu, cleanup} = getUserMenu(user.value, router);
|
const { menu, cleanup } = getUserMenu(user.value, router);
|
||||||
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
|
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parallaxLoop() {
|
function parallaxLoop() {
|
||||||
parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop);
|
parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop);
|
||||||
parallax();
|
parallax();
|
||||||
}
|
}
|
||||||
|
|
||||||
function parallax() {
|
function parallax() {
|
||||||
const banner = bannerEl.value as any;
|
const banner = bannerEl.value as any;
|
||||||
if (banner == null) return;
|
if (banner == null) return;
|
||||||
|
|
||||||
const top = getScrollPosition(rootEl.value);
|
const top = getScrollPosition(rootEl.value);
|
||||||
|
|
||||||
if (top < 0) return;
|
if (top < 0) return;
|
||||||
|
|
||||||
const z = 1.75; // 奥行き(小さいほど奥)
|
const z = 1.75; // 奥行き(小さいほど奥)
|
||||||
const pos = -(top / z);
|
const pos = -(top / z);
|
||||||
banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
|
banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMemoTextarea() {
|
function showMemoTextarea() {
|
||||||
isEditingMemo.value = true;
|
isEditingMemo.value = true;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
memoTextareaEl.value?.focus();
|
memoTextareaEl.value?.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function adjustMemoTextarea() {
|
function adjustMemoTextarea() {
|
||||||
if (!memoTextareaEl.value) return;
|
if (!memoTextareaEl.value) return;
|
||||||
memoTextareaEl.value.style.height = '0px';
|
memoTextareaEl.value.style.height = '0px';
|
||||||
memoTextareaEl.value.style.height = `${memoTextareaEl.value.scrollHeight}px`;
|
memoTextareaEl.value.style.height = `${memoTextareaEl.value.scrollHeight}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateMemo() {
|
async function updateMemo() {
|
||||||
await misskeyApi('users/update-memo', {
|
await misskeyApi('users/update-memo', {
|
||||||
memo: memoDraft.value,
|
memo: memoDraft.value,
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
});
|
});
|
||||||
isEditingMemo.value = false;
|
isEditingMemo.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([props.user], () => {
|
watch([props.user], () => {
|
||||||
memoDraft.value = props.user.memo;
|
memoDraft.value = props.user.memo;
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.requestAnimationFrame(parallaxLoop);
|
window.requestAnimationFrame(parallaxLoop);
|
||||||
narrow.value = rootEl.value!.clientWidth < 1000;
|
narrow.value = rootEl.value!.clientWidth < 1000;
|
||||||
|
|
||||||
if (props.user.birthday) {
|
if (props.user.birthday) {
|
||||||
const m = new Date().getMonth() + 1;
|
const m = new Date().getMonth() + 1;
|
||||||
const d = new Date().getDate();
|
const d = new Date().getDate();
|
||||||
const bm = parseInt(props.user.birthday.split('-')[1]);
|
const bm = parseInt(props.user.birthday.split('-')[1]);
|
||||||
const bd = parseInt(props.user.birthday.split('-')[2]);
|
const bd = parseInt(props.user.birthday.split('-')[2]);
|
||||||
if (m === bm && d === bd) {
|
if (m === bm && d === bd) {
|
||||||
confetti({
|
confetti({
|
||||||
duration: 1000 * 4,
|
duration: 1000 * 4,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
adjustMemoTextarea();
|
adjustMemoTextarea();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (parallaxAnimationId.value) {
|
if (parallaxAnimationId.value) {
|
||||||
window.cancelAnimationFrame(parallaxAnimationId.value);
|
window.cancelAnimationFrame(parallaxAnimationId.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue