feat: ユーザーのリアクション一覧を見れるように

This commit is contained in:
syuilo 2021-10-17 01:33:15 +09:00
parent 8b646822fc
commit 835aad44bb
5 changed files with 175 additions and 3 deletions

View File

@ -2,12 +2,19 @@
## 12.x.x (unreleased) ## 12.x.x (unreleased)
### Improvements ### Improvements
- ページロードエラーページにリロードボタンを追加
### Bugfixes ### Bugfixes
--> -->
## 12.x.x (unreleased)
### Improvements
- クライアント: ユーザーのリアクション一覧を見れるように
- API: ユーザーのリアクション一覧を取得する users/reactions を追加
### Bugfixes
## 12.92.0 (2021/10/16) ## 12.92.0 (2021/10/16)
### Improvements ### Improvements

View File

@ -181,6 +181,7 @@
</template> </template>
<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/> <XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/>
<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/> <XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/>
<XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/>
<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/> <XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/> <XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/> <XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
@ -223,6 +224,7 @@ export default defineComponent({
MkTab, MkTab,
MkInfo, MkInfo,
XFollowList: defineAsyncComponent(() => import('./follow-list.vue')), XFollowList: defineAsyncComponent(() => import('./follow-list.vue')),
XReactions: defineAsyncComponent(() => import('./reactions.vue')),
XClips: defineAsyncComponent(() => import('./clips.vue')), XClips: defineAsyncComponent(() => import('./clips.vue')),
XPages: defineAsyncComponent(() => import('./pages.vue')), XPages: defineAsyncComponent(() => import('./pages.vue')),
XGallery: defineAsyncComponent(() => import('./gallery.vue')), XGallery: defineAsyncComponent(() => import('./gallery.vue')),
@ -268,6 +270,11 @@ export default defineComponent({
title: this.$ts.overview, title: this.$ts.overview,
icon: 'fas fa-home', icon: 'fas fa-home',
onClick: () => { this.$router.push('/@' + getAcct(this.user)); }, 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', active: this.page === 'clips',
title: this.$ts.clips, title: this.$ts.clips,

View File

@ -0,0 +1,81 @@
<template>
<div>
<MkPagination :pagination="pagination" #default="{items}" ref="list">
<div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap afdcfbfb">
<div class="header">
<MkAvatar class="avatar" :user="user"/>
<MkReactionIcon class="reaction" :reaction="item.type" :custom-emojis="item.note.emojis" :no-style="true"/>
<MkTime :time="item.createdAt" class="createdAt"/>
</div>
<MkNote :note="item.note" @update:note="updated(note, $event)" :key="item.id"/>
</div>
</MkPagination>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkPagination from '@client/components/ui/pagination.vue';
import MkNote from '@client/components/note.vue';
import MkReactionIcon from '@client/components/reaction-icon.vue';
export default defineComponent({
components: {
MkPagination,
MkNote,
MkReactionIcon,
},
props: {
user: {
type: Object,
required: true
},
},
data() {
return {
pagination: {
endpoint: 'users/reactions',
limit: 20,
params: {
userId: this.user.id,
}
},
};
},
watch: {
user() {
this.$refs.list.reload();
}
},
});
</script>
<style lang="scss" scoped>
.afdcfbfb {
> .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>

View File

@ -1,6 +1,6 @@
import { EntityRepository, Repository } from 'typeorm'; import { EntityRepository, Repository } from 'typeorm';
import { NoteReaction } from '@/models/entities/note-reaction'; import { NoteReaction } from '@/models/entities/note-reaction';
import { Users } from '../index'; import { Notes, Users } from '../index';
import { Packed } from '@/misc/schema'; import { Packed } from '@/misc/schema';
import { convertLegacyReaction } from '@/misc/reaction-lib'; import { convertLegacyReaction } from '@/misc/reaction-lib';
import { User } from '@/models/entities/user'; import { User } from '@/models/entities/user';
@ -9,8 +9,15 @@ import { User } from '@/models/entities/user';
export class NoteReactionRepository extends Repository<NoteReaction> { export class NoteReactionRepository extends Repository<NoteReaction> {
public async pack( public async pack(
src: NoteReaction['id'] | NoteReaction, src: NoteReaction['id'] | NoteReaction,
me?: { id: User['id'] } | null | undefined me?: { id: User['id'] } | null | undefined,
options?: {
withNote: boolean;
},
): Promise<Packed<'NoteReaction'>> { ): Promise<Packed<'NoteReaction'>> {
const opts = Object.assign({
withNote: false,
}, options);
const reaction = typeof src === 'object' ? src : await this.findOneOrFail(src); const reaction = typeof src === 'object' ? src : await this.findOneOrFail(src);
return { return {
@ -18,6 +25,9 @@ export class NoteReactionRepository extends Repository<NoteReaction> {
createdAt: reaction.createdAt.toISOString(), createdAt: reaction.createdAt.toISOString(),
user: await Users.pack(reaction.userId, me), user: await Users.pack(reaction.userId, me),
type: convertLegacyReaction(reaction.reaction), type: convertLegacyReaction(reaction.reaction),
...(opts.withNote ? {
note: await Notes.pack(reaction.noteId, me),
} : {})
}; };
} }
} }

View File

@ -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 })));
});