1 import { Component, linkEvent } from 'inferno';
2 import { Helmet } from 'inferno-helmet';
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 });
159 get documentTitle(): string {
160 if (this.state.site.name) {
162 return `${i18n.t('search')} - ${this.state.q} - ${
166 return `${i18n.t('search')} - ${this.state.site.name}`;
175 <div class="container">
176 <Helmet title={this.documentTitle} />
177 <h5>{i18n.t('search')}</h5>
180 {this.state.type_ == SearchType.All && this.all()}
181 {this.state.type_ == SearchType.Comments && this.comments()}
182 {this.state.type_ == SearchType.Posts && this.posts()}
183 {this.state.type_ == SearchType.Communities && this.communities()}
184 {this.state.type_ == SearchType.Users && this.users()}
185 {this.resultsCount() == 0 && <span>{i18n.t('no_results')}</span>}
195 onSubmit={linkEvent(this, this.handleSearchSubmit)}
199 class="form-control mr-2"
200 value={this.state.searchText}
201 placeholder={`${i18n.t('search')}...`}
202 onInput={linkEvent(this, this.handleQChange)}
206 <button type="submit" class="btn btn-secondary mr-2">
207 {this.state.loading ? (
208 <svg class="icon icon-spinner spin">
209 <use xlinkHref="#icon-spinner"></use>
212 <span>{i18n.t('search')}</span>
221 <div className="mb-2">
223 value={this.state.type_}
224 onChange={linkEvent(this, this.handleTypeChange)}
225 class="custom-select w-auto"
227 <option disabled>{i18n.t('type')}</option>
228 <option value={SearchType.All}>{i18n.t('all')}</option>
229 <option value={SearchType.Comments}>{i18n.t('comments')}</option>
230 <option value={SearchType.Posts}>{i18n.t('posts')}</option>
231 <option value={SearchType.Communities}>
232 {i18n.t('communities')}
234 <option value={SearchType.Users}>{i18n.t('users')}</option>
238 sort={this.state.sort}
239 onChange={this.handleSortChange}
248 let combined: Array<{
250 data: Comment | Post | Community | UserView;
252 let comments = this.state.searchResponse.comments.map(e => {
253 return { type_: 'comments', data: e };
255 let posts = this.state.searchResponse.posts.map(e => {
256 return { type_: 'posts', data: e };
258 let communities = this.state.searchResponse.communities.map(e => {
259 return { type_: 'communities', data: e };
261 let users = this.state.searchResponse.users.map(e => {
262 return { type_: 'users', data: e };
265 combined.push(...comments);
266 combined.push(...posts);
267 combined.push(...communities);
268 combined.push(...users);
271 if (this.state.sort == SortType.New) {
272 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
276 ((b.data as Comment | Post).score |
277 (b.data as Community).number_of_subscribers |
278 (b.data as UserView).comment_score) -
279 ((a.data as Comment | Post).score |
280 (a.data as Community).number_of_subscribers |
281 (a.data as UserView).comment_score)
290 {i.type_ == 'posts' && (
292 post={i.data as Post}
294 enableDownvotes={this.state.site.enable_downvotes}
295 enableNsfw={this.state.site.enable_nsfw}
298 {i.type_ == 'comments' && (
300 nodes={[{ comment: i.data as Comment }]}
303 enableDownvotes={this.state.site.enable_downvotes}
306 {i.type_ == 'communities' && (
307 <div>{this.communityListing(i.data as Community)}</div>
309 {i.type_ == 'users' && (
315 name: (i.data as UserView).name,
316 avatar: (i.data as UserView).avatar,
320 <span>{` - ${i18n.t('number_of_comments', {
321 count: (i.data as UserView).number_of_comments,
335 nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
338 enableDownvotes={this.state.site.enable_downvotes}
346 {this.state.searchResponse.posts.map(post => (
352 enableDownvotes={this.state.site.enable_downvotes}
353 enableNsfw={this.state.site.enable_nsfw}
362 // Todo possibly create UserListing and CommunityListing
366 {this.state.searchResponse.communities.map(community => (
368 <div class="col-12">{this.communityListing(community)}</div>
375 communityListing(community: Community) {
379 <CommunityLink community={community} />
381 <span>{` - ${community.title} -
382 ${i18n.t('number_of_subscribers', {
383 count: community.number_of_subscribers,
393 {this.state.searchResponse.users.map(user => (
405 <span>{` - ${i18n.t('number_of_comments', {
406 count: user.number_of_comments,
418 {this.state.page > 1 && (
420 class="btn btn-secondary mr-1"
421 onClick={linkEvent(this, this.prevPage)}
427 {this.resultsCount() > 0 && (
429 class="btn btn-secondary"
430 onClick={linkEvent(this, this.nextPage)}
439 resultsCount(): number {
440 let res = this.state.searchResponse;
443 res.comments.length +
444 res.communities.length +
449 nextPage(i: Search) {
450 i.updateUrl({ page: i.state.page + 1 });
453 prevPage(i: Search) {
454 i.updateUrl({ page: i.state.page - 1 });
458 let form: SearchForm = {
460 type_: SearchType[this.state.type_],
461 sort: SortType[this.state.sort],
462 page: this.state.page,
466 if (this.state.q != '') {
467 WebSocketService.Instance.search(form);
471 handleSortChange(val: SortType) {
472 this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
475 handleTypeChange(i: Search, event: any) {
477 type_: SearchType[Number(event.target.value)].toLowerCase(),
482 handleSearchSubmit(i: Search, event: any) {
483 event.preventDefault();
485 q: i.state.searchText,
486 type_: SearchType[i.state.type_].toLowerCase(),
487 sort: SortType[i.state.sort].toLowerCase(),
492 handleQChange(i: Search, event: any) {
493 i.setState({ searchText: event.target.value });
496 updateUrl(paramUpdates: UrlParams) {
497 const qStr = paramUpdates.q || this.state.q;
499 paramUpdates.type_ || SearchType[this.state.type_].toLowerCase();
501 paramUpdates.sort || SortType[this.state.sort].toLowerCase();
502 const page = paramUpdates.page || this.state.page;
503 this.props.history.push(
504 `/search/q/${qStr}/type/${typeStr}/sort/${sortStr}/page/${page}`
508 parseMessage(msg: WebSocketJsonResponse) {
510 let res = wsJsonToRes(msg);
512 toast(i18n.t(msg.error), 'danger');
514 } else if (res.op == UserOperation.Search) {
515 let data = res.data as SearchResponse;
516 this.state.searchResponse = data;
517 this.state.loading = false;
518 window.scrollTo(0, 0);
519 this.setState(this.state);
520 } else if (res.op == UserOperation.CreateCommentLike) {
521 let data = res.data as CommentResponse;
522 createCommentLikeRes(data, this.state.searchResponse.comments);
523 this.setState(this.state);
524 } else if (res.op == UserOperation.CreatePostLike) {
525 let data = res.data as PostResponse;
526 createPostLikeFindRes(data, this.state.searchResponse.posts);
527 this.setState(this.state);
528 } else if (res.op == UserOperation.GetSite) {
529 let data = res.data as GetSiteResponse;
530 this.state.site = data.site;
531 this.setState(this.state);