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 WebSocketJsonResponse,
16 } from '../interfaces';
17 import { WebSocketService } from '../services';
21 routeSearchTypeToEnum,
23 pictshareAvatarThumbnail,
26 import { PostListing } from './post-listing';
27 import { SortSelect } from './sort-select';
28 import { CommentNodes } from './comment-nodes';
29 import { i18n } from '../i18next';
30 import { T } from 'inferno-i18next';
32 interface SearchState {
37 searchResponse: SearchResponse;
41 export class Search extends Component<any, SearchState> {
42 private subscription: Subscription;
43 private emptyState: SearchState = {
44 q: this.getSearchQueryFromProps(this.props),
45 type_: this.getSearchTypeFromProps(this.props),
46 sort: this.getSortTypeFromProps(this.props),
47 page: this.getPageFromProps(this.props),
58 getSearchQueryFromProps(props: any): string {
59 return props.match.params.q ? props.match.params.q : '';
62 getSearchTypeFromProps(props: any): SearchType {
63 return props.match.params.type
64 ? routeSearchTypeToEnum(props.match.params.type)
68 getSortTypeFromProps(props: any): SortType {
69 return props.match.params.sort
70 ? routeSortTypeToEnum(props.match.params.sort)
74 getPageFromProps(props: any): number {
75 return props.match.params.page ? Number(props.match.params.page) : 1;
78 constructor(props: any, context: any) {
79 super(props, context);
81 this.state = this.emptyState;
82 this.handleSortChange = this.handleSortChange.bind(this);
84 this.subscription = WebSocketService.Instance.subject
85 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
87 msg => this.parseMessage(msg),
88 err => console.error(err),
89 () => console.log('complete')
97 componentWillUnmount() {
98 this.subscription.unsubscribe();
101 // Necessary for back button for some reason
102 componentWillReceiveProps(nextProps: any) {
104 nextProps.history.action == 'POP' ||
105 nextProps.history.action == 'PUSH'
107 this.state = this.emptyState;
108 this.state.q = this.getSearchQueryFromProps(nextProps);
109 this.state.type_ = this.getSearchTypeFromProps(nextProps);
110 this.state.sort = this.getSortTypeFromProps(nextProps);
111 this.state.page = this.getPageFromProps(nextProps);
112 this.setState(this.state);
117 componentDidMount() {
118 document.title = `${i18n.t('search')} - ${
119 WebSocketService.Instance.site.name
125 <div class="container">
129 <T i18nKey="search">#</T>
133 {this.state.type_ == SearchType.All && this.all()}
134 {this.state.type_ == SearchType.Comments && this.comments()}
135 {this.state.type_ == SearchType.Posts && this.posts()}
136 {this.state.type_ == SearchType.Communities && this.communities()}
137 {this.state.type_ == SearchType.Users && this.users()}
150 onSubmit={linkEvent(this, this.handleSearchSubmit)}
154 class="form-control mr-2"
156 placeholder={`${i18n.t('search')}...`}
157 onInput={linkEvent(this, this.handleQChange)}
161 <button type="submit" class="btn btn-secondary mr-2">
162 {this.state.loading ? (
163 <svg class="icon icon-spinner spin">
164 <use xlinkHref="#icon-spinner"></use>
168 <T i18nKey="search">#</T>
178 <div className="mb-2">
180 value={this.state.type_}
181 onChange={linkEvent(this, this.handleTypeChange)}
182 class="custom-select custom-select-sm w-auto"
185 <T i18nKey="type">#</T>
187 <option value={SearchType.All}>
188 <T i18nKey="all">#</T>
190 <option value={SearchType.Comments}>
191 <T i18nKey="comments">#</T>
193 <option value={SearchType.Posts}>
194 <T i18nKey="posts">#</T>
196 <option value={SearchType.Communities}>
197 <T i18nKey="communities">#</T>
199 <option value={SearchType.Users}>
200 <T i18nKey="users">#</T>
205 sort={this.state.sort}
206 onChange={this.handleSortChange}
215 let combined: Array<{
217 data: Comment | Post | Community | UserView;
219 let comments = this.state.searchResponse.comments.map(e => {
220 return { type_: 'comments', data: e };
222 let posts = this.state.searchResponse.posts.map(e => {
223 return { type_: 'posts', data: e };
225 let communities = this.state.searchResponse.communities.map(e => {
226 return { type_: 'communities', data: e };
228 let users = this.state.searchResponse.users.map(e => {
229 return { type_: 'users', data: e };
232 combined.push(...comments);
233 combined.push(...posts);
234 combined.push(...communities);
235 combined.push(...users);
238 if (this.state.sort == SortType.New) {
239 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
243 ((b.data as Comment | Post).score |
244 (b.data as Community).number_of_subscribers |
245 (b.data as UserView).comment_score) -
246 ((a.data as Comment | Post).score |
247 (a.data as Community).number_of_subscribers |
248 (a.data as UserView).comment_score)
256 {i.type_ == 'posts' && (
257 <PostListing post={i.data as Post} showCommunity viewOnly />
259 {i.type_ == 'comments' && (
261 nodes={[{ comment: i.data as Comment }]}
266 {i.type_ == 'communities' && (
269 <Link to={`/c/${(i.data as Community).name}`}>{`/c/${
270 (i.data as Community).name
273 <span>{` - ${(i.data as Community).title} - ${
274 (i.data as Community).number_of_subscribers
275 } subscribers`}</span>
278 {i.type_ == 'users' && (
282 className="text-info"
283 to={`/u/${(i.data as UserView).name}`}
285 {(i.data as UserView).avatar && showAvatars() && (
289 src={pictshareAvatarThumbnail(
290 (i.data as UserView).avatar
292 class="rounded-circle mr-1"
295 <span>{`/u/${(i.data as UserView).name}`}</span>
299 (i.data as UserView).comment_score
300 } comment karma`}</span>
312 {this.state.searchResponse.comments.map(comment => (
313 <CommentNodes nodes={[{ comment: comment }]} locked noIndent />
322 {this.state.searchResponse.posts.map(post => (
323 <PostListing post={post} showCommunity viewOnly />
329 // Todo possibly create UserListing and CommunityListing
333 {this.state.searchResponse.communities.map(community => (
336 <Link to={`/c/${community.name}`}>{`/c/${community.name}`}</Link>
338 <span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
348 {this.state.searchResponse.users.map(user => (
352 className="text-info"
353 to={`/u/${user.name}`}
354 >{`/u/${user.name}`}</Link>
356 <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)}
371 <T i18nKey="prev">#</T>
375 class="btn btn-sm btn-secondary"
376 onClick={linkEvent(this, this.nextPage)}
378 <T i18nKey="next">#</T>
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 && (
394 <T i18nKey="no_results">#</T>
401 nextPage(i: Search) {
408 prevPage(i: Search) {
416 let form: SearchForm = {
418 type_: SearchType[this.state.type_],
419 sort: SortType[this.state.sort],
420 page: this.state.page,
424 if (this.state.q != '') {
425 WebSocketService.Instance.search(form);
429 handleSortChange(val: SortType) {
430 this.state.sort = val;
432 this.setState(this.state);
436 handleTypeChange(i: Search, event: any) {
437 i.state.type_ = Number(event.target.value);
443 handleSearchSubmit(i: Search, event: any) {
444 event.preventDefault();
445 i.state.loading = true;
451 handleQChange(i: Search, event: any) {
452 i.state.q = event.target.value;
457 let typeStr = SearchType[this.state.type_].toLowerCase();
458 let sortStr = SortType[this.state.sort].toLowerCase();
459 this.props.history.push(
460 `/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
464 parseMessage(msg: WebSocketJsonResponse) {
466 let res = wsJsonToRes(msg);
468 alert(i18n.t(res.error));
470 } else if (res.op == UserOperation.Search) {
471 let data = res.data as SearchResponse;
472 this.state.searchResponse = data;
473 this.state.loading = false;
474 document.title = `${i18n.t('search')} - ${this.state.q} - ${
475 WebSocketService.Instance.site.name
477 window.scrollTo(0, 0);
478 this.setState(this.state);