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,
18 } from '../interfaces';
19 import { WebSocketService } from '../services';
23 routeSearchTypeToEnum,
25 pictshareAvatarThumbnail,
29 import { PostListing } from './post-listing';
30 import { SortSelect } from './sort-select';
31 import { CommentNodes } from './comment-nodes';
32 import { i18n } from '../i18next';
34 interface SearchState {
39 searchResponse: SearchResponse;
43 export class Search extends Component<any, SearchState> {
44 private subscription: Subscription;
45 private emptyState: SearchState = {
46 q: this.getSearchQueryFromProps(this.props),
47 type_: this.getSearchTypeFromProps(this.props),
48 sort: this.getSortTypeFromProps(this.props),
49 page: this.getPageFromProps(this.props),
60 getSearchQueryFromProps(props: any): string {
61 return props.match.params.q ? props.match.params.q : '';
64 getSearchTypeFromProps(props: any): SearchType {
65 return props.match.params.type
66 ? routeSearchTypeToEnum(props.match.params.type)
70 getSortTypeFromProps(props: any): SortType {
71 return props.match.params.sort
72 ? routeSortTypeToEnum(props.match.params.sort)
76 getPageFromProps(props: any): number {
77 return props.match.params.page ? Number(props.match.params.page) : 1;
80 constructor(props: any, context: any) {
81 super(props, context);
83 this.state = this.emptyState;
84 this.handleSortChange = this.handleSortChange.bind(this);
86 this.subscription = WebSocketService.Instance.subject
87 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
89 msg => this.parseMessage(msg),
90 err => console.error(err),
91 () => console.log('complete')
99 componentWillUnmount() {
100 this.subscription.unsubscribe();
103 // Necessary for back button for some reason
104 componentWillReceiveProps(nextProps: any) {
106 nextProps.history.action == 'POP' ||
107 nextProps.history.action == 'PUSH'
109 this.state = this.emptyState;
110 this.state.q = this.getSearchQueryFromProps(nextProps);
111 this.state.type_ = this.getSearchTypeFromProps(nextProps);
112 this.state.sort = this.getSortTypeFromProps(nextProps);
113 this.state.page = this.getPageFromProps(nextProps);
114 this.setState(this.state);
119 componentDidMount() {
120 document.title = `${i18n.t('search')} - ${
121 WebSocketService.Instance.site.name
127 <div class="container">
128 <h5>{i18n.t('search')}</h5>
131 {this.state.type_ == SearchType.All && this.all()}
132 {this.state.type_ == SearchType.Comments && this.comments()}
133 {this.state.type_ == SearchType.Posts && this.posts()}
134 {this.state.type_ == SearchType.Communities && this.communities()}
135 {this.state.type_ == SearchType.Users && this.users()}
146 onSubmit={linkEvent(this, this.handleSearchSubmit)}
150 class="form-control mr-2"
152 placeholder={`${i18n.t('search')}...`}
153 onInput={linkEvent(this, this.handleQChange)}
157 <button type="submit" class="btn btn-secondary mr-2">
158 {this.state.loading ? (
159 <svg class="icon icon-spinner spin">
160 <use xlinkHref="#icon-spinner"></use>
163 <span>{i18n.t('search')}</span>
172 <div className="mb-2">
174 value={this.state.type_}
175 onChange={linkEvent(this, this.handleTypeChange)}
176 class="custom-select custom-select-sm w-auto"
178 <option disabled>{i18n.t('type')}</option>
179 <option value={SearchType.All}>{i18n.t('all')}</option>
180 <option value={SearchType.Comments}>{i18n.t('comments')}</option>
181 <option value={SearchType.Posts}>{i18n.t('posts')}</option>
182 <option value={SearchType.Communities}>
183 {i18n.t('communities')}
185 <option value={SearchType.Users}>{i18n.t('users')}</option>
189 sort={this.state.sort}
190 onChange={this.handleSortChange}
199 let combined: Array<{
201 data: Comment | Post | Community | UserView;
203 let comments = this.state.searchResponse.comments.map(e => {
204 return { type_: 'comments', data: e };
206 let posts = this.state.searchResponse.posts.map(e => {
207 return { type_: 'posts', data: e };
209 let communities = this.state.searchResponse.communities.map(e => {
210 return { type_: 'communities', data: e };
212 let users = this.state.searchResponse.users.map(e => {
213 return { type_: 'users', data: e };
216 combined.push(...comments);
217 combined.push(...posts);
218 combined.push(...communities);
219 combined.push(...users);
222 if (this.state.sort == SortType.New) {
223 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
227 ((b.data as Comment | Post).score |
228 (b.data as Community).number_of_subscribers |
229 (b.data as UserView).comment_score) -
230 ((a.data as Comment | Post).score |
231 (a.data as Community).number_of_subscribers |
232 (a.data as UserView).comment_score)
241 {i.type_ == 'posts' && (
242 <PostListing post={i.data as Post} showCommunity />
244 {i.type_ == 'comments' && (
246 nodes={[{ comment: i.data as Comment }]}
251 {i.type_ == 'communities' && (
254 <Link to={`/c/${(i.data as Community).name}`}>{`/c/${
255 (i.data as Community).name
258 <span>{` - ${(i.data as Community).title} - ${
259 (i.data as Community).number_of_subscribers
260 } subscribers`}</span>
263 {i.type_ == 'users' && (
267 className="text-info"
268 to={`/u/${(i.data as UserView).name}`}
270 {(i.data as UserView).avatar && showAvatars() && (
274 src={pictshareAvatarThumbnail(
275 (i.data as UserView).avatar
277 class="rounded-circle mr-1"
280 <span>{`/u/${(i.data as UserView).name}`}</span>
284 (i.data as UserView).comment_score
285 } comment karma`}</span>
298 {this.state.searchResponse.comments.map(comment => (
301 <CommentNodes nodes={[{ comment: comment }]} locked noIndent />
312 {this.state.searchResponse.posts.map(post => (
315 <PostListing post={post} showCommunity />
323 // Todo possibly create UserListing and CommunityListing
327 {this.state.searchResponse.communities.map(community => (
332 to={`/c/${community.name}`}
333 >{`/c/${community.name}`}</Link>
335 <span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
346 {this.state.searchResponse.users.map(user => (
351 className="text-info"
352 to={`/u/${user.name}`}
353 >{`/u/${user.name}`}</Link>
355 <span>{` - ${user.comment_score} comment karma`}</span>
366 {this.state.page > 1 && (
368 class="btn btn-sm btn-secondary mr-1"
369 onClick={linkEvent(this, this.prevPage)}
375 class="btn btn-sm btn-secondary"
376 onClick={linkEvent(this, this.nextPage)}
385 let res = this.state.searchResponse;
389 res.posts.length == 0 &&
390 res.comments.length == 0 &&
391 res.communities.length == 0 &&
392 res.users.length == 0 && <span>{i18n.t('no_results')}</span>}
397 nextPage(i: Search) {
404 prevPage(i: Search) {
412 let form: SearchForm = {
414 type_: SearchType[this.state.type_],
415 sort: SortType[this.state.sort],
416 page: this.state.page,
420 if (this.state.q != '') {
421 WebSocketService.Instance.search(form);
425 handleSortChange(val: SortType) {
426 this.state.sort = val;
428 this.setState(this.state);
432 handleTypeChange(i: Search, event: any) {
433 i.state.type_ = Number(event.target.value);
439 handleSearchSubmit(i: Search, event: any) {
440 event.preventDefault();
441 i.state.loading = true;
447 handleQChange(i: Search, event: any) {
448 i.state.q = event.target.value;
453 let typeStr = SearchType[this.state.type_].toLowerCase();
454 let sortStr = SortType[this.state.sort].toLowerCase();
455 this.props.history.push(
456 `/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
460 parseMessage(msg: WebSocketJsonResponse) {
462 let res = wsJsonToRes(msg);
464 toast(i18n.t(msg.error), 'danger');
466 } else if (res.op == UserOperation.Search) {
467 let data = res.data as SearchResponse;
468 this.state.searchResponse = data;
469 this.state.loading = false;
470 document.title = `${i18n.t('search')} - ${this.state.q} - ${
471 WebSocketService.Instance.site.name
473 window.scrollTo(0, 0);
474 this.setState(this.state);
475 } else if (res.op == UserOperation.CreateCommentLike) {
476 let data = res.data as CommentResponse;
477 let found: Comment = this.state.searchResponse.comments.find(
478 c => c.id === data.comment.id
480 found.score = data.comment.score;
481 found.upvotes = data.comment.upvotes;
482 found.downvotes = data.comment.downvotes;
483 if (data.comment.my_vote !== null) {
484 found.my_vote = data.comment.my_vote;
485 found.upvoteLoading = false;
486 found.downvoteLoading = false;
488 this.setState(this.state);
489 } else if (res.op == UserOperation.CreatePostLike) {
490 let data = res.data as PostResponse;
491 let found = this.state.searchResponse.posts.find(
492 c => c.id == data.post.id
494 found.my_vote = data.post.my_vote;
495 found.score = data.post.score;
496 found.upvotes = data.post.upvotes;
497 found.downvotes = data.post.downvotes;
498 this.setState(this.state);