diff --git a/CHANGELOG.md b/CHANGELOG.md
index 873716a3d5..1139707b5a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,12 +2,19 @@
## 12.x.x (unreleased)
### Improvements
-- ページロードエラーページにリロードボタンを追加
### Bugfixes
-->
+## 12.x.x (unreleased)
+
+### Improvements
+- クライアント: ユーザーのリアクション一覧を見れるように
+- API: ユーザーのリアクション一覧を取得する users/reactions を追加
+
+### Bugfixes
+
## 12.92.0 (2021/10/16)
### Improvements
diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
index 0ddf73d572..6811dff2db 100644
--- a/src/client/pages/user/index.vue
+++ b/src/client/pages/user/index.vue
@@ -181,6 +181,7 @@
+
@@ -223,6 +224,7 @@ export default defineComponent({
MkTab,
MkInfo,
XFollowList: defineAsyncComponent(() => import('./follow-list.vue')),
+ XReactions: defineAsyncComponent(() => import('./reactions.vue')),
XClips: defineAsyncComponent(() => import('./clips.vue')),
XPages: defineAsyncComponent(() => import('./pages.vue')),
XGallery: defineAsyncComponent(() => import('./gallery.vue')),
@@ -268,6 +270,11 @@ export default defineComponent({
title: this.$ts.overview,
icon: 'fas fa-home',
onClick: () => { this.$router.push('/@' + getAcct(this.user)); },
+ }, {
+ active: this.page === 'reactions',
+ title: this.$ts.reaction,
+ icon: 'fas fa-laugh',
+ onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/reactions'); },
}, {
active: this.page === 'clips',
title: this.$ts.clips,
diff --git a/src/client/pages/user/reactions.vue b/src/client/pages/user/reactions.vue
new file mode 100644
index 0000000000..5ac7e01027
--- /dev/null
+++ b/src/client/pages/user/reactions.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts
index ba74076f6c..5d86065526 100644
--- a/src/models/repositories/note-reaction.ts
+++ b/src/models/repositories/note-reaction.ts
@@ -1,6 +1,6 @@
import { EntityRepository, Repository } from 'typeorm';
import { NoteReaction } from '@/models/entities/note-reaction';
-import { Users } from '../index';
+import { Notes, Users } from '../index';
import { Packed } from '@/misc/schema';
import { convertLegacyReaction } from '@/misc/reaction-lib';
import { User } from '@/models/entities/user';
@@ -9,8 +9,15 @@ import { User } from '@/models/entities/user';
export class NoteReactionRepository extends Repository {
public async pack(
src: NoteReaction['id'] | NoteReaction,
- me?: { id: User['id'] } | null | undefined
+ me?: { id: User['id'] } | null | undefined,
+ options?: {
+ withNote: boolean;
+ },
): Promise> {
+ const opts = Object.assign({
+ withNote: false,
+ }, options);
+
const reaction = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
@@ -18,6 +25,9 @@ export class NoteReactionRepository extends Repository {
createdAt: reaction.createdAt.toISOString(),
user: await Users.pack(reaction.userId, me),
type: convertLegacyReaction(reaction.reaction),
+ ...(opts.withNote ? {
+ note: await Notes.pack(reaction.noteId, me),
+ } : {})
};
}
}
diff --git a/src/server/api/endpoints/users/reactions.ts b/src/server/api/endpoints/users/reactions.ts
new file mode 100644
index 0000000000..44d7887482
--- /dev/null
+++ b/src/server/api/endpoints/users/reactions.ts
@@ -0,0 +1,67 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { NoteReactions } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+
+export const meta = {
+ tags: ['users', 'reactions'],
+
+ requireCredential: false as const,
+
+ params: {
+ userId: {
+ validator: $.type(ID),
+ },
+
+ limit: {
+ validator: $.optional.num.range(1, 100),
+ default: 10,
+ },
+
+ sinceId: {
+ validator: $.optional.type(ID),
+ },
+
+ untilId: {
+ validator: $.optional.type(ID),
+ },
+
+ sinceDate: {
+ validator: $.optional.num,
+ },
+
+ untilDate: {
+ validator: $.optional.num,
+ },
+ },
+
+ res: {
+ type: 'array' as const,
+ optional: false as const, nullable: false as const,
+ items: {
+ type: 'object' as const,
+ optional: false as const, nullable: false as const,
+ ref: 'NoteReaction',
+ }
+ },
+
+ errors: {
+ }
+};
+
+export default define(meta, async (ps, me) => {
+ const query = makePaginationQuery(NoteReactions.createQueryBuilder('reaction'),
+ ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere(`reaction.userId = :userId`, { userId: ps.userId })
+ .leftJoinAndSelect('reaction.note', 'note');
+
+ generateVisibilityQuery(query, me);
+
+ const reactions = await query
+ .take(ps.limit!)
+ .getMany();
+
+ return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, me, { withNote: true })));
+});