enhance: ユーザー検索の精度を強化
This commit is contained in:
		
							parent
							
								
									0c21ae226b
								
							
						
					
					
						commit
						dec69cc67b
					
				
					 5 changed files with 103 additions and 64 deletions
				
			
		|  | @ -11,7 +11,9 @@ | |||
| 
 | ||||
| ### Improvements | ||||
| - クライアント: ユーザーのリアクション一覧を見れるように | ||||
| - クライアント: ユーザー検索の精度を強化 | ||||
| - API: ユーザーのリアクション一覧を取得する users/reactions を追加 | ||||
| - API: users/search および users/search-by-username-and-host を強化 | ||||
| 
 | ||||
| ### Bugfixes | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,7 +22,6 @@ export default defineComponent({ | |||
| 		} | ||||
| 	}, | ||||
| 	render() { | ||||
| 		const label = this.$slots.desc(); | ||||
| 		let options = this.$slots.default(); | ||||
| 
 | ||||
| 		// なぜかFragmentになることがあるため | ||||
|  | @ -31,7 +30,6 @@ export default defineComponent({ | |||
| 		return h('div', { | ||||
| 			class: 'novjtcto' | ||||
| 		}, [ | ||||
| 			h('div', { class: 'label' }, label), | ||||
| 			...options.map(option => h(MkRadio, { | ||||
| 				key: option.key, | ||||
| 				value: option.props.value, | ||||
|  | @ -45,16 +43,6 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss"> | ||||
| .novjtcto { | ||||
| 	> .label { | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 0 0 8px 12px; | ||||
| 		user-select: none; | ||||
| 
 | ||||
| 		&:empty { | ||||
| 			display: none; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&:first-child { | ||||
| 		margin-top: 0; | ||||
| 	} | ||||
|  |  | |||
|  | @ -65,13 +65,18 @@ | |||
| 			</div> | ||||
| 			<div v-else-if="tab === 'search'"> | ||||
| 				<div class="_isolated"> | ||||
| 					<MkInput v-model="query" :debounce="true" type="search"> | ||||
| 					<MkInput v-model="searchQuery" :debounce="true" type="search"> | ||||
| 						<template #prefix><i class="fas fa-search"></i></template> | ||||
| 						<template #label>{{ $ts.searchUser }}</template> | ||||
| 					</MkInput> | ||||
| 					<MkRadios v-model="searchScope"> | ||||
| 						<option value="local">{{ $ts.local }}</option> | ||||
| 						<option value="remote">{{ $ts.remote }}</option> | ||||
| 						<option value="both">{{ $ts.both }}</option> | ||||
| 					</MkRadios> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<XUserList v-if="query" class="_gap" :pagination="searchPagination" ref="search"/> | ||||
| 				<XUserList v-if="searchQuery" class="_gap" :pagination="searchPagination" ref="search"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
|  | @ -83,6 +88,7 @@ import { computed, defineComponent } from 'vue'; | |||
| import XUserList from '@client/components/user-list.vue'; | ||||
| import MkFolder from '@client/components/ui/folder.vue'; | ||||
| import MkInput from '@client/components/form/input.vue'; | ||||
| import MkRadios from '@client/components/form/radios.vue'; | ||||
| import number from '@client/filters/number'; | ||||
| import * as os from '@client/os'; | ||||
| import * as symbols from '@client/symbols'; | ||||
|  | @ -92,6 +98,7 @@ export default defineComponent({ | |||
| 		XUserList, | ||||
| 		MkFolder, | ||||
| 		MkInput, | ||||
| 		MkRadios, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
|  | @ -158,14 +165,16 @@ export default defineComponent({ | |||
| 			searchPagination: { | ||||
| 				endpoint: 'users/search', | ||||
| 				limit: 10, | ||||
| 				params: computed(() => (this.query && this.query !== '') ? { | ||||
| 					query: this.query | ||||
| 				params: computed(() => (this.searchQuery && this.searchQuery !== '') ? { | ||||
| 					query: this.searchQuery, | ||||
| 					scope: this.searchScope, | ||||
| 				} : null) | ||||
| 			}, | ||||
| 			tagsLocal: [], | ||||
| 			tagsRemote: [], | ||||
| 			stats: null, | ||||
| 			query: null, | ||||
| 			searchQuery: null, | ||||
| 			searchScope: 'both', | ||||
| 			num: number, | ||||
| 		}; | ||||
| 	}, | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| import $ from 'cafy'; | ||||
| import define from '../../define'; | ||||
| import { Users } from '@/models/index'; | ||||
| import { Brackets } from 'typeorm'; | ||||
| import { USER_ACTIVE_THRESHOLD } from '@/const'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['users'], | ||||
|  | @ -64,8 +66,11 @@ export default define(meta, async (ps, me) => { | |||
| 			.where('user.host IS NULL') | ||||
| 			.andWhere('user.isSuspended = FALSE') | ||||
| 			.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }) | ||||
| 			.andWhere('user.updatedAt IS NOT NULL') | ||||
| 			.orderBy('user.updatedAt', 'DESC') | ||||
| 			.andWhere(new Brackets(qb => { qb | ||||
| 				.where('user.lastActiveDate IS NULL') | ||||
| 				.orWhere('user.lastActiveDate > :activeThreshold', { activeThreshold: new Date(Date.now() - USER_ACTIVE_THRESHOLD) }); | ||||
| 			})) | ||||
| 			.orderBy('user.lastActiveDate', 'DESC', 'NULLS LAST') | ||||
| 			.take(ps.limit!) | ||||
| 			.skip(ps.offset) | ||||
| 			.getMany(); | ||||
|  |  | |||
|  | @ -2,6 +2,8 @@ import $ from 'cafy'; | |||
| import define from '../../define'; | ||||
| import { UserProfiles, Users } from '@/models/index'; | ||||
| import { User } from '@/models/entities/user'; | ||||
| import { Brackets } from 'typeorm'; | ||||
| import { USER_ACTIVE_THRESHOLD } from '@/const'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['users'], | ||||
|  | @ -23,9 +25,9 @@ export const meta = { | |||
| 			default: 10, | ||||
| 		}, | ||||
| 
 | ||||
| 		localOnly: { | ||||
| 			validator: $.optional.bool, | ||||
| 			default: false, | ||||
| 		scope: { | ||||
| 			validator: $.optional.str.or(['local', 'remote', 'both']), | ||||
| 			default: 'both', | ||||
| 		}, | ||||
| 
 | ||||
| 		detail: { | ||||
|  | @ -51,58 +53,91 @@ export default define(meta, async (ps, me) => { | |||
| 	let users: User[] = []; | ||||
| 
 | ||||
| 	if (isUsername) { | ||||
| 		users = await Users.createQueryBuilder('user') | ||||
| 			.where('user.host IS NULL') | ||||
| 			.andWhere('user.isSuspended = FALSE') | ||||
| 			.andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) | ||||
| 			.andWhere('user.updatedAt IS NOT NULL') | ||||
| 			.orderBy('user.updatedAt', 'DESC') | ||||
| 			.take(ps.limit!) | ||||
| 			.skip(ps.offset) | ||||
| 			.getMany(); | ||||
| 		const usernameQuery = Users.createQueryBuilder('user') | ||||
| 			.where('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) | ||||
| 			.andWhere(new Brackets(qb => { qb | ||||
| 				.where('user.lastActiveDate IS NULL') | ||||
| 				.orWhere('user.lastActiveDate > :activeThreshold', { activeThreshold: new Date(Date.now() - USER_ACTIVE_THRESHOLD) }); | ||||
| 			})) | ||||
| 			.andWhere('user.isSuspended = FALSE'); | ||||
| 
 | ||||
| 		if (users.length < ps.limit! && !ps.localOnly) { | ||||
| 			const otherUsers = await Users.createQueryBuilder('user') | ||||
| 				.where('user.host IS NOT NULL') | ||||
| 				.andWhere('user.isSuspended = FALSE') | ||||
| 				.andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) | ||||
| 				.andWhere('user.updatedAt IS NOT NULL') | ||||
| 				.orderBy('user.updatedAt', 'DESC') | ||||
| 				.take(ps.limit! - users.length) | ||||
| 				.getMany(); | ||||
| 
 | ||||
| 			users = users.concat(otherUsers); | ||||
| 		if (ps.scope === 'local') { | ||||
| 			usernameQuery | ||||
| 				.andWhere('user.host IS NULL') | ||||
| 				.orderBy('user.lastActiveDate', 'DESC', 'NULLS LAST'); | ||||
| 		} else if (ps.scope === 'remote') { | ||||
| 			usernameQuery | ||||
| 				.andWhere('user.host IS NOT NULL') | ||||
| 				.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); | ||||
| 		} else { // both
 | ||||
| 			usernameQuery | ||||
| 				.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); | ||||
| 		} | ||||
| 	} else { | ||||
| 		const profQuery = UserProfiles.createQueryBuilder('prof') | ||||
| 			.select('prof.userId') | ||||
| 			.where('prof.userHost IS NULL') | ||||
| 			.andWhere('prof.description ilike :query', { query: '%' + ps.query + '%' }); | ||||
| 
 | ||||
| 		users = await Users.createQueryBuilder('user') | ||||
| 			.where(`user.id IN (${ profQuery.getQuery() })`) | ||||
| 			.setParameters(profQuery.getParameters()) | ||||
| 			.andWhere('user.updatedAt IS NOT NULL') | ||||
| 			.orderBy('user.updatedAt', 'DESC') | ||||
| 		users = await usernameQuery | ||||
| 			.take(ps.limit!) | ||||
| 			.skip(ps.offset) | ||||
| 			.getMany(); | ||||
| 	} else { | ||||
| 		const nameQuery = Users.createQueryBuilder('user') | ||||
| 			.where('user.name ilike :query', { query: '%' + ps.query + '%' }) | ||||
| 			.andWhere(new Brackets(qb => { qb | ||||
| 				.where('user.lastActiveDate IS NULL') | ||||
| 				.orWhere('user.lastActiveDate > :activeThreshold', { activeThreshold: new Date(Date.now() - USER_ACTIVE_THRESHOLD) }); | ||||
| 			})) | ||||
| 			.andWhere('user.isSuspended = FALSE'); | ||||
| 
 | ||||
| 		if (ps.scope === 'local') { | ||||
| 			nameQuery | ||||
| 				.andWhere('user.host IS NULL') | ||||
| 				.orderBy('user.lastActiveDate', 'DESC', 'NULLS LAST'); | ||||
| 		} else if (ps.scope === 'remote') { | ||||
| 			nameQuery | ||||
| 				.andWhere('user.host IS NOT NULL') | ||||
| 				.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); | ||||
| 		} else { // both
 | ||||
| 			nameQuery | ||||
| 				.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); | ||||
| 		} | ||||
| 
 | ||||
| 		users = await nameQuery | ||||
| 			.take(ps.limit!) | ||||
| 			.skip(ps.offset) | ||||
| 			.getMany(); | ||||
| 
 | ||||
| 		if (users.length < ps.limit! && !ps.localOnly) { | ||||
| 			const profQuery2 = UserProfiles.createQueryBuilder('prof') | ||||
| 		if (users.length < ps.limit!) { | ||||
| 			const profQuery = UserProfiles.createQueryBuilder('prof') | ||||
| 				.select('prof.userId') | ||||
| 				.where('prof.userHost IS NOT NULL') | ||||
| 				.andWhere('prof.description ilike :query', { query: '%' + ps.query + '%' }); | ||||
| 				.where('prof.description ilike :query', { query: '%' + ps.query + '%' }); | ||||
| 
 | ||||
| 			const otherUsers = await Users.createQueryBuilder('user') | ||||
| 				.where(`user.id IN (${ profQuery2.getQuery() })`) | ||||
| 				.setParameters(profQuery2.getParameters()) | ||||
| 				.andWhere('user.updatedAt IS NOT NULL') | ||||
| 				.orderBy('user.updatedAt', 'DESC') | ||||
| 				.take(ps.limit! - users.length) | ||||
| 				.getMany(); | ||||
| 			if (ps.scope === 'local') { | ||||
| 				profQuery.andWhere('prof.userHost IS NULL'); | ||||
| 			} else if (ps.scope === 'remote') { | ||||
| 				profQuery.andWhere('prof.userHost IS NOT NULL'); | ||||
| 			} | ||||
| 
 | ||||
| 			users = users.concat(otherUsers); | ||||
| 			const query = Users.createQueryBuilder('user') | ||||
| 				.where(`user.id IN (${ profQuery.getQuery() })`) | ||||
| 				.andWhere(new Brackets(qb => { qb | ||||
| 					.where('user.lastActiveDate IS NULL') | ||||
| 					.orWhere('user.lastActiveDate > :activeThreshold', { activeThreshold: new Date(Date.now() - USER_ACTIVE_THRESHOLD) }); | ||||
| 				})) | ||||
| 				.andWhere('user.isSuspended = FALSE') | ||||
| 				.setParameters(profQuery.getParameters()); | ||||
| 
 | ||||
| 			if (ps.scope === 'local') { | ||||
| 				query.orderBy('user.lastActiveDate', 'DESC', 'NULLS LAST'); | ||||
| 			} else if (ps.scope === 'remote') { | ||||
| 				query.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); | ||||
| 			} else { // both
 | ||||
| 				query.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); | ||||
| 			} | ||||
| 
 | ||||
| 			users = users.concat(await query | ||||
| 				.take(ps.limit!) | ||||
| 				.skip(ps.offset) | ||||
| 				.getMany() | ||||
| 			); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue