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 createPostLikeFindRes,
32 import { PostListing } from './post-listing';
33 import { SortSelect } from './sort-select';
34 import { CommentNodes } from './comment-nodes';
35 import { i18n } from '../i18next';
37 interface SearchState {
42 searchResponse: SearchResponse;
46 export class Search extends Component<any, SearchState> {
47 private subscription: Subscription;
48 private emptyState: SearchState = {
49 q: this.getSearchQueryFromProps(this.props),
50 type_: this.getSearchTypeFromProps(this.props),
51 sort: this.getSortTypeFromProps(this.props),
52 page: this.getPageFromProps(this.props),
63 getSearchQueryFromProps(props: any): string {
64 return props.match.params.q ? props.match.params.q : '';
67 getSearchTypeFromProps(props: any): SearchType {
68 return props.match.params.type
69 ? routeSearchTypeToEnum(props.match.params.type)
73 getSortTypeFromProps(props: any): SortType {
74 return props.match.params.sort
75 ? routeSortTypeToEnum(props.match.params.sort)
79 getPageFromProps(props: any): number {
80 return props.match.params.page ? Number(props.match.params.page) : 1;
83 constructor(props: any, context: any) {
84 super(props, context);
86 this.state = this.emptyState;
87 this.handleSortChange = this.handleSortChange.bind(this);
89 this.subscription = WebSocketService.Instance.subject
90 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
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">
131 <h5>{i18n.t('search')}</h5>
134 {this.state.type_ == SearchType.All && this.all()}
135 {this.state.type_ == SearchType.Comments && this.comments()}
136 {this.state.type_ == SearchType.Posts && this.posts()}
137 {this.state.type_ == SearchType.Communities && this.communities()}
138 {this.state.type_ == SearchType.Users && this.users()}
149 onSubmit={linkEvent(this, this.handleSearchSubmit)}
153 class="form-control mr-2"
155 placeholder={`${i18n.t('search')}...`}
156 onInput={linkEvent(this, this.handleQChange)}
160 <button type="submit" class="btn btn-secondary mr-2">
161 {this.state.loading ? (
162 <svg class="icon icon-spinner spin">
163 <use xlinkHref="#icon-spinner"></use>
166 <span>{i18n.t('search')}</span>
175 <div className="mb-2">
177 value={this.state.type_}
178 onChange={linkEvent(this, this.handleTypeChange)}
179 class="custom-select custom-select-sm w-auto"
181 <option disabled>{i18n.t('type')}</option>
182 <option value={SearchType.All}>{i18n.t('all')}</option>
183 <option value={SearchType.Comments}>{i18n.t('comments')}</option>
184 <option value={SearchType.Posts}>{i18n.t('posts')}</option>
185 <option value={SearchType.Communities}>
186 {i18n.t('communities')}
188 <option value={SearchType.Users}>{i18n.t('users')}</option>
192 sort={this.state.sort}
193 onChange={this.handleSortChange}
202 let combined: Array<{
204 data: Comment | Post | Community | UserView;
206 let comments = this.state.searchResponse.comments.map(e => {
207 return { type_: 'comments', data: e };
209 let posts = this.state.searchResponse.posts.map(e => {
210 return { type_: 'posts', data: e };
212 let communities = this.state.searchResponse.communities.map(e => {
213 return { type_: 'communities', data: e };
215 let users = this.state.searchResponse.users.map(e => {
216 return { type_: 'users', data: e };
219 combined.push(...comments);
220 combined.push(...posts);
221 combined.push(...communities);
222 combined.push(...users);
225 if (this.state.sort == SortType.New) {
226 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
230 ((b.data as Comment | Post).score |
231 (b.data as Community).number_of_subscribers |
232 (b.data as UserView).comment_score) -
233 ((a.data as Comment | Post).score |
234 (a.data as Community).number_of_subscribers |
235 (a.data as UserView).comment_score)
244 {i.type_ == 'posts' && (
245 <PostListing post={i.data as Post} showCommunity />
247 {i.type_ == 'comments' && (
249 nodes={[{ comment: i.data as Comment }]}
254 {i.type_ == 'communities' && (
257 <Link to={`/c/${(i.data as Community).name}`}>{`/c/${
258 (i.data as Community).name
261 <span>{` - ${(i.data as Community).title} - ${
262 (i.data as Community).number_of_subscribers
263 } subscribers`}</span>
266 {i.type_ == 'users' && (
270 className="text-info"
271 to={`/u/${(i.data as UserView).name}`}
273 {(i.data as UserView).avatar && showAvatars() && (
277 src={pictshareAvatarThumbnail(
278 (i.data as UserView).avatar
280 class="rounded-circle mr-1"
283 <span>{`/u/${(i.data as UserView).name}`}</span>
287 (i.data as UserView).comment_score
288 } comment karma`}</span>
301 nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
311 {this.state.searchResponse.posts.map(post => (
314 <PostListing post={post} showCommunity />
322 // Todo possibly create UserListing and CommunityListing
326 {this.state.searchResponse.communities.map(community => (
331 to={`/c/${community.name}`}
332 >{`/c/${community.name}`}</Link>
334 <span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
345 {this.state.searchResponse.users.map(user => (
350 className="text-info"
351 to={`/u/${user.name}`}
352 >{`/u/${user.name}`}</Link>
354 <span>{` - ${user.comment_score} comment karma`}</span>
365 {this.state.page > 1 && (
367 class="btn btn-sm btn-secondary mr-1"
368 onClick={linkEvent(this, this.prevPage)}
374 class="btn btn-sm btn-secondary"
375 onClick={linkEvent(this, this.nextPage)}
384 let res = this.state.searchResponse;
388 res.posts.length == 0 &&
389 res.comments.length == 0 &&
390 res.communities.length == 0 &&
391 res.users.length == 0 && <span>{i18n.t('no_results')}</span>}
396 nextPage(i: Search) {
403 prevPage(i: Search) {
411 let form: SearchForm = {
413 type_: SearchType[this.state.type_],
414 sort: SortType[this.state.sort],
415 page: this.state.page,
419 if (this.state.q != '') {
420 WebSocketService.Instance.search(form);
424 handleSortChange(val: SortType) {
425 this.state.sort = val;
427 this.setState(this.state);
431 handleTypeChange(i: Search, event: any) {
432 i.state.type_ = Number(event.target.value);
438 handleSearchSubmit(i: Search, event: any) {
439 event.preventDefault();
440 i.state.loading = true;
446 handleQChange(i: Search, event: any) {
447 i.state.q = event.target.value;
452 let typeStr = SearchType[this.state.type_].toLowerCase();
453 let sortStr = SortType[this.state.sort].toLowerCase();
454 this.props.history.push(
455 `/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
459 parseMessage(msg: WebSocketJsonResponse) {
461 let res = wsJsonToRes(msg);
463 toast(i18n.t(msg.error), 'danger');
465 } else if (res.op == UserOperation.Search) {
466 let data = res.data as SearchResponse;
467 this.state.searchResponse = data;
468 this.state.loading = false;
469 document.title = `${i18n.t('search')} - ${this.state.q} - ${
470 WebSocketService.Instance.site.name
472 window.scrollTo(0, 0);
473 this.setState(this.state);
474 } else if (res.op == UserOperation.CreateCommentLike) {
475 let data = res.data as CommentResponse;
476 createCommentLikeRes(data, this.state.searchResponse.comments);
477 this.setState(this.state);
478 } else if (res.op == UserOperation.CreatePostLike) {
479 let data = res.data as PostResponse;
480 createPostLikeFindRes(data, this.state.searchResponse.posts);
481 this.setState(this.state);