1 import { Component, linkEvent } from 'inferno';
2 import { Helmet } from 'inferno-helmet';
3 import { Subscription } from 'rxjs';
4 import { retryWhen, delay, take } from 'rxjs/operators';
17 WebSocketJsonResponse,
20 } from 'lemmy-js-client';
21 import { WebSocketService } from '../services';
25 routeSearchTypeToEnum,
29 createPostLikeFindRes,
33 import { PostListing } from './post-listing';
34 import { UserListing } from './user-listing';
35 import { CommunityLink } from './community-link';
36 import { SortSelect } from './sort-select';
37 import { CommentNodes } from './comment-nodes';
38 import { i18n } from '../i18next';
40 interface SearchState {
45 searchResponse: SearchResponse;
51 interface SearchProps {
65 export class Search extends Component<any, SearchState> {
66 private subscription: Subscription;
67 private emptyState: SearchState = {
68 q: Search.getSearchQueryFromProps(this.props),
69 type_: Search.getSearchTypeFromProps(this.props),
70 sort: Search.getSortTypeFromProps(this.props),
71 page: getPageFromProps(this.props),
72 searchText: Search.getSearchQueryFromProps(this.props),
84 creator_id: undefined,
86 creator_name: undefined,
87 number_of_users: undefined,
88 number_of_posts: undefined,
89 number_of_comments: undefined,
90 number_of_communities: undefined,
91 enable_downvotes: undefined,
92 open_registration: undefined,
93 enable_nsfw: undefined,
97 static getSearchQueryFromProps(props: any): string {
98 return props.match.params.q ? props.match.params.q : '';
101 static getSearchTypeFromProps(props: any): SearchType {
102 return props.match.params.type
103 ? routeSearchTypeToEnum(props.match.params.type)
107 static getSortTypeFromProps(props: any): SortType {
108 return props.match.params.sort
109 ? routeSortTypeToEnum(props.match.params.sort)
113 constructor(props: any, context: any) {
114 super(props, context);
116 this.state = this.emptyState;
117 this.handleSortChange = this.handleSortChange.bind(this);
119 this.subscription = WebSocketService.Instance.subject
120 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
122 msg => this.parseMessage(msg),
123 err => console.error(err),
124 () => console.log('complete')
127 WebSocketService.Instance.getSite();
134 componentWillUnmount() {
135 this.subscription.unsubscribe();
138 static getDerivedStateFromProps(props: any): SearchProps {
140 q: Search.getSearchQueryFromProps(props),
141 type_: Search.getSearchTypeFromProps(props),
142 sort: Search.getSortTypeFromProps(props),
143 page: getPageFromProps(props),
147 componentDidUpdate(_: any, lastState: SearchState) {
149 lastState.q !== this.state.q ||
150 lastState.type_ !== this.state.type_ ||
151 lastState.sort !== this.state.sort ||
152 lastState.page !== this.state.page
154 this.setState({ loading: true, searchText: this.state.q });
159 get documentTitle(): string {
160 if (this.state.site.name) {
162 return `${i18n.t('search')} - ${this.state.q} - ${
166 return `${i18n.t('search')} - ${this.state.site.name}`;
175 <div class="container">
176 <Helmet title={this.documentTitle} />
177 <h5>{i18n.t('search')}</h5>
180 {this.state.type_ == SearchType.All && this.all()}
181 {this.state.type_ == SearchType.Comments && this.comments()}
182 {this.state.type_ == SearchType.Posts && this.posts()}
183 {this.state.type_ == SearchType.Communities && this.communities()}
184 {this.state.type_ == SearchType.Users && this.users()}
185 {this.resultsCount() == 0 && <span>{i18n.t('no_results')}</span>}
195 onSubmit={linkEvent(this, this.handleSearchSubmit)}
199 class="form-control mr-2 mb-2"
200 value={this.state.searchText}
201 placeholder={`${i18n.t('search')}...`}
202 onInput={linkEvent(this, this.handleQChange)}
206 <button type="submit" class="btn btn-secondary mr-2 mb-2">
207 {this.state.loading ? (
208 <svg class="icon icon-spinner spin">
209 <use xlinkHref="#icon-spinner"></use>
212 <span>{i18n.t('search')}</span>
221 <div className="mb-2">
223 value={this.state.type_}
224 onChange={linkEvent(this, this.handleTypeChange)}
225 class="custom-select w-auto mb-2"
227 <option disabled>{i18n.t('type')}</option>
228 <option value={SearchType.All}>{i18n.t('all')}</option>
229 <option value={SearchType.Comments}>{i18n.t('comments')}</option>
230 <option value={SearchType.Posts}>{i18n.t('posts')}</option>
231 <option value={SearchType.Communities}>
232 {i18n.t('communities')}
234 <option value={SearchType.Users}>{i18n.t('users')}</option>
238 sort={this.state.sort}
239 onChange={this.handleSortChange}
248 let combined: Array<{
250 data: Comment | Post | Community | UserView;
252 let comments = this.state.searchResponse.comments.map(e => {
253 return { type_: 'comments', data: e };
255 let posts = this.state.searchResponse.posts.map(e => {
256 return { type_: 'posts', data: e };
258 let communities = this.state.searchResponse.communities.map(e => {
259 return { type_: 'communities', data: e };
261 let users = this.state.searchResponse.users.map(e => {
262 return { type_: 'users', data: e };
265 combined.push(...comments);
266 combined.push(...posts);
267 combined.push(...communities);
268 combined.push(...users);
271 if (this.state.sort == SortType.New) {
272 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
276 ((b.data as Comment | Post).score |
277 (b.data as Community).number_of_subscribers |
278 (b.data as UserView).comment_score) -
279 ((a.data as Comment | Post).score |
280 (a.data as Community).number_of_subscribers |
281 (a.data as UserView).comment_score)
290 {i.type_ == 'posts' && (
292 key={(i.data as Post).id}
293 post={i.data as Post}
295 enableDownvotes={this.state.site.enable_downvotes}
296 enableNsfw={this.state.site.enable_nsfw}
299 {i.type_ == 'comments' && (
301 key={(i.data as Comment).id}
302 nodes={[{ comment: i.data as Comment }]}
305 enableDownvotes={this.state.site.enable_downvotes}
308 {i.type_ == 'communities' && (
309 <div>{this.communityListing(i.data as Community)}</div>
311 {i.type_ == 'users' && (
316 name: (i.data as UserView).name,
317 preferred_username: (i.data as UserView)
319 avatar: (i.data as UserView).avatar,
323 <span>{` - ${i18n.t('number_of_comments', {
324 count: (i.data as UserView).number_of_comments,
338 nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
341 enableDownvotes={this.state.site.enable_downvotes}
349 {this.state.searchResponse.posts.map(post => (
355 enableDownvotes={this.state.site.enable_downvotes}
356 enableNsfw={this.state.site.enable_nsfw}
365 // Todo possibly create UserListing and CommunityListing
369 {this.state.searchResponse.communities.map(community => (
371 <div class="col-12">{this.communityListing(community)}</div>
378 communityListing(community: Community) {
382 <CommunityLink community={community} />
384 <span>{` - ${community.title} -
385 ${i18n.t('number_of_subscribers', {
386 count: community.number_of_subscribers,
396 {this.state.searchResponse.users.map(user => (
404 preferred_username: user.preferred_username,
408 <span>{` - ${i18n.t('number_of_comments', {
409 count: user.number_of_comments,
421 {this.state.page > 1 && (
423 class="btn btn-secondary mr-1"
424 onClick={linkEvent(this, this.prevPage)}
430 {this.resultsCount() > 0 && (
432 class="btn btn-secondary"
433 onClick={linkEvent(this, this.nextPage)}
442 resultsCount(): number {
443 let res = this.state.searchResponse;
446 res.comments.length +
447 res.communities.length +
452 nextPage(i: Search) {
453 i.updateUrl({ page: i.state.page + 1 });
456 prevPage(i: Search) {
457 i.updateUrl({ page: i.state.page - 1 });
461 let form: SearchForm = {
463 type_: this.state.type_,
464 sort: this.state.sort,
465 page: this.state.page,
469 if (this.state.q != '') {
470 WebSocketService.Instance.search(form);
474 handleSortChange(val: SortType) {
475 this.updateUrl({ sort: val, page: 1 });
478 handleTypeChange(i: Search, event: any) {
480 type_: SearchType[event.target.value],
485 handleSearchSubmit(i: Search, event: any) {
486 event.preventDefault();
488 q: i.state.searchText,
489 type_: i.state.type_,
495 handleQChange(i: Search, event: any) {
496 i.setState({ searchText: event.target.value });
499 updateUrl(paramUpdates: UrlParams) {
500 const qStr = paramUpdates.q || this.state.q;
501 const typeStr = paramUpdates.type_ || this.state.type_;
502 const sortStr = paramUpdates.sort || this.state.sort;
503 const page = paramUpdates.page || this.state.page;
504 this.props.history.push(
505 `/search/q/${qStr}/type/${typeStr}/sort/${sortStr}/page/${page}`
509 parseMessage(msg: WebSocketJsonResponse) {
511 let res = wsJsonToRes(msg);
513 toast(i18n.t(msg.error), 'danger');
515 } else if (res.op == UserOperation.Search) {
516 let data = res.data as SearchResponse;
517 this.state.searchResponse = data;
518 this.state.loading = false;
519 window.scrollTo(0, 0);
520 this.setState(this.state);
521 } else if (res.op == UserOperation.CreateCommentLike) {
522 let data = res.data as CommentResponse;
523 createCommentLikeRes(data, this.state.searchResponse.comments);
524 this.setState(this.state);
525 } else if (res.op == UserOperation.CreatePostLike) {
526 let data = res.data as PostResponse;
527 createPostLikeFindRes(data, this.state.searchResponse.posts);
528 this.setState(this.state);
529 } else if (res.op == UserOperation.GetSite) {
530 let data = res.data as GetSiteResponse;
531 this.state.site = data.site;
532 this.setState(this.state);