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}
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' && (
317 name: (i.data as UserView).name,
318 preferred_username: (i.data as UserView)
320 avatar: (i.data as UserView).avatar,
324 <span>{` - ${i18n.t('number_of_comments', {
325 count: (i.data as UserView).number_of_comments,
339 nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
342 enableDownvotes={this.state.site.enable_downvotes}
350 {this.state.searchResponse.posts.map(post => (
356 enableDownvotes={this.state.site.enable_downvotes}
357 enableNsfw={this.state.site.enable_nsfw}
366 // Todo possibly create UserListing and CommunityListing
370 {this.state.searchResponse.communities.map(community => (
372 <div class="col-12">{this.communityListing(community)}</div>
379 communityListing(community: Community) {
383 <CommunityLink community={community} />
385 <span>{` - ${community.title} -
386 ${i18n.t('number_of_subscribers', {
387 count: community.number_of_subscribers,
397 {this.state.searchResponse.users.map(user => (
409 <span>{` - ${i18n.t('number_of_comments', {
410 count: user.number_of_comments,
422 {this.state.page > 1 && (
424 class="btn btn-secondary mr-1"
425 onClick={linkEvent(this, this.prevPage)}
431 {this.resultsCount() > 0 && (
433 class="btn btn-secondary"
434 onClick={linkEvent(this, this.nextPage)}
443 resultsCount(): number {
444 let res = this.state.searchResponse;
447 res.comments.length +
448 res.communities.length +
453 nextPage(i: Search) {
454 i.updateUrl({ page: i.state.page + 1 });
457 prevPage(i: Search) {
458 i.updateUrl({ page: i.state.page - 1 });
462 let form: SearchForm = {
464 type_: this.state.type_,
465 sort: this.state.sort,
466 page: this.state.page,
470 if (this.state.q != '') {
471 WebSocketService.Instance.search(form);
475 handleSortChange(val: SortType) {
476 this.updateUrl({ sort: val, page: 1 });
479 handleTypeChange(i: Search, event: any) {
481 type_: SearchType[event.target.value],
486 handleSearchSubmit(i: Search, event: any) {
487 event.preventDefault();
489 q: i.state.searchText,
490 type_: i.state.type_,
496 handleQChange(i: Search, event: any) {
497 i.setState({ searchText: event.target.value });
500 updateUrl(paramUpdates: UrlParams) {
501 const qStr = paramUpdates.q || this.state.q;
502 const typeStr = paramUpdates.type_ || this.state.type_;
503 const sortStr = paramUpdates.sort || this.state.sort;
504 const page = paramUpdates.page || this.state.page;
505 this.props.history.push(
506 `/search/q/${qStr}/type/${typeStr}/sort/${sortStr}/page/${page}`
510 parseMessage(msg: WebSocketJsonResponse) {
512 let res = wsJsonToRes(msg);
514 toast(i18n.t(msg.error), 'danger');
516 } else if (res.op == UserOperation.Search) {
517 let data = res.data as SearchResponse;
518 this.state.searchResponse = data;
519 this.state.loading = false;
520 window.scrollTo(0, 0);
521 this.setState(this.state);
522 } else if (res.op == UserOperation.CreateCommentLike) {
523 let data = res.data as CommentResponse;
524 createCommentLikeRes(data, this.state.searchResponse.comments);
525 this.setState(this.state);
526 } else if (res.op == UserOperation.CreatePostLike) {
527 let data = res.data as PostResponse;
528 createPostLikeFindRes(data, this.state.searchResponse.posts);
529 this.setState(this.state);
530 } else if (res.op == UserOperation.GetSite) {
531 let data = res.data as GetSiteResponse;
532 this.state.site = data.site;
533 this.setState(this.state);