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,
33 import { PostListing } from './post-listing';
34 import { UserListing } from './user-listing';
35 import { CommunityLink } from './community-link';
36 import { SortSelect } from './sort-select';
37 import { CommentNodes } from './comment-nodes';
38 import { i18n } from '../i18next';
40 interface SearchState {
45 searchResponse: SearchResponse;
51 interface SearchProps {
65 export class Search extends Component<any, SearchState> {
66 private subscription: Subscription;
67 private emptyState: SearchState = {
68 q: Search.getSearchQueryFromProps(this.props),
69 type_: Search.getSearchTypeFromProps(this.props),
70 sort: Search.getSortTypeFromProps(this.props),
71 page: getPageFromProps(this.props),
72 searchText: Search.getSearchQueryFromProps(this.props),
84 creator_id: undefined,
86 creator_name: undefined,
87 number_of_users: undefined,
88 number_of_posts: undefined,
89 number_of_comments: undefined,
90 number_of_communities: undefined,
91 enable_downvotes: undefined,
92 open_registration: undefined,
93 enable_nsfw: undefined,
97 static getSearchQueryFromProps(props: any): string {
98 return props.match.params.q ? props.match.params.q : '';
101 static getSearchTypeFromProps(props: any): SearchType {
102 return props.match.params.type
103 ? routeSearchTypeToEnum(props.match.params.type)
107 static getSortTypeFromProps(props: any): SortType {
108 return props.match.params.sort
109 ? routeSortTypeToEnum(props.match.params.sort)
113 constructor(props: any, context: any) {
114 super(props, context);
116 this.state = this.emptyState;
117 this.handleSortChange = this.handleSortChange.bind(this);
119 this.subscription = WebSocketService.Instance.subject
120 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
122 msg => this.parseMessage(msg),
123 err => console.error(err),
124 () => console.log('complete')
127 WebSocketService.Instance.getSite();
134 componentWillUnmount() {
135 this.subscription.unsubscribe();
138 static getDerivedStateFromProps(props: any): SearchProps {
140 q: Search.getSearchQueryFromProps(props),
141 type_: Search.getSearchTypeFromProps(props),
142 sort: Search.getSortTypeFromProps(props),
143 page: getPageFromProps(props),
147 componentDidUpdate(_: any, lastState: SearchState) {
149 lastState.q !== this.state.q ||
150 lastState.type_ !== this.state.type_ ||
151 lastState.sort !== this.state.sort ||
152 lastState.page !== this.state.page
154 this.setState({ loading: true, searchText: this.state.q });
161 <div class="container">
162 <h5>{i18n.t('search')}</h5>
165 {this.state.type_ == SearchType.All && this.all()}
166 {this.state.type_ == SearchType.Comments && this.comments()}
167 {this.state.type_ == SearchType.Posts && this.posts()}
168 {this.state.type_ == SearchType.Communities && this.communities()}
169 {this.state.type_ == SearchType.Users && this.users()}
170 {this.resultsCount() == 0 && <span>{i18n.t('no_results')}</span>}
180 onSubmit={linkEvent(this, this.handleSearchSubmit)}
184 class="form-control mr-2"
185 value={this.state.searchText}
186 placeholder={`${i18n.t('search')}...`}
187 onInput={linkEvent(this, this.handleQChange)}
191 <button type="submit" class="btn btn-secondary mr-2">
192 {this.state.loading ? (
193 <svg class="icon icon-spinner spin">
194 <use xlinkHref="#icon-spinner"></use>
197 <span>{i18n.t('search')}</span>
206 <div className="mb-2">
208 value={this.state.type_}
209 onChange={linkEvent(this, this.handleTypeChange)}
210 class="custom-select custom-select-sm w-auto"
212 <option disabled>{i18n.t('type')}</option>
213 <option value={SearchType.All}>{i18n.t('all')}</option>
214 <option value={SearchType.Comments}>{i18n.t('comments')}</option>
215 <option value={SearchType.Posts}>{i18n.t('posts')}</option>
216 <option value={SearchType.Communities}>
217 {i18n.t('communities')}
219 <option value={SearchType.Users}>{i18n.t('users')}</option>
223 sort={this.state.sort}
224 onChange={this.handleSortChange}
233 let combined: Array<{
235 data: Comment | Post | Community | UserView;
237 let comments = this.state.searchResponse.comments.map(e => {
238 return { type_: 'comments', data: e };
240 let posts = this.state.searchResponse.posts.map(e => {
241 return { type_: 'posts', data: e };
243 let communities = this.state.searchResponse.communities.map(e => {
244 return { type_: 'communities', data: e };
246 let users = this.state.searchResponse.users.map(e => {
247 return { type_: 'users', data: e };
250 combined.push(...comments);
251 combined.push(...posts);
252 combined.push(...communities);
253 combined.push(...users);
256 if (this.state.sort == SortType.New) {
257 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
261 ((b.data as Comment | Post).score |
262 (b.data as Community).number_of_subscribers |
263 (b.data as UserView).comment_score) -
264 ((a.data as Comment | Post).score |
265 (a.data as Community).number_of_subscribers |
266 (a.data as UserView).comment_score)
275 {i.type_ == 'posts' && (
277 post={i.data as Post}
279 enableDownvotes={this.state.site.enable_downvotes}
280 enableNsfw={this.state.site.enable_nsfw}
283 {i.type_ == 'comments' && (
285 nodes={[{ comment: i.data as Comment }]}
288 enableDownvotes={this.state.site.enable_downvotes}
291 {i.type_ == 'communities' && (
292 <div>{this.communityListing(i.data as Community)}</div>
294 {i.type_ == 'users' && (
300 name: (i.data as UserView).name,
301 avatar: (i.data as UserView).avatar,
305 <span>{` - ${i18n.t('number_of_comments', {
306 count: (i.data as UserView).number_of_comments,
320 nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
323 enableDownvotes={this.state.site.enable_downvotes}
331 {this.state.searchResponse.posts.map(post => (
337 enableDownvotes={this.state.site.enable_downvotes}
338 enableNsfw={this.state.site.enable_nsfw}
347 // Todo possibly create UserListing and CommunityListing
351 {this.state.searchResponse.communities.map(community => (
353 <div class="col-12">{this.communityListing(community)}</div>
360 communityListing(community: Community) {
364 <CommunityLink community={community} />
366 <span>{` - ${community.title} -
367 ${i18n.t('number_of_subscribers', {
368 count: community.number_of_subscribers,
378 {this.state.searchResponse.users.map(user => (
390 <span>{` - ${i18n.t('number_of_comments', {
391 count: user.number_of_comments,
403 {this.state.page > 1 && (
405 class="btn btn-sm btn-secondary mr-1"
406 onClick={linkEvent(this, this.prevPage)}
412 {this.resultsCount() > 0 && (
414 class="btn btn-sm btn-secondary"
415 onClick={linkEvent(this, this.nextPage)}
424 resultsCount(): number {
425 let res = this.state.searchResponse;
428 res.comments.length +
429 res.communities.length +
434 nextPage(i: Search) {
435 i.updateUrl({ page: i.state.page + 1 });
438 prevPage(i: Search) {
439 i.updateUrl({ page: i.state.page - 1 });
443 let form: SearchForm = {
445 type_: SearchType[this.state.type_],
446 sort: SortType[this.state.sort],
447 page: this.state.page,
451 if (this.state.q != '') {
452 WebSocketService.Instance.search(form);
456 handleSortChange(val: SortType) {
457 this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
460 handleTypeChange(i: Search, event: any) {
462 type_: SearchType[Number(event.target.value)].toLowerCase(),
467 handleSearchSubmit(i: Search, event: any) {
468 event.preventDefault();
470 q: i.state.searchText,
471 type_: SearchType[i.state.type_].toLowerCase(),
472 sort: SortType[i.state.sort].toLowerCase(),
477 handleQChange(i: Search, event: any) {
478 i.setState({ searchText: event.target.value });
481 updateUrl(paramUpdates: UrlParams) {
482 const qStr = paramUpdates.q || this.state.q;
484 paramUpdates.type_ || SearchType[this.state.type_].toLowerCase();
486 paramUpdates.sort || SortType[this.state.sort].toLowerCase();
487 const page = paramUpdates.page || this.state.page;
488 this.props.history.push(
489 `/search/q/${qStr}/type/${typeStr}/sort/${sortStr}/page/${page}`
493 parseMessage(msg: WebSocketJsonResponse) {
495 let res = wsJsonToRes(msg);
497 toast(i18n.t(msg.error), 'danger');
499 } else if (res.op == UserOperation.Search) {
500 let data = res.data as SearchResponse;
501 this.state.searchResponse = data;
502 this.state.loading = false;
503 document.title = `${i18n.t('search')} - ${this.state.q} - ${
506 window.scrollTo(0, 0);
507 this.setState(this.state);
508 } else if (res.op == UserOperation.CreateCommentLike) {
509 let data = res.data as CommentResponse;
510 createCommentLikeRes(data, this.state.searchResponse.comments);
511 this.setState(this.state);
512 } else if (res.op == UserOperation.CreatePostLike) {
513 let data = res.data as PostResponse;
514 createPostLikeFindRes(data, this.state.searchResponse.posts);
515 this.setState(this.state);
516 } else if (res.op == UserOperation.GetSite) {
517 let data = res.data as GetSiteResponse;
518 this.state.site = data.site;
519 this.setState(this.state);
520 document.title = `${i18n.t('search')} - ${data.site.name}`;