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) | ||||
| 
 | ||||
| ### Improvements | ||||
| - ページロードエラーページにリロードボタンを追加 | ||||
| 
 | ||||
| ### Bugfixes | ||||
| 
 | ||||
| --> | ||||
| 
 | ||||
| ## 12.x.x (unreleased) | ||||
| 
 | ||||
| ### Improvements | ||||
| - クライアント: ユーザーのリアクション一覧を見れるように | ||||
| - API: ユーザーのリアクション一覧を取得する users/reactions を追加 | ||||
| 
 | ||||
| ### Bugfixes | ||||
| 
 | ||||
| ## 12.92.0 (2021/10/16) | ||||
| 
 | ||||
| ### Improvements | ||||
|  |  | |||
|  | @ -181,6 +181,7 @@ | |||
| 				</template> | ||||
| 				<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"/> | ||||
| 				<XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/> | ||||
| 				<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/> | ||||
| 				<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/> | ||||
| 				<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/> | ||||
|  | @ -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, | ||||
|  |  | |||
							
								
								
									
										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 { 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<NoteReaction> { | ||||
| 	public async pack( | ||||
| 		src: NoteReaction['id'] | NoteReaction, | ||||
| 		me?: { id: User['id'] } | null | undefined | ||||
| 		me?: { id: User['id'] } | null | undefined, | ||||
| 		options?: { | ||||
| 			withNote: boolean; | ||||
| 		}, | ||||
| 	): Promise<Packed<'NoteReaction'>> { | ||||
| 		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<NoteReaction> { | |||
| 			createdAt: reaction.createdAt.toISOString(), | ||||
| 			user: await Users.pack(reaction.userId, me), | ||||
| 			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…
	
	Add table
		Add a link
		
	
		Reference in a new issue