feature(backend): ユーザー統計表示機能の復活 (MisskeyIO#258)
This commit is contained in:
		
							parent
							
								
									84a7f12e7f
								
							
						
					
					
						commit
						114c7fe6b3
					
				|  | @ -362,6 +362,7 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js'; | |||
| import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; | ||||
| import * as ep___users_search from './endpoints/users/search.js'; | ||||
| import * as ep___users_show from './endpoints/users/show.js'; | ||||
| import * as ep___users_stats from './endpoints/users/stats.js'; | ||||
| import * as ep___users_achievements from './endpoints/users/achievements.js'; | ||||
| import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; | ||||
| import * as ep___fetchRss from './endpoints/fetch-rss.js'; | ||||
|  | @ -727,6 +728,7 @@ const $users_reportAbuse: Provider = { provide: 'ep:users/report-abuse', useClas | |||
| const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default }; | ||||
| const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default }; | ||||
| const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default }; | ||||
| const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default }; | ||||
| const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default }; | ||||
| const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default }; | ||||
| const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; | ||||
|  | @ -1096,6 +1098,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention | |||
| 		$users_searchByUsernameAndHost, | ||||
| 		$users_search, | ||||
| 		$users_show, | ||||
| 		$users_stats, | ||||
| 		$users_achievements, | ||||
| 		$users_updateMemo, | ||||
| 		$fetchRss, | ||||
|  | @ -1456,6 +1459,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention | |||
| 		$users_searchByUsernameAndHost, | ||||
| 		$users_search, | ||||
| 		$users_show, | ||||
| 		$users_stats, | ||||
| 		$users_achievements, | ||||
| 		$users_updateMemo, | ||||
| 		$fetchRss, | ||||
|  |  | |||
|  | @ -362,6 +362,7 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js'; | |||
| import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; | ||||
| import * as ep___users_search from './endpoints/users/search.js'; | ||||
| import * as ep___users_show from './endpoints/users/show.js'; | ||||
| import * as ep___users_stats from './endpoints/users/stats.js'; | ||||
| import * as ep___users_achievements from './endpoints/users/achievements.js'; | ||||
| import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; | ||||
| import * as ep___fetchRss from './endpoints/fetch-rss.js'; | ||||
|  | @ -725,6 +726,7 @@ const eps = [ | |||
| 	['users/search-by-username-and-host', ep___users_searchByUsernameAndHost], | ||||
| 	['users/search', ep___users_search], | ||||
| 	['users/show', ep___users_show], | ||||
| 	['users/stats', ep___users_stats], | ||||
| 	['users/achievements', ep___users_achievements], | ||||
| 	['users/update-memo', ep___users_updateMemo], | ||||
| 	['fetch-rss', ep___fetchRss], | ||||
|  |  | |||
|  | @ -0,0 +1,233 @@ | |||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { awaitAll } from '@/misc/prelude/await-all.js'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import type { UsersRepository, NotesRepository, FollowingsRepository, DriveFilesRepository, NoteReactionsRepository, PageLikesRepository, NoteFavoritesRepository, PollVotesRepository } from '@/models/_.js'; | ||||
| import { ApiError } from '../../error.js'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['users'], | ||||
| 
 | ||||
| 	requireCredential: false, | ||||
| 
 | ||||
| 	description: 'Show statistics about a user.', | ||||
| 
 | ||||
| 	errors: { | ||||
| 		noSuchUser: { | ||||
| 			message: 'No such user.', | ||||
| 			code: 'NO_SUCH_USER', | ||||
| 			id: '9e638e45-3b25-4ef7-8f95-07e8498f1819', | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	res: { | ||||
| 		type: 'object', | ||||
| 		optional: false, nullable: false, | ||||
| 		properties: { | ||||
| 			notesCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			repliesCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			renotesCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			repliedCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			renotedCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			pollVotesCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			pollVotedCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			localFollowingCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			remoteFollowingCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			localFollowersCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			remoteFollowersCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			followingCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			followersCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			sentReactionsCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			receivedReactionsCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			noteFavoritesCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			pageLikesCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			pageLikedCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			driveFilesCount: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 			driveUsage: { | ||||
| 				type: 'integer', | ||||
| 				optional: false, nullable: false, | ||||
| 				description: 'Drive usage in bytes', | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
| 
 | ||||
| export const paramDef = { | ||||
| 	type: 'object', | ||||
| 	properties: { | ||||
| 		userId: { type: 'string', format: 'misskey:id' }, | ||||
| 	}, | ||||
| 	required: ['userId'], | ||||
| } as const; | ||||
| 
 | ||||
| // eslint-disable-next-line import/no-default-export
 | ||||
| @Injectable() | ||||
| export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 	constructor( | ||||
| 		@Inject(DI.usersRepository) | ||||
| 		private usersRepository: UsersRepository, | ||||
| 
 | ||||
| 		@Inject(DI.notesRepository) | ||||
| 		private notesRepository: NotesRepository, | ||||
| 
 | ||||
| 		@Inject(DI.followingsRepository) | ||||
| 		private followingsRepository: FollowingsRepository, | ||||
| 
 | ||||
| 		@Inject(DI.driveFilesRepository) | ||||
| 		private driveFilesRepository: DriveFilesRepository, | ||||
| 
 | ||||
| 		@Inject(DI.noteReactionsRepository) | ||||
| 		private noteReactionsRepository: NoteReactionsRepository, | ||||
| 
 | ||||
| 		@Inject(DI.pageLikesRepository) | ||||
| 		private pageLikesRepository: PageLikesRepository, | ||||
| 
 | ||||
| 		@Inject(DI.noteFavoritesRepository) | ||||
| 		private noteFavoritesRepository: NoteFavoritesRepository, | ||||
| 
 | ||||
| 		@Inject(DI.pollVotesRepository) | ||||
| 		private pollVotesRepository: PollVotesRepository, | ||||
| 
 | ||||
| 		private driveFileEntityService: DriveFileEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const user = await this.usersRepository.findOneBy({ id: ps.userId }); | ||||
| 			if (user == null) { | ||||
| 				throw new ApiError(meta.errors.noSuchUser); | ||||
| 			} | ||||
| 
 | ||||
| 			const result = await awaitAll({ | ||||
| 				notesCount: this.notesRepository.createQueryBuilder('note') | ||||
| 					.where('note.userId = :userId', { userId: user.id }) | ||||
| 					.getCount(), | ||||
| 				repliesCount: this.notesRepository.createQueryBuilder('note') | ||||
| 					.where('note.userId = :userId', { userId: user.id }) | ||||
| 					.andWhere('note.replyId IS NOT NULL') | ||||
| 					.getCount(), | ||||
| 				renotesCount: this.notesRepository.createQueryBuilder('note') | ||||
| 					.where('note.userId = :userId', { userId: user.id }) | ||||
| 					.andWhere('note.renoteId IS NOT NULL') | ||||
| 					.getCount(), | ||||
| 				repliedCount: this.notesRepository.createQueryBuilder('note') | ||||
| 					.where('note.replyUserId = :userId', { userId: user.id }) | ||||
| 					.getCount(), | ||||
| 				renotedCount: this.notesRepository.createQueryBuilder('note') | ||||
| 					.where('note.renoteUserId = :userId', { userId: user.id }) | ||||
| 					.getCount(), | ||||
| 				pollVotesCount: this.pollVotesRepository.createQueryBuilder('vote') | ||||
| 					.where('vote.userId = :userId', { userId: user.id }) | ||||
| 					.getCount(), | ||||
| 				pollVotedCount: this.pollVotesRepository.createQueryBuilder('vote') | ||||
| 					.innerJoin('vote.note', 'note') | ||||
| 					.where('note.userId = :userId', { userId: user.id }) | ||||
| 					.getCount(), | ||||
| 				localFollowingCount: this.followingsRepository.createQueryBuilder('following') | ||||
| 					.where('following.followerId = :userId', { userId: user.id }) | ||||
| 					.andWhere('following.followeeHost IS NULL') | ||||
| 					.getCount(), | ||||
| 				remoteFollowingCount: this.followingsRepository.createQueryBuilder('following') | ||||
| 					.where('following.followerId = :userId', { userId: user.id }) | ||||
| 					.andWhere('following.followeeHost IS NOT NULL') | ||||
| 					.getCount(), | ||||
| 				localFollowersCount: this.followingsRepository.createQueryBuilder('following') | ||||
| 					.where('following.followeeId = :userId', { userId: user.id }) | ||||
| 					.andWhere('following.followerHost IS NULL') | ||||
| 					.getCount(), | ||||
| 				remoteFollowersCount: this.followingsRepository.createQueryBuilder('following') | ||||
| 					.where('following.followeeId = :userId', { userId: user.id }) | ||||
| 					.andWhere('following.followerHost IS NOT NULL') | ||||
| 					.getCount(), | ||||
| 				sentReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction') | ||||
| 					.where('reaction.userId = :userId', { userId: user.id }) | ||||
| 					.getCount(), | ||||
| 				receivedReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction') | ||||
| 					.innerJoin('reaction.note', 'note') | ||||
| 					.where('note.userId = :userId', { userId: user.id }) | ||||
| 					.getCount(), | ||||
| 				noteFavoritesCount: this.noteFavoritesRepository.createQueryBuilder('favorite') | ||||
| 					.where('favorite.userId = :userId', { userId: user.id }) | ||||
| 					.getCount(), | ||||
| 				pageLikesCount: this.pageLikesRepository.createQueryBuilder('like') | ||||
| 					.where('like.userId = :userId', { userId: user.id }) | ||||
| 					.getCount(), | ||||
| 				pageLikedCount: this.pageLikesRepository.createQueryBuilder('like') | ||||
| 					.innerJoin('like.page', 'page') | ||||
| 					.where('page.userId = :userId', { userId: user.id }) | ||||
| 					.getCount(), | ||||
| 				driveFilesCount: this.driveFilesRepository.createQueryBuilder('file') | ||||
| 					.where('file.userId = :userId', { userId: user.id }) | ||||
| 					.getCount(), | ||||
| 				driveUsage: this.driveFileEntityService.calcDriveUsageOf(user), | ||||
| 			}); | ||||
| 
 | ||||
| 			return { | ||||
| 				...result, | ||||
| 				followingCount: result.localFollowingCount + result.remoteFollowingCount, | ||||
| 				followersCount: result.localFollowersCount + result.remoteFollowersCount, | ||||
| 			}; | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,151 @@ | |||
| <!-- | ||||
| SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
| SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
| 
 | ||||
| <template> | ||||
| <div class="_gaps_m"> | ||||
| 	<FormSection v-if="stats" first> | ||||
| 		<template #label>{{ i18n.ts.statistics }}</template> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.notesCount }}</template> | ||||
| 			<template #value>{{ number(stats.notesCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.repliesCount }}</template> | ||||
| 			<template #value>{{ number(stats.repliesCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.renotesCount }}</template> | ||||
| 			<template #value>{{ number(stats.renotesCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.repliedCount }}</template> | ||||
| 			<template #value>{{ number(stats.repliedCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.renotedCount }}</template> | ||||
| 			<template #value>{{ number(stats.renotedCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.pollVotesCount }}</template> | ||||
| 			<template #value>{{ number(stats.pollVotesCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.pollVotedCount }}</template> | ||||
| 			<template #value>{{ number(stats.pollVotedCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.sentReactionsCount }}</template> | ||||
| 			<template #value>{{ number(stats.sentReactionsCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.receivedReactionsCount }}</template> | ||||
| 			<template #value>{{ number(stats.receivedReactionsCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.noteFavoritesCount }}</template> | ||||
| 			<template #value>{{ number(stats.noteFavoritesCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.followingCount }}</template> | ||||
| 			<template #value>{{ number(stats.followingCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.local }})</template> | ||||
| 			<template #value>{{ number(stats.localFollowingCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.remote }})</template> | ||||
| 			<template #value>{{ number(stats.remoteFollowingCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.followersCount }}</template> | ||||
| 			<template #value>{{ number(stats.followersCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.local }})</template> | ||||
| 			<template #value>{{ number(stats.localFollowersCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.remote }})</template> | ||||
| 			<template #value>{{ number(stats.remoteFollowersCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.pageLikesCount }}</template> | ||||
| 			<template #value>{{ number(stats.pageLikesCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.pageLikedCount }}</template> | ||||
| 			<template #value>{{ number(stats.pageLikedCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.driveFilesCount }}</template> | ||||
| 			<template #value>{{ number(stats.driveFilesCount) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ i18n.ts.driveUsage }}</template> | ||||
| 			<template #value>{{ bytes(stats.driveUsage) }}</template> | ||||
| 		</MkKeyValue> | ||||
| 	</FormSection> | ||||
| 
 | ||||
| 	<FormSection> | ||||
| 		<template #label>{{ i18n.ts.other }}</template> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>emailVerified</template> | ||||
| 			<template #value>{{ $i.emailVerified ? i18n.ts.yes : i18n.ts.no }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>twoFactorEnabled</template> | ||||
| 			<template #value>{{ $i.twoFactorEnabled ? i18n.ts.yes : i18n.ts.no }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>securityKeys</template> | ||||
| 			<template #value>{{ $i.securityKeys ? i18n.ts.yes : i18n.ts.no }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>usePasswordLessLogin</template> | ||||
| 			<template #value>{{ $i.usePasswordLessLogin ? i18n.ts.yes : i18n.ts.no }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>isModerator</template> | ||||
| 			<template #value>{{ $i.isModerator ? i18n.ts.yes : i18n.ts.no }}</template> | ||||
| 		</MkKeyValue> | ||||
| 		<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 			<template #key>isAdmin</template> | ||||
| 			<template #value>{{ $i.isAdmin ? i18n.ts.yes : i18n.ts.no }}</template> | ||||
| 		</MkKeyValue> | ||||
| 	</FormSection> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, ref } from 'vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import MkKeyValue from '@/components/MkKeyValue.vue'; | ||||
| import * as os from '@/os.js'; | ||||
| import number from '@/filters/number.js'; | ||||
| import bytes from '@/filters/bytes.js'; | ||||
| import { $i } from '@/account.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| 
 | ||||
| const stats = ref<any>({}); | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	os.api('users/stats', { | ||||
| 		userId: $i!.id, | ||||
| 	}).then(response => { | ||||
| 		stats.value = response; | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.accountInfo, | ||||
| 	icon: 'ti ti-info-circle', | ||||
| }); | ||||
| </script> | ||||
|  | @ -31,6 +31,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 						<template #key>{{ i18n.ts.registeredDate }}</template> | ||||
| 						<template #value><MkTime :time="$i.createdAt" mode="detail"/></template> | ||||
| 					</MkKeyValue> | ||||
| 
 | ||||
| 					<FormLink to="/settings/account-stats"><template #icon><i class="ti ti-info-circle"/></template>{{ i18n.ts.statistics }}</FormLink> | ||||
| 				</div> | ||||
| 			</MkFolder> | ||||
| 
 | ||||
|  |  | |||
|  | @ -170,6 +170,10 @@ export const routes = [{ | |||
| 		path: '/accounts', | ||||
| 		name: 'profile', | ||||
| 		component: page(() => import('./pages/settings/accounts.vue')), | ||||
| 	}, { | ||||
| 		path: '/account-stats', | ||||
| 		name: 'other', | ||||
| 		component: page(() => import('./pages/settings/account-stats.vue')), | ||||
| 	}, { | ||||
| 		path: '/other', | ||||
| 		name: 'other', | ||||
|  |  | |||
|  | @ -2273,6 +2273,10 @@ export type Endpoints = { | |||
|             }; | ||||
|         }; | ||||
|     }; | ||||
|     'users/stats': { | ||||
|         req: TODO; | ||||
|         res: TODO; | ||||
|     }; | ||||
|     'fetch-rss': { | ||||
|         req: { | ||||
|             url: string; | ||||
|  |  | |||
|  | @ -644,7 +644,7 @@ export type Endpoints = { | |||
| 			$default: UserDetailed; | ||||
| 		}; | ||||
| 	}; }; | ||||
| 
 | ||||
| 	'users/stats': { req: TODO; res: TODO; }; | ||||
| 	// fetching external data
 | ||||
| 	'fetch-rss': { req: { url: string; }; res: TODO; }; | ||||
| 	'fetch-external-resources': { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue