Merge 0b47e8c746
into 30df768d26
This commit is contained in:
commit
16a9ddbbae
|
@ -5254,6 +5254,14 @@ export interface Locale extends ILocale {
|
||||||
* このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。
|
* このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。
|
||||||
*/
|
*/
|
||||||
"federationDisabled": string;
|
"federationDisabled": string;
|
||||||
|
/**
|
||||||
|
* リアクション統計
|
||||||
|
*/
|
||||||
|
"reactionStats": string;
|
||||||
|
/**
|
||||||
|
* 最も使用された絵文字リアクション上位100件を表示します
|
||||||
|
*/
|
||||||
|
"reactionStatsDescription": string;
|
||||||
"_accountSettings": {
|
"_accountSettings": {
|
||||||
/**
|
/**
|
||||||
* コンテンツの表示にログインを必須にする
|
* コンテンツの表示にログインを必須にする
|
||||||
|
|
|
@ -1309,6 +1309,8 @@ availableRoles: "利用可能なロール"
|
||||||
acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。"
|
acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。"
|
||||||
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
|
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
|
||||||
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
|
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
|
||||||
|
reactionStats: "リアクション統計"
|
||||||
|
reactionStatsDescription: "最も使用された絵文字リアクション上位100件を表示します"
|
||||||
|
|
||||||
_accountSettings:
|
_accountSettings:
|
||||||
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
|
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
|
||||||
|
|
|
@ -339,6 +339,7 @@ export * as 'pages/update' from './endpoints/pages/update.js';
|
||||||
export * as 'ping' from './endpoints/ping.js';
|
export * as 'ping' from './endpoints/ping.js';
|
||||||
export * as 'pinned-users' from './endpoints/pinned-users.js';
|
export * as 'pinned-users' from './endpoints/pinned-users.js';
|
||||||
export * as 'promo/read' from './endpoints/promo/read.js';
|
export * as 'promo/read' from './endpoints/promo/read.js';
|
||||||
|
export * as 'reaction-stats' from './endpoints/reaction-stats.js';
|
||||||
export * as 'renote-mute/create' from './endpoints/renote-mute/create.js';
|
export * as 'renote-mute/create' from './endpoints/renote-mute/create.js';
|
||||||
export * as 'renote-mute/delete' from './endpoints/renote-mute/delete.js';
|
export * as 'renote-mute/delete' from './endpoints/renote-mute/delete.js';
|
||||||
export * as 'renote-mute/list' from './endpoints/renote-mute/list.js';
|
export * as 'renote-mute/list' from './endpoints/renote-mute/list.js';
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: lqvp
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import type { NoteReactionsRepository } from '@/models/_.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['meta'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
kind: 'read:account',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
reaction: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false,
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchUser: {
|
||||||
|
message: 'No such user.',
|
||||||
|
code: 'NO_SUCH_USER',
|
||||||
|
id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
site: { type: 'boolean', default: false },
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.noteReactionsRepository)
|
||||||
|
private noteReactionsRepository: NoteReactionsRepository,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const query =
|
||||||
|
this.noteReactionsRepository.createQueryBuilder('nr')
|
||||||
|
.select('nr.reaction', 'reaction')
|
||||||
|
.addSelect('count(nr.id)', 'count')
|
||||||
|
.groupBy('nr.reaction')
|
||||||
|
.orderBy('count', 'DESC')
|
||||||
|
.limit(100);
|
||||||
|
|
||||||
|
if (!ps.site) {
|
||||||
|
query.where('nr.userId = :id', { id: me.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await query.getRawMany();
|
||||||
|
|
||||||
|
return res.map(x => ({
|
||||||
|
reaction: x.reaction,
|
||||||
|
count: x.count,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -176,6 +176,11 @@ export const navbarItemDef = reactive({
|
||||||
show: computed(() => $i != null),
|
show: computed(() => $i != null),
|
||||||
to: `/@${$i?.username}`,
|
to: `/@${$i?.username}`,
|
||||||
},
|
},
|
||||||
|
reactionStats: {
|
||||||
|
title: i18n.ts.reactionStats,
|
||||||
|
icon: 'ti ti-chart-bar',
|
||||||
|
to: '/reaction-stats',
|
||||||
|
},
|
||||||
cacheClear: {
|
cacheClear: {
|
||||||
title: i18n.ts.clearCache,
|
title: i18n.ts.clearCache,
|
||||||
icon: 'ti ti-trash',
|
icon: 'ti ti-trash',
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: lqvp
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkStickyContainer>
|
||||||
|
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
|
<MkInfo>
|
||||||
|
{{ i18n.ts.reactionStatsDescription }}
|
||||||
|
</MkInfo>
|
||||||
|
<MkSpacer v-if="tab === 'me'" :contentMax="1000" :marginMin="20">
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkButton key="copyMyReactionsList" @click="copyToClipboard(myReactionsListMfm)"><i class="ti ti-copy"></i> {{ i18n.ts.copyContent }}</MkButton>
|
||||||
|
</div>
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<p>
|
||||||
|
<Mfm key="myreactionslist" :text="myReactionsListMfm"></Mfm>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
|
<MkSpacer v-else-if="tab === 'site'" :contentMax="1000" :marginMin="20">
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkButton key="copySiteReactionsList" @click="copyToClipboard(serverReactionsListMfm)"><i class="ti ti-copy"></i> {{ i18n.ts.copyContent }}</MkButton>
|
||||||
|
</div>
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<p>
|
||||||
|
<Mfm key="sitereactionslist" :text="serverReactionsListMfm"></Mfm>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkStickyContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { misskeyApi } from '@/scripts/misskey-api';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
import { signinRequired } from '@/account.js';
|
||||||
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||||
|
|
||||||
|
const $i = signinRequired();
|
||||||
|
|
||||||
|
const myReactionsListMfm = ref('Loading...');
|
||||||
|
const serverReactionsListMfm = ref('Loading...');
|
||||||
|
|
||||||
|
const tab = ref('me');
|
||||||
|
|
||||||
|
watch(tab, async () => {
|
||||||
|
if (tab.value === 'site' && serverReactionsListMfm.value !== 'Loading...') { return; }
|
||||||
|
if (tab.value !== 'site' && myReactionsListMfm.value !== 'Loading...') { return; }
|
||||||
|
|
||||||
|
const reactionsList = await misskeyApi('reaction-stats', { site: tab.value === 'site' });
|
||||||
|
|
||||||
|
const res = reactionsList.map((x) => `${x.reaction} ${x.count}`).join('\n');
|
||||||
|
|
||||||
|
if (tab.value === 'site') {
|
||||||
|
serverReactionsListMfm.value = res;
|
||||||
|
} else {
|
||||||
|
myReactionsListMfm.value = res;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerActions = computed(() => []);
|
||||||
|
|
||||||
|
const headerTabs = computed(() => [{
|
||||||
|
key: 'me',
|
||||||
|
title: $i.username,
|
||||||
|
icon: 'ti ti-user',
|
||||||
|
}, {
|
||||||
|
key: 'site',
|
||||||
|
title: i18n.ts.instance,
|
||||||
|
icon: 'ti ti-planet',
|
||||||
|
}]);
|
||||||
|
|
||||||
|
definePageMetadata(() => ({
|
||||||
|
title: i18n.ts.reactionStats,
|
||||||
|
icon: 'ti ti-chart-bar',
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-bottom: solid 2px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createdAt {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -575,6 +575,10 @@ const routes: RouteDef[] = [{
|
||||||
}, {
|
}, {
|
||||||
path: '/timeline',
|
path: '/timeline',
|
||||||
component: page(() => import('@/pages/timeline.vue')),
|
component: page(() => import('@/pages/timeline.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/reactions-stats',
|
||||||
|
component: page(() => import('@/pages/reaction-stats.vue')),
|
||||||
|
loginRequired: true,
|
||||||
}, {
|
}, {
|
||||||
name: 'index',
|
name: 'index',
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|
|
@ -1724,6 +1724,8 @@ declare namespace entities {
|
||||||
PingResponse,
|
PingResponse,
|
||||||
PinnedUsersResponse,
|
PinnedUsersResponse,
|
||||||
PromoReadRequest,
|
PromoReadRequest,
|
||||||
|
ReactionStatsRequest,
|
||||||
|
ReactionStatsResponse,
|
||||||
RenoteMuteCreateRequest,
|
RenoteMuteCreateRequest,
|
||||||
RenoteMuteDeleteRequest,
|
RenoteMuteDeleteRequest,
|
||||||
RenoteMuteListRequest,
|
RenoteMuteListRequest,
|
||||||
|
@ -2937,6 +2939,12 @@ type QueueStats = {
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type QueueStatsLog = QueueStats[];
|
type QueueStatsLog = QueueStats[];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type ReactionStatsRequest = operations['reaction-stats']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type ReactionStatsResponse = operations['reaction-stats']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json'];
|
type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
|
|
@ -3671,6 +3671,17 @@ declare module '../api.js' {
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||||
|
*/
|
||||||
|
request<E extends 'reaction-stats', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No description provided.
|
* No description provided.
|
||||||
*
|
*
|
||||||
|
|
|
@ -489,6 +489,8 @@ import type {
|
||||||
PingResponse,
|
PingResponse,
|
||||||
PinnedUsersResponse,
|
PinnedUsersResponse,
|
||||||
PromoReadRequest,
|
PromoReadRequest,
|
||||||
|
ReactionStatsRequest,
|
||||||
|
ReactionStatsResponse,
|
||||||
RenoteMuteCreateRequest,
|
RenoteMuteCreateRequest,
|
||||||
RenoteMuteDeleteRequest,
|
RenoteMuteDeleteRequest,
|
||||||
RenoteMuteListRequest,
|
RenoteMuteListRequest,
|
||||||
|
@ -915,6 +917,7 @@ export type Endpoints = {
|
||||||
'ping': { req: EmptyRequest; res: PingResponse };
|
'ping': { req: EmptyRequest; res: PingResponse };
|
||||||
'pinned-users': { req: EmptyRequest; res: PinnedUsersResponse };
|
'pinned-users': { req: EmptyRequest; res: PinnedUsersResponse };
|
||||||
'promo/read': { req: PromoReadRequest; res: EmptyResponse };
|
'promo/read': { req: PromoReadRequest; res: EmptyResponse };
|
||||||
|
'reaction-stats': { req: ReactionStatsRequest; res: ReactionStatsResponse };
|
||||||
'renote-mute/create': { req: RenoteMuteCreateRequest; res: EmptyResponse };
|
'renote-mute/create': { req: RenoteMuteCreateRequest; res: EmptyResponse };
|
||||||
'renote-mute/delete': { req: RenoteMuteDeleteRequest; res: EmptyResponse };
|
'renote-mute/delete': { req: RenoteMuteDeleteRequest; res: EmptyResponse };
|
||||||
'renote-mute/list': { req: RenoteMuteListRequest; res: RenoteMuteListResponse };
|
'renote-mute/list': { req: RenoteMuteListRequest; res: RenoteMuteListResponse };
|
||||||
|
|
|
@ -492,6 +492,8 @@ export type PagesUpdateRequest = operations['pages___update']['requestBody']['co
|
||||||
export type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
|
export type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
|
||||||
export type PinnedUsersResponse = operations['pinned-users']['responses']['200']['content']['application/json'];
|
export type PinnedUsersResponse = operations['pinned-users']['responses']['200']['content']['application/json'];
|
||||||
export type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json'];
|
export type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json'];
|
||||||
|
export type ReactionStatsRequest = operations['reaction-stats']['requestBody']['content']['application/json'];
|
||||||
|
export type ReactionStatsResponse = operations['reaction-stats']['responses']['200']['content']['application/json'];
|
||||||
export type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json'];
|
export type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json'];
|
||||||
export type RenoteMuteDeleteRequest = operations['renote-mute___delete']['requestBody']['content']['application/json'];
|
export type RenoteMuteDeleteRequest = operations['renote-mute___delete']['requestBody']['content']['application/json'];
|
||||||
export type RenoteMuteListRequest = operations['renote-mute___list']['requestBody']['content']['application/json'];
|
export type RenoteMuteListRequest = operations['renote-mute___list']['requestBody']['content']['application/json'];
|
||||||
|
|
|
@ -3176,6 +3176,15 @@ export type paths = {
|
||||||
*/
|
*/
|
||||||
post: operations['promo___read'];
|
post: operations['promo___read'];
|
||||||
};
|
};
|
||||||
|
'/reaction-stats': {
|
||||||
|
/**
|
||||||
|
* reaction-stats
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||||
|
*/
|
||||||
|
post: operations['reaction-stats'];
|
||||||
|
};
|
||||||
'/renote-mute/create': {
|
'/renote-mute/create': {
|
||||||
/**
|
/**
|
||||||
* renote-mute/create
|
* renote-mute/create
|
||||||
|
@ -24856,6 +24865,63 @@ export type operations = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* reaction-stats
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||||
|
*/
|
||||||
|
'reaction-stats': {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
/** @default false */
|
||||||
|
site?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK (with results) */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
reaction: string;
|
||||||
|
count: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @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'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* renote-mute/create
|
* renote-mute/create
|
||||||
* @description No description provided.
|
* @description No description provided.
|
||||||
|
|
Loading…
Reference in New Issue