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';
15 } from '../interfaces';
16 import { WebSocketService } from '../services';
20 routeSearchTypeToEnum,
23 import { PostListing } from './post-listing';
24 import { SortSelect } from './sort-select';
25 import { CommentNodes } from './comment-nodes';
26 import { i18n } from '../i18next';
27 import { T } from 'inferno-i18next';
29 interface SearchState {
34 searchResponse: SearchResponse;
38 export class Search extends Component<any, SearchState> {
39 private subscription: Subscription;
40 private emptyState: SearchState = {
41 q: this.getSearchQueryFromProps(this.props),
42 type_: this.getSearchTypeFromProps(this.props),
43 sort: this.getSortTypeFromProps(this.props),
44 page: this.getPageFromProps(this.props),
56 getSearchQueryFromProps(props: any): string {
57 return props.match.params.q ? props.match.params.q : '';
60 getSearchTypeFromProps(props: any): SearchType {
61 return props.match.params.type
62 ? routeSearchTypeToEnum(props.match.params.type)
66 getSortTypeFromProps(props: any): SortType {
67 return props.match.params.sort
68 ? routeSortTypeToEnum(props.match.params.sort)
72 getPageFromProps(props: any): number {
73 return props.match.params.page ? Number(props.match.params.page) : 1;
76 constructor(props: any, context: any) {
77 super(props, context);
79 this.state = this.emptyState;
80 this.handleSortChange = this.handleSortChange.bind(this);
82 this.subscription = WebSocketService.Instance.subject
92 msg => this.parseMessage(msg),
93 err => console.error(err),
94 () => console.log('complete')
102 componentWillUnmount() {
103 this.subscription.unsubscribe();
106 // Necessary for back button for some reason
107 componentWillReceiveProps(nextProps: any) {
109 nextProps.history.action == 'POP' ||
110 nextProps.history.action == 'PUSH'
112 this.state = this.emptyState;
113 this.state.q = this.getSearchQueryFromProps(nextProps);
114 this.state.type_ = this.getSearchTypeFromProps(nextProps);
115 this.state.sort = this.getSortTypeFromProps(nextProps);
116 this.state.page = this.getPageFromProps(nextProps);
117 this.setState(this.state);
122 componentDidMount() {
123 document.title = `${i18n.t('search')} - ${
124 WebSocketService.Instance.site.name
130 <div class="container">
134 <T i18nKey="search">#</T>
138 {this.state.type_ == SearchType.All && this.all()}
139 {this.state.type_ == SearchType.Comments && this.comments()}
140 {this.state.type_ == SearchType.Posts && this.posts()}
141 {this.state.type_ == SearchType.Communities && this.communities()}
142 {this.state.type_ == SearchType.Users && this.users()}
155 onSubmit={linkEvent(this, this.handleSearchSubmit)}
159 class="form-control mr-2"
161 placeholder={`${i18n.t('search')}...`}
162 onInput={linkEvent(this, this.handleQChange)}
166 <button type="submit" class="btn btn-secondary mr-2">
167 {this.state.loading ? (
168 <svg class="icon icon-spinner spin">
169 <use xlinkHref="#icon-spinner"></use>
173 <T i18nKey="search">#</T>
183 <div className="mb-2">
185 value={this.state.type_}
186 onChange={linkEvent(this, this.handleTypeChange)}
187 class="custom-select custom-select-sm w-auto"
190 <T i18nKey="type">#</T>
192 <option value={SearchType.All}>
193 <T i18nKey="all">#</T>
195 <option value={SearchType.Comments}>
196 <T i18nKey="comments">#</T>
198 <option value={SearchType.Posts}>
199 <T i18nKey="posts">#</T>
201 <option value={SearchType.Communities}>
202 <T i18nKey="communities">#</T>
204 <option value={SearchType.Users}>
205 <T i18nKey="users">#</T>
210 sort={this.state.sort}
211 onChange={this.handleSortChange}
220 let combined: Array<{
222 data: Comment | Post | Community | UserView;
224 let comments = this.state.searchResponse.comments.map(e => {
225 return { type_: 'comments', data: e };
227 let posts = this.state.searchResponse.posts.map(e => {
228 return { type_: 'posts', data: e };
230 let communities = this.state.searchResponse.communities.map(e => {
231 return { type_: 'communities', data: e };
233 let users = this.state.searchResponse.users.map(e => {
234 return { type_: 'users', data: e };
237 combined.push(...comments);
238 combined.push(...posts);
239 combined.push(...communities);
240 combined.push(...users);
243 if (this.state.sort == SortType.New) {
244 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
248 ((b.data as Comment | Post).score |
249 (b.data as Community).number_of_subscribers |
250 (b.data as UserView).comment_score) -
251 ((a.data as Comment | Post).score |
252 (a.data as Community).number_of_subscribers |
253 (a.data as UserView).comment_score)
261 {i.type_ == 'posts' && (
262 <PostListing post={i.data as Post} showCommunity viewOnly />
264 {i.type_ == 'comments' && (
266 nodes={[{ comment: i.data as Comment }]}
271 {i.type_ == 'communities' && (
274 <Link to={`/c/${(i.data as Community).name}`}>{`/c/${
275 (i.data as Community).name
278 <span>{` - ${(i.data as Community).title} - ${
279 (i.data as Community).number_of_subscribers
280 } subscribers`}</span>
283 {i.type_ == 'users' && (
287 className="text-info"
288 to={`/u/${(i.data as UserView).name}`}
289 >{`/u/${(i.data as UserView).name}`}</Link>
292 (i.data as UserView).comment_score
293 } comment karma`}</span>
305 {this.state.searchResponse.comments.map(comment => (
306 <CommentNodes nodes={[{ comment: comment }]} noIndent viewOnly />
315 {this.state.searchResponse.posts.map(post => (
316 <PostListing post={post} showCommunity viewOnly />
322 // Todo possibly create UserListing and CommunityListing
326 {this.state.searchResponse.communities.map(community => (
329 <Link to={`/c/${community.name}`}>{`/c/${community.name}`}</Link>
331 <span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
341 {this.state.searchResponse.users.map(user => (
345 className="text-info"
346 to={`/u/${user.name}`}
347 >{`/u/${user.name}`}</Link>
349 <span>{` - ${user.comment_score} comment karma`}</span>
359 {this.state.page > 1 && (
361 class="btn btn-sm btn-secondary mr-1"
362 onClick={linkEvent(this, this.prevPage)}
364 <T i18nKey="prev">#</T>
368 class="btn btn-sm btn-secondary"
369 onClick={linkEvent(this, this.nextPage)}
371 <T i18nKey="next">#</T>
378 let res = this.state.searchResponse;
383 res.posts.length == 0 &&
384 res.comments.length == 0 &&
385 res.communities.length == 0 &&
386 res.users.length == 0 && (
388 <T i18nKey="no_results">#</T>
395 nextPage(i: Search) {
402 prevPage(i: Search) {
410 let form: SearchForm = {
412 type_: SearchType[this.state.type_],
413 sort: SortType[this.state.sort],
414 page: this.state.page,
418 if (this.state.q != '') {
419 WebSocketService.Instance.search(form);
423 handleSortChange(val: SortType) {
424 this.state.sort = val;
426 this.setState(this.state);
430 handleTypeChange(i: Search, event: any) {
431 i.state.type_ = Number(event.target.value);
437 handleSearchSubmit(i: Search, event: any) {
438 event.preventDefault();
439 i.state.loading = true;
445 handleQChange(i: Search, event: any) {
446 i.state.q = event.target.value;
451 let typeStr = SearchType[this.state.type_].toLowerCase();
452 let sortStr = SortType[this.state.sort].toLowerCase();
453 this.props.history.push(
454 `/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
458 parseMessage(msg: any) {
460 let op: UserOperation = msgOp(msg);
462 alert(i18n.t(msg.error));
464 } else if (op == UserOperation.Search) {
465 let res: SearchResponse = msg;
466 this.state.searchResponse = res;
467 this.state.loading = false;
468 document.title = `${i18n.t('search')} - ${this.state.q} - ${
469 WebSocketService.Instance.site.name
471 window.scrollTo(0, 0);
472 this.setState(this.state);