Merge 0b47e8c746
into 30df768d26
This commit is contained in:
commit
16a9ddbbae
|
@ -5254,6 +5254,14 @@ export interface Locale extends ILocale {
|
|||
* このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。
|
||||
*/
|
||||
"federationDisabled": string;
|
||||
/**
|
||||
* リアクション統計
|
||||
*/
|
||||
"reactionStats": string;
|
||||
/**
|
||||
* 最も使用された絵文字リアクション上位100件を表示します
|
||||
*/
|
||||
"reactionStatsDescription": string;
|
||||
"_accountSettings": {
|
||||
/**
|
||||
* コンテンツの表示にログインを必須にする
|
||||
|
|
|
@ -1309,6 +1309,8 @@ availableRoles: "利用可能なロール"
|
|||
acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。"
|
||||
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
|
||||
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
|
||||
reactionStats: "リアクション統計"
|
||||
reactionStatsDescription: "最も使用された絵文字リアクション上位100件を表示します"
|
||||
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
|
||||
|
|
|
@ -339,6 +339,7 @@ export * as 'pages/update' from './endpoints/pages/update.js';
|
|||
export * as 'ping' from './endpoints/ping.js';
|
||||
export * as 'pinned-users' from './endpoints/pinned-users.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/delete' from './endpoints/renote-mute/delete.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),
|
||||
to: `/@${$i?.username}`,
|
||||
},
|
||||
reactionStats: {
|
||||
title: i18n.ts.reactionStats,
|
||||
icon: 'ti ti-chart-bar',
|
||||
to: '/reaction-stats',
|
||||
},
|
||||
cacheClear: {
|
||||
title: i18n.ts.clearCache,
|
||||
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',
|
||||
component: page(() => import('@/pages/timeline.vue')),
|
||||
}, {
|
||||
path: '/reactions-stats',
|
||||
component: page(() => import('@/pages/reaction-stats.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
name: 'index',
|
||||
path: '/',
|
||||
|
|
|
@ -1724,6 +1724,8 @@ declare namespace entities {
|
|||
PingResponse,
|
||||
PinnedUsersResponse,
|
||||
PromoReadRequest,
|
||||
ReactionStatsRequest,
|
||||
ReactionStatsResponse,
|
||||
RenoteMuteCreateRequest,
|
||||
RenoteMuteDeleteRequest,
|
||||
RenoteMuteListRequest,
|
||||
|
@ -2937,6 +2939,12 @@ type QueueStats = {
|
|||
// @public (undocumented)
|
||||
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)
|
||||
type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json'];
|
||||
|
||||
|
|
|
@ -3671,6 +3671,17 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): 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.
|
||||
*
|
||||
|
|
|
@ -489,6 +489,8 @@ import type {
|
|||
PingResponse,
|
||||
PinnedUsersResponse,
|
||||
PromoReadRequest,
|
||||
ReactionStatsRequest,
|
||||
ReactionStatsResponse,
|
||||
RenoteMuteCreateRequest,
|
||||
RenoteMuteDeleteRequest,
|
||||
RenoteMuteListRequest,
|
||||
|
@ -915,6 +917,7 @@ export type Endpoints = {
|
|||
'ping': { req: EmptyRequest; res: PingResponse };
|
||||
'pinned-users': { req: EmptyRequest; res: PinnedUsersResponse };
|
||||
'promo/read': { req: PromoReadRequest; res: EmptyResponse };
|
||||
'reaction-stats': { req: ReactionStatsRequest; res: ReactionStatsResponse };
|
||||
'renote-mute/create': { req: RenoteMuteCreateRequest; res: EmptyResponse };
|
||||
'renote-mute/delete': { req: RenoteMuteDeleteRequest; res: EmptyResponse };
|
||||
'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 PinnedUsersResponse = operations['pinned-users']['responses']['200']['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 RenoteMuteDeleteRequest = operations['renote-mute___delete']['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'];
|
||||
};
|
||||
'/reaction-stats': {
|
||||
/**
|
||||
* reaction-stats
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||
*/
|
||||
post: operations['reaction-stats'];
|
||||
};
|
||||
'/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
|
||||
* @description No description provided.
|
||||
|
|
Loading…
Reference in New Issue