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 CreatePostLikeResponse,
17 } from '../interfaces';
18 import { WebSocketService } from '../services';
22 routeSearchTypeToEnum,
24 pictshareAvatarThumbnail,
27 import { PostListing } from './post-listing';
28 import { SortSelect } from './sort-select';
29 import { CommentNodes } from './comment-nodes';
30 import { i18n } from '../i18next';
31 import { T } from 'inferno-i18next';
33 interface SearchState {
38 searchResponse: SearchResponse;
42 export class Search extends Component<any, SearchState> {
43 private subscription: Subscription;
44 private emptyState: SearchState = {
45 q: this.getSearchQueryFromProps(this.props),
46 type_: this.getSearchTypeFromProps(this.props),
47 sort: this.getSortTypeFromProps(this.props),
48 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">
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()}
148 onSubmit={linkEvent(this, this.handleSearchSubmit)}
152 class="form-control mr-2"
154 placeholder={`${i18n.t('search')}...`}
155 onInput={linkEvent(this, this.handleQChange)}
159 <button type="submit" class="btn btn-secondary mr-2">
160 {this.state.loading ? (
161 <svg class="icon icon-spinner spin">
162 <use xlinkHref="#icon-spinner"></use>
166 <T i18nKey="search">#</T>
176 <div className="mb-2">
178 value={this.state.type_}
179 onChange={linkEvent(this, this.handleTypeChange)}
180 class="custom-select custom-select-sm w-auto"
183 <T i18nKey="type">#</T>
185 <option value={SearchType.All}>
186 <T i18nKey="all">#</T>
188 <option value={SearchType.Comments}>
189 <T i18nKey="comments">#</T>
191 <option value={SearchType.Posts}>
192 <T i18nKey="posts">#</T>
194 <option value={SearchType.Communities}>
195 <T i18nKey="communities">#</T>
197 <option value={SearchType.Users}>
198 <T i18nKey="users">#</T>
203 sort={this.state.sort}
204 onChange={this.handleSortChange}
213 let combined: Array<{
215 data: Comment | Post | Community | UserView;
217 let comments = this.state.searchResponse.comments.map(e => {
218 return { type_: 'comments', data: e };
220 let posts = this.state.searchResponse.posts.map(e => {
221 return { type_: 'posts', data: e };
223 let communities = this.state.searchResponse.communities.map(e => {
224 return { type_: 'communities', data: e };
226 let users = this.state.searchResponse.users.map(e => {
227 return { type_: 'users', data: e };
230 combined.push(...comments);
231 combined.push(...posts);
232 combined.push(...communities);
233 combined.push(...users);
236 if (this.state.sort == SortType.New) {
237 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
241 ((b.data as Comment | Post).score |
242 (b.data as Community).number_of_subscribers |
243 (b.data as UserView).comment_score) -
244 ((a.data as Comment | Post).score |
245 (a.data as Community).number_of_subscribers |
246 (a.data as UserView).comment_score)
255 {i.type_ == 'posts' && (
256 <PostListing post={i.data as Post} showCommunity />
258 {i.type_ == 'comments' && (
260 nodes={[{ comment: i.data as Comment }]}
265 {i.type_ == 'communities' && (
268 <Link to={`/c/${(i.data as Community).name}`}>{`/c/${
269 (i.data as Community).name
272 <span>{` - ${(i.data as Community).title} - ${
273 (i.data as Community).number_of_subscribers
274 } subscribers`}</span>
277 {i.type_ == 'users' && (
281 className="text-info"
282 to={`/u/${(i.data as UserView).name}`}
284 {(i.data as UserView).avatar && showAvatars() && (
288 src={pictshareAvatarThumbnail(
289 (i.data as UserView).avatar
291 class="rounded-circle mr-1"
294 <span>{`/u/${(i.data as UserView).name}`}</span>
298 (i.data as UserView).comment_score
299 } comment karma`}</span>
312 {this.state.searchResponse.comments.map(comment => (
315 <CommentNodes nodes={[{ comment: comment }]} locked noIndent />
326 {this.state.searchResponse.posts.map(post => (
329 <PostListing post={post} showCommunity />
337 // Todo possibly create UserListing and CommunityListing
341 {this.state.searchResponse.communities.map(community => (
346 to={`/c/${community.name}`}
347 >{`/c/${community.name}`}</Link>
349 <span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
360 {this.state.searchResponse.users.map(user => (
365 className="text-info"
366 to={`/u/${user.name}`}
367 >{`/u/${user.name}`}</Link>
369 <span>{` - ${user.comment_score} comment karma`}</span>
380 {this.state.page > 1 && (
382 class="btn btn-sm btn-secondary mr-1"
383 onClick={linkEvent(this, this.prevPage)}
385 <T i18nKey="prev">#</T>
389 class="btn btn-sm btn-secondary"
390 onClick={linkEvent(this, this.nextPage)}
392 <T i18nKey="next">#</T>
399 let res = this.state.searchResponse;
404 res.posts.length == 0 &&
405 res.comments.length == 0 &&
406 res.communities.length == 0 &&
407 res.users.length == 0 && (
409 <T i18nKey="no_results">#</T>
416 nextPage(i: Search) {
423 prevPage(i: Search) {
431 let form: SearchForm = {
433 type_: SearchType[this.state.type_],
434 sort: SortType[this.state.sort],
435 page: this.state.page,
439 if (this.state.q != '') {
440 WebSocketService.Instance.search(form);
444 handleSortChange(val: SortType) {
445 this.state.sort = val;
447 this.setState(this.state);
451 handleTypeChange(i: Search, event: any) {
452 i.state.type_ = Number(event.target.value);
458 handleSearchSubmit(i: Search, event: any) {
459 event.preventDefault();
460 i.state.loading = true;
466 handleQChange(i: Search, event: any) {
467 i.state.q = event.target.value;
472 let typeStr = SearchType[this.state.type_].toLowerCase();
473 let sortStr = SortType[this.state.sort].toLowerCase();
474 this.props.history.push(
475 `/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
479 parseMessage(msg: any) {
481 let op: UserOperation = msgOp(msg);
483 alert(i18n.t(msg.error));
485 } else if (op == UserOperation.Search) {
486 let res: SearchResponse = msg;
487 this.state.searchResponse = res;
488 this.state.loading = false;
489 document.title = `${i18n.t('search')} - ${this.state.q} - ${
490 WebSocketService.Instance.site.name
492 window.scrollTo(0, 0);
493 this.setState(this.state);
494 } else if (op == UserOperation.CreateCommentLike) {
495 let res: CommentResponse = msg;
496 let found: Comment = this.state.searchResponse.comments.find(
497 c => c.id === res.comment.id
499 found.score = res.comment.score;
500 found.upvotes = res.comment.upvotes;
501 found.downvotes = res.comment.downvotes;
502 if (res.comment.my_vote !== null) {
503 found.my_vote = res.comment.my_vote;
504 found.upvoteLoading = false;
505 found.downvoteLoading = false;
507 this.setState(this.state);
508 } else if (op == UserOperation.CreatePostLike) {
509 let res: CreatePostLikeResponse = msg;
510 let found = this.state.searchResponse.posts.find(
511 c => c.id == res.post.id
513 found.my_vote = res.post.my_vote;
514 found.score = res.post.score;
515 found.upvotes = res.post.upvotes;
516 found.downvotes = res.post.downvotes;
517 this.setState(this.state);