1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { Subscription } from 'rxjs';
4 import { retryWhen, delay, take } from 'rxjs/operators';
17 WebSocketJsonResponse,
20 } from '../interfaces';
21 import { WebSocketService } from '../services';
25 routeSearchTypeToEnum,
29 createPostLikeFindRes,
32 import { PostListing } from './post-listing';
33 import { UserListing } from './user-listing';
34 import { CommunityLink } from './community-link';
35 import { SortSelect } from './sort-select';
36 import { CommentNodes } from './comment-nodes';
37 import { i18n } from '../i18next';
39 interface SearchState {
44 searchResponse: SearchResponse;
50 export class Search extends Component<any, SearchState> {
51 private subscription: Subscription;
52 private emptyState: SearchState = {
53 q: Search.getSearchQueryFromProps(this.props),
54 type_: Search.getSearchTypeFromProps(this.props),
55 sort: Search.getSortTypeFromProps(this.props),
56 page: Search.getPageFromProps(this.props),
57 searchText: Search.getSearchQueryFromProps(this.props),
69 creator_id: undefined,
71 creator_name: undefined,
72 number_of_users: undefined,
73 number_of_posts: undefined,
74 number_of_comments: undefined,
75 number_of_communities: undefined,
76 enable_downvotes: undefined,
77 open_registration: undefined,
78 enable_nsfw: undefined,
82 static getSearchQueryFromProps(props: any): string {
83 return props.match.params.q ? props.match.params.q : '';
86 static getSearchTypeFromProps(props: any): SearchType {
87 return props.match.params.type
88 ? routeSearchTypeToEnum(props.match.params.type)
92 static getSortTypeFromProps(props: any): SortType {
93 return props.match.params.sort
94 ? routeSortTypeToEnum(props.match.params.sort)
98 static getPageFromProps(props: any): number {
99 return props.match.params.page ? Number(props.match.params.page) : 1;
102 constructor(props: any, context: any) {
103 super(props, context);
105 this.state = this.emptyState;
106 this.handleSortChange = this.handleSortChange.bind(this);
108 this.subscription = WebSocketService.Instance.subject
109 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
111 msg => this.parseMessage(msg),
112 err => console.error(err),
113 () => console.log('complete')
116 WebSocketService.Instance.getSite();
123 componentWillUnmount() {
124 this.subscription.unsubscribe();
127 static getDerivedStateFromProps(props) {
129 q: Search.getSearchQueryFromProps(props),
130 type_: Search.getSearchTypeFromProps(props),
131 sort: Search.getSortTypeFromProps(props),
132 page: Search.getPageFromProps(props),
136 componentDidUpdate(_, lastState) {
138 lastState.q !== this.state.q ||
139 lastState.type_ !== this.state.type_ ||
140 lastState.sort !== this.state.sort ||
141 lastState.page !== this.state.page
143 this.setState({ loading: true, searchText: this.state.q });
150 <div class="container">
151 <h5>{i18n.t('search')}</h5>
154 {this.state.type_ == SearchType.All && this.all()}
155 {this.state.type_ == SearchType.Comments && this.comments()}
156 {this.state.type_ == SearchType.Posts && this.posts()}
157 {this.state.type_ == SearchType.Communities && this.communities()}
158 {this.state.type_ == SearchType.Users && this.users()}
159 {this.resultsCount() == 0 && <span>{i18n.t('no_results')}</span>}
169 onSubmit={linkEvent(this, this.handleSearchSubmit)}
173 class="form-control mr-2"
174 value={this.state.searchText}
175 placeholder={`${i18n.t('search')}...`}
176 onInput={linkEvent(this, this.handleQChange)}
180 <button type="submit" class="btn btn-secondary mr-2">
181 {this.state.loading ? (
182 <svg class="icon icon-spinner spin">
183 <use xlinkHref="#icon-spinner"></use>
186 <span>{i18n.t('search')}</span>
195 <div className="mb-2">
197 value={this.state.type_}
198 onChange={linkEvent(this, this.handleTypeChange)}
199 class="custom-select custom-select-sm w-auto"
201 <option disabled>{i18n.t('type')}</option>
202 <option value={SearchType.All}>{i18n.t('all')}</option>
203 <option value={SearchType.Comments}>{i18n.t('comments')}</option>
204 <option value={SearchType.Posts}>{i18n.t('posts')}</option>
205 <option value={SearchType.Communities}>
206 {i18n.t('communities')}
208 <option value={SearchType.Users}>{i18n.t('users')}</option>
212 sort={this.state.sort}
213 onChange={this.handleSortChange}
222 let combined: Array<{
224 data: Comment | Post | Community | UserView;
226 let comments = this.state.searchResponse.comments.map(e => {
227 return { type_: 'comments', data: e };
229 let posts = this.state.searchResponse.posts.map(e => {
230 return { type_: 'posts', data: e };
232 let communities = this.state.searchResponse.communities.map(e => {
233 return { type_: 'communities', data: e };
235 let users = this.state.searchResponse.users.map(e => {
236 return { type_: 'users', data: e };
239 combined.push(...comments);
240 combined.push(...posts);
241 combined.push(...communities);
242 combined.push(...users);
245 if (this.state.sort == SortType.New) {
246 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
250 ((b.data as Comment | Post).score |
251 (b.data as Community).number_of_subscribers |
252 (b.data as UserView).comment_score) -
253 ((a.data as Comment | Post).score |
254 (a.data as Community).number_of_subscribers |
255 (a.data as UserView).comment_score)
264 {i.type_ == 'posts' && (
266 post={i.data as Post}
268 enableDownvotes={this.state.site.enable_downvotes}
269 enableNsfw={this.state.site.enable_nsfw}
272 {i.type_ == 'comments' && (
274 nodes={[{ comment: i.data as Comment }]}
277 enableDownvotes={this.state.site.enable_downvotes}
280 {i.type_ == 'communities' && (
281 <div>{this.communityListing(i.data as Community)}</div>
283 {i.type_ == 'users' && (
289 name: (i.data as UserView).name,
290 avatar: (i.data as UserView).avatar,
294 <span>{` - ${i18n.t('number_of_comments', {
295 count: (i.data as UserView).number_of_comments,
309 nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
312 enableDownvotes={this.state.site.enable_downvotes}
320 {this.state.searchResponse.posts.map(post => (
326 enableDownvotes={this.state.site.enable_downvotes}
327 enableNsfw={this.state.site.enable_nsfw}
336 // Todo possibly create UserListing and CommunityListing
340 {this.state.searchResponse.communities.map(community => (
342 <div class="col-12">{this.communityListing(community)}</div>
349 communityListing(community: Community) {
353 <CommunityLink community={community} />
355 <span>{` - ${community.title} -
356 ${i18n.t('number_of_subscribers', {
357 count: community.number_of_subscribers,
367 {this.state.searchResponse.users.map(user => (
379 <span>{` - ${i18n.t('number_of_comments', {
380 count: user.number_of_comments,
392 {this.state.page > 1 && (
394 class="btn btn-sm btn-secondary mr-1"
395 onClick={linkEvent(this, this.prevPage)}
401 {this.resultsCount() > 0 && (
403 class="btn btn-sm btn-secondary"
404 onClick={linkEvent(this, this.nextPage)}
413 resultsCount(): number {
414 let res = this.state.searchResponse;
417 res.comments.length +
418 res.communities.length +
423 nextPage(i: Search) {
424 i.updateUrl({ page: i.state.page + 1 });
427 prevPage(i: Search) {
428 i.updateUrl({ page: i.state.page - 1 });
432 let form: SearchForm = {
434 type_: SearchType[this.state.type_],
435 sort: SortType[this.state.sort],
436 page: this.state.page,
440 if (this.state.q != '') {
441 WebSocketService.Instance.search(form);
445 handleSortChange(val: SortType) {
446 this.updateUrl({ sort: val, page: 1 });
449 handleTypeChange(i: Search, event: any) {
450 i.updateUrl({ type_: Number(event.target.value), page: 1 });
453 handleSearchSubmit(i: Search, event: any) {
454 event.preventDefault();
456 q: i.state.searchText,
457 type_: i.state.type_,
463 handleQChange(i: Search, event: any) {
464 i.setState({ searchText: event.target.value });
467 updateUrl(paramUpdates: {
473 const qStr = paramUpdates.q || Search.getSearchQueryFromProps(this.props);
475 SearchType[paramUpdates.type_] ||
476 SearchType[Search.getSearchTypeFromProps(this.props)];
478 SortType[paramUpdates.sort] ||
479 SortType[Search.getSortTypeFromProps(this.props)];
480 const page = paramUpdates.page || Search.getPageFromProps(this.props);
481 this.props.history.push(
482 `/search/q/${qStr}/type/${typeStr.toLowerCase()}/sort/${sortStr.toLowerCase()}/page/${page}`
486 parseMessage(msg: WebSocketJsonResponse) {
488 let res = wsJsonToRes(msg);
490 toast(i18n.t(msg.error), 'danger');
492 } else if (res.op == UserOperation.Search) {
493 let data = res.data as SearchResponse;
494 this.state.searchResponse = data;
495 this.state.loading = false;
496 document.title = `${i18n.t('search')} - ${this.state.q} - ${
499 window.scrollTo(0, 0);
500 this.setState(this.state);
501 } else if (res.op == UserOperation.CreateCommentLike) {
502 let data = res.data as CommentResponse;
503 createCommentLikeRes(data, this.state.searchResponse.comments);
504 this.setState(this.state);
505 } else if (res.op == UserOperation.CreatePostLike) {
506 let data = res.data as PostResponse;
507 createPostLikeFindRes(data, this.state.searchResponse.posts);
508 this.setState(this.state);
509 } else if (res.op == UserOperation.GetSite) {
510 let data = res.data as GetSiteResponse;
511 this.state.site = data.site;
512 this.setState(this.state);
513 document.title = `${i18n.t('search')} - ${data.site.name}`;