1 import { Component, linkEvent } from 'inferno';
2 import { Subscription } from 'rxjs';
16 } from 'lemmy-js-client';
17 import { WebSocketService } from '../services';
21 routeSearchTypeToEnum,
25 createPostLikeFindRes,
34 restoreScrollPosition,
36 import { PostListing } from './post-listing';
37 import { HtmlTags } from './html-tags';
38 import { UserListing } from './user-listing';
39 import { CommunityLink } from './community-link';
40 import { SortSelect } from './sort-select';
41 import { CommentNodes } from './comment-nodes';
42 import { i18n } from '../i18next';
43 import { InitialFetchRequest } from 'shared/interfaces';
45 interface SearchProps {
52 interface SearchState {
57 searchResponse: SearchResponse;
70 export class Search extends Component<any, SearchState> {
71 private isoData = setIsoData(this.context);
72 private subscription: Subscription;
73 private emptyState: SearchState = {
74 q: Search.getSearchQueryFromProps(this.props.match.params.q),
75 type_: Search.getSearchTypeFromProps(this.props.match.params.type),
76 sort: Search.getSortTypeFromProps(this.props.match.params.sort),
77 page: Search.getPageFromProps(this.props.match.params.page),
78 searchText: Search.getSearchQueryFromProps(this.props.match.params.q),
87 site: this.isoData.site_res.site_view.site,
90 static getSearchQueryFromProps(q: string): string {
91 return decodeURIComponent(q) || '';
94 static getSearchTypeFromProps(type_: string): SearchType {
95 return type_ ? routeSearchTypeToEnum(type_) : SearchType.All;
98 static getSortTypeFromProps(sort: string): SortType {
99 return sort ? routeSortTypeToEnum(sort) : SortType.TopAll;
102 static getPageFromProps(page: string): number {
103 return page ? Number(page) : 1;
106 constructor(props: any, context: any) {
107 super(props, context);
109 this.state = this.emptyState;
110 this.handleSortChange = this.handleSortChange.bind(this);
112 this.parseMessage = this.parseMessage.bind(this);
113 this.subscription = wsSubscribe(this.parseMessage);
115 // Only fetch the data if coming from another route
116 if (this.state.q != '') {
117 if (this.isoData.path == this.context.router.route.match.url) {
118 this.state.searchResponse = this.isoData.routeData[0];
119 this.state.loading = false;
126 componentWillUnmount() {
127 this.subscription.unsubscribe();
128 saveScrollPosition(this.context);
131 static getDerivedStateFromProps(props: any): SearchProps {
133 q: Search.getSearchQueryFromProps(props.match.params.q),
134 type_: Search.getSearchTypeFromProps(props.match.params.type),
135 sort: Search.getSortTypeFromProps(props.match.params.sort),
136 page: Search.getPageFromProps(props.match.params.page),
140 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
141 let pathSplit = req.path.split('/');
142 let promises: Promise<any>[] = [];
144 let form: SearchForm = {
145 q: this.getSearchQueryFromProps(pathSplit[3]),
146 type_: this.getSearchTypeFromProps(pathSplit[5]),
147 sort: this.getSortTypeFromProps(pathSplit[7]),
148 page: this.getPageFromProps(pathSplit[9]),
151 setOptionalAuth(form, req.auth);
154 promises.push(req.client.search(form));
160 componentDidUpdate(_: any, lastState: SearchState) {
162 lastState.q !== this.state.q ||
163 lastState.type_ !== this.state.type_ ||
164 lastState.sort !== this.state.sort ||
165 lastState.page !== this.state.page
167 this.setState({ loading: true, searchText: this.state.q });
172 get documentTitle(): string {
174 return `${i18n.t('search')} - ${this.state.q} - ${this.state.site.name}`;
176 return `${i18n.t('search')} - ${this.state.site.name}`;
182 <div class="container">
184 title={this.documentTitle}
185 path={this.context.router.route.match.url}
187 <h5>{i18n.t('search')}</h5>
190 {this.state.type_ == SearchType.All && this.all()}
191 {this.state.type_ == SearchType.Comments && this.comments()}
192 {this.state.type_ == SearchType.Posts && this.posts()}
193 {this.state.type_ == SearchType.Communities && this.communities()}
194 {this.state.type_ == SearchType.Users && this.users()}
195 {this.resultsCount() == 0 && <span>{i18n.t('no_results')}</span>}
205 onSubmit={linkEvent(this, this.handleSearchSubmit)}
209 class="form-control mr-2 mb-2"
210 value={this.state.searchText}
211 placeholder={`${i18n.t('search')}...`}
212 aria-label={i18n.t('search')}
213 onInput={linkEvent(this, this.handleQChange)}
219 class="btn btn-secondary mr-2 mb-2"
220 aria-label={i18n.t('search')}
222 {this.state.loading ? (
223 <svg class="icon icon-spinner spin">
224 <use xlinkHref="#icon-spinner"></use>
227 <span>{i18n.t('search')}</span>
236 <div className="mb-2">
238 value={this.state.type_}
239 onChange={linkEvent(this, this.handleTypeChange)}
240 class="custom-select w-auto mb-2"
241 aria-label={i18n.t('type')}
243 <option disabled aria-hidden="true">
246 <option value={SearchType.All}>{i18n.t('all')}</option>
247 <option value={SearchType.Comments}>{i18n.t('comments')}</option>
248 <option value={SearchType.Posts}>{i18n.t('posts')}</option>
249 <option value={SearchType.Communities}>
250 {i18n.t('communities')}
252 <option value={SearchType.Users}>{i18n.t('users')}</option>
256 sort={this.state.sort}
257 onChange={this.handleSortChange}
269 data: CommentView | PostView | CommunityView | UserViewSafe;
272 let comments = this.state.searchResponse.comments.map(e => {
273 return { type_: 'comments', data: e, published: e.comment.published };
275 let posts = this.state.searchResponse.posts.map(e => {
276 return { type_: 'posts', data: e, published: e.post.published };
278 let communities = this.state.searchResponse.communities.map(e => {
280 type_: 'communities',
282 published: e.community.published,
285 let users = this.state.searchResponse.users.map(e => {
286 return { type_: 'users', data: e, published: e.user.published };
289 combined.push(...comments);
290 combined.push(...posts);
291 combined.push(...communities);
292 combined.push(...users);
295 if (this.state.sort == SortType.New) {
296 combined.sort((a, b) => b.published.localeCompare(a.published));
300 ((b.data as CommentView | PostView).counts.score |
301 (b.data as CommunityView).counts.subscribers |
302 (b.data as UserViewSafe).counts.comment_score) -
303 ((a.data as CommentView | PostView).counts.score |
304 (a.data as CommunityView).counts.subscribers |
305 (a.data as UserViewSafe).counts.comment_score)
314 {i.type_ == 'posts' && (
316 key={(i.data as PostView).post.id}
317 post_view={i.data as PostView}
319 enableDownvotes={this.state.site.enable_downvotes}
320 enableNsfw={this.state.site.enable_nsfw}
323 {i.type_ == 'comments' && (
325 key={(i.data as CommentView).comment.id}
326 nodes={[{ comment_view: i.data as CommentView }]}
329 enableDownvotes={this.state.site.enable_downvotes}
332 {i.type_ == 'communities' && (
333 <div>{this.communityListing(i.data as CommunityView)}</div>
335 {i.type_ == 'users' && (
336 <div>{this.userListing(i.data as UserViewSafe)}</div>
348 nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
351 enableDownvotes={this.state.site.enable_downvotes}
359 {this.state.searchResponse.posts.map(post => (
365 enableDownvotes={this.state.site.enable_downvotes}
366 enableNsfw={this.state.site.enable_nsfw}
378 {this.state.searchResponse.communities.map(community => (
380 <div class="col-12">{this.communityListing(community)}</div>
387 communityListing(community_view: CommunityView) {
391 <CommunityLink community={community_view.community} />
394 ${i18n.t('number_of_subscribers', {
395 count: community_view.counts.subscribers,
402 userListing(user_view: UserViewSafe) {
405 <UserListing user={user_view.user} showApubName />
407 <span>{` - ${i18n.t('number_of_comments', {
408 count: user_view.counts.comment_count,
416 {this.state.searchResponse.users.map(user => (
418 <div class="col-12">{this.userListing(user)}</div>
428 {this.state.page > 1 && (
430 class="btn btn-secondary mr-1"
431 onClick={linkEvent(this, this.prevPage)}
437 {this.resultsCount() > 0 && (
439 class="btn btn-secondary"
440 onClick={linkEvent(this, this.nextPage)}
449 resultsCount(): number {
450 let res = this.state.searchResponse;
453 res.comments.length +
454 res.communities.length +
459 nextPage(i: Search) {
460 i.updateUrl({ page: i.state.page + 1 });
463 prevPage(i: Search) {
464 i.updateUrl({ page: i.state.page - 1 });
468 let form: SearchForm = {
470 type_: this.state.type_,
471 sort: this.state.sort,
472 page: this.state.page,
474 auth: authField(false),
477 if (this.state.q != '') {
478 WebSocketService.Instance.send(wsClient.search(form));
482 handleSortChange(val: SortType) {
483 this.updateUrl({ sort: val, page: 1 });
486 handleTypeChange(i: Search, event: any) {
488 type_: SearchType[event.target.value],
493 handleSearchSubmit(i: Search, event: any) {
494 event.preventDefault();
496 q: i.state.searchText,
497 type_: i.state.type_,
503 handleQChange(i: Search, event: any) {
504 i.setState({ searchText: event.target.value });
507 updateUrl(paramUpdates: UrlParams) {
508 const qStr = paramUpdates.q || this.state.q;
509 const qStrEncoded = encodeURIComponent(qStr);
510 const typeStr = paramUpdates.type_ || this.state.type_;
511 const sortStr = paramUpdates.sort || this.state.sort;
512 const page = paramUpdates.page || this.state.page;
513 this.props.history.push(
514 `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/page/${page}`
518 parseMessage(msg: any) {
520 let op = wsUserOp(msg);
522 toast(i18n.t(msg.error), 'danger');
524 } else if (op == UserOperation.Search) {
525 let data = wsJsonToRes<SearchResponse>(msg).data;
526 this.state.searchResponse = data;
527 this.state.loading = false;
528 window.scrollTo(0, 0);
529 this.setState(this.state);
530 restoreScrollPosition(this.context);
531 } else if (op == UserOperation.CreateCommentLike) {
532 let data = wsJsonToRes<CommentResponse>(msg).data;
533 createCommentLikeRes(
535 this.state.searchResponse.comments
537 this.setState(this.state);
538 } else if (op == UserOperation.CreatePostLike) {
539 let data = wsJsonToRes<PostResponse>(msg).data;
540 createPostLikeFindRes(data.post_view, this.state.searchResponse.posts);
541 this.setState(this.state);