forked from FoundKeyGang/FoundKey
feat: ユーザーのリアクション一覧を見れるように
This commit is contained in:
parent
8b646822fc
commit
835aad44bb
5 changed files with 175 additions and 3 deletions
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
81
src/client/pages/user/reactions.vue
Normal file
81
src/client/pages/user/reactions.vue
Normal 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>
|
|
@ -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),
|
||||||
|
} : {})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
67
src/server/api/endpoints/users/reactions.ts
Normal file
67
src/server/api/endpoints/users/reactions.ts
Normal 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 })));
|
||||||
|
});
|
Loading…
Reference in a new issue