1 import { Component, linkEvent } from 'inferno';
2 import { Subscription } from 'rxjs';
15 WebSocketJsonResponse,
17 } from 'lemmy-js-client';
18 import { WebSocketService } from '../services';
22 routeSearchTypeToEnum,
26 createPostLikeFindRes,
33 import { PostListing } from './post-listing';
34 import { HtmlTags } from './html-tags';
35 import { UserListing } from './user-listing';
36 import { CommunityLink } from './community-link';
37 import { SortSelect } from './sort-select';
38 import { CommentNodes } from './comment-nodes';
39 import { i18n } from '../i18next';
41 interface SearchProps {
48 interface SearchState {
53 searchResponse: SearchResponse;
66 export class Search extends Component<any, SearchState> {
67 private isoData = setIsoData(this.context);
68 private subscription: Subscription;
69 private emptyState: SearchState = {
70 q: Search.getSearchQueryFromProps(this.props.match.params.q),
71 type_: Search.getSearchTypeFromProps(this.props.match.params.type),
72 sort: Search.getSortTypeFromProps(this.props.match.params.sort),
73 page: Search.getPageFromProps(this.props.match.params.page),
74 searchText: Search.getSearchQueryFromProps(this.props.match.params.q),
83 site: this.isoData.site.site,
86 static getSearchQueryFromProps(q: string): string {
87 return decodeURIComponent(q) || '';
90 static getSearchTypeFromProps(type_: string): SearchType {
91 return type_ ? routeSearchTypeToEnum(type_) : SearchType.All;
94 static getSortTypeFromProps(sort: string): SortType {
95 return sort ? routeSortTypeToEnum(sort) : SortType.TopAll;
98 static getPageFromProps(page: string): number {
99 return page ? Number(page) : 1;
102 constructor(props: any, context: any) {
103 super(props, context);
105 this.state = this.emptyState;
106 this.handleSortChange = this.handleSortChange.bind(this);
108 this.parseMessage = this.parseMessage.bind(this);
109 this.subscription = wsSubscribe(this.parseMessage);
111 // Only fetch the data if coming from another route
112 if (this.state.q != '') {
113 if (this.isoData.path == this.context.router.route.match.url) {
114 this.state.searchResponse = this.isoData.routeData[0];
115 this.state.loading = false;
122 componentWillUnmount() {
123 this.subscription.unsubscribe();
126 static getDerivedStateFromProps(props: any): SearchProps {
128 q: Search.getSearchQueryFromProps(props.match.params.q),
129 type_: Search.getSearchTypeFromProps(props.match.params.type),
130 sort: Search.getSortTypeFromProps(props.match.params.sort),
131 page: Search.getPageFromProps(props.match.params.page),
135 static fetchInitialData(auth: string, path: string): Promise<any>[] {
136 let pathSplit = path.split('/');
137 let promises: Promise<any>[] = [];
139 let form: SearchForm = {
140 q: this.getSearchQueryFromProps(pathSplit[3]),
141 type_: this.getSearchTypeFromProps(pathSplit[5]),
142 sort: this.getSortTypeFromProps(pathSplit[7]),
143 page: this.getPageFromProps(pathSplit[9]),
149 promises.push(lemmyHttp.search(form));
155 componentDidUpdate(_: any, lastState: SearchState) {
157 lastState.q !== this.state.q ||
158 lastState.type_ !== this.state.type_ ||
159 lastState.sort !== this.state.sort ||
160 lastState.page !== this.state.page
162 this.setState({ loading: true, searchText: this.state.q });
167 get documentTitle(): string {
169 return `${i18n.t('search')} - ${this.state.q} - ${this.state.site.name}`;
171 return `${i18n.t('search')} - ${this.state.site.name}`;
177 <div class="container">
179 title={this.documentTitle}
180 path={this.context.router.route.match.url}
182 <h5>{i18n.t('search')}</h5>
185 {this.state.type_ == SearchType.All && this.all()}
186 {this.state.type_ == SearchType.Comments && this.comments()}
187 {this.state.type_ == SearchType.Posts && this.posts()}
188 {this.state.type_ == SearchType.Communities && this.communities()}
189 {this.state.type_ == SearchType.Users && this.users()}
190 {this.resultsCount() == 0 && <span>{i18n.t('no_results')}</span>}
200 onSubmit={linkEvent(this, this.handleSearchSubmit)}
204 class="form-control mr-2 mb-2"
205 value={this.state.searchText}
206 placeholder={`${i18n.t('search')}...`}
207 onInput={linkEvent(this, this.handleQChange)}
211 <button type="submit" class="btn btn-secondary mr-2 mb-2">
212 {this.state.loading ? (
213 <svg class="icon icon-spinner spin">
214 <use xlinkHref="#icon-spinner"></use>
217 <span>{i18n.t('search')}</span>
226 <div className="mb-2">
228 value={this.state.type_}
229 onChange={linkEvent(this, this.handleTypeChange)}
230 class="custom-select w-auto mb-2"
232 <option disabled>{i18n.t('type')}</option>
233 <option value={SearchType.All}>{i18n.t('all')}</option>
234 <option value={SearchType.Comments}>{i18n.t('comments')}</option>
235 <option value={SearchType.Posts}>{i18n.t('posts')}</option>
236 <option value={SearchType.Communities}>
237 {i18n.t('communities')}
239 <option value={SearchType.Users}>{i18n.t('users')}</option>
243 sort={this.state.sort}
244 onChange={this.handleSortChange}
255 data: Comment | Post | Community | UserView;
257 let comments = this.state.searchResponse.comments.map(e => {
258 return { type_: 'comments', data: e };
260 let posts = this.state.searchResponse.posts.map(e => {
261 return { type_: 'posts', data: e };
263 let communities = this.state.searchResponse.communities.map(e => {
264 return { type_: 'communities', data: e };
266 let users = this.state.searchResponse.users.map(e => {
267 return { type_: 'users', data: e };
270 combined.push(...comments);
271 combined.push(...posts);
272 combined.push(...communities);
273 combined.push(...users);
276 if (this.state.sort == SortType.New) {
277 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
281 ((b.data as Comment | Post).score |
282 (b.data as Community).number_of_subscribers |
283 (b.data as UserView).comment_score) -
284 ((a.data as Comment | Post).score |
285 (a.data as Community).number_of_subscribers |
286 (a.data as UserView).comment_score)
295 {i.type_ == 'posts' && (
297 key={(i.data as Post).id}
298 post={i.data as Post}
300 enableDownvotes={this.state.site.enable_downvotes}
301 enableNsfw={this.state.site.enable_nsfw}
304 {i.type_ == 'comments' && (
306 key={(i.data as Comment).id}
307 nodes={[{ comment: i.data as Comment }]}
310 enableDownvotes={this.state.site.enable_downvotes}
313 {i.type_ == 'communities' && (
314 <div>{this.communityListing(i.data as Community)}</div>
316 {i.type_ == 'users' && (
322 name: (i.data as UserView).name,
323 preferred_username: (i.data as UserView)
325 avatar: (i.data as UserView).avatar,
329 <span>{` - ${i18n.t('number_of_comments', {
330 count: (i.data as UserView).number_of_comments,
344 nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
347 enableDownvotes={this.state.site.enable_downvotes}
355 {this.state.searchResponse.posts.map(post => (
361 enableDownvotes={this.state.site.enable_downvotes}
362 enableNsfw={this.state.site.enable_nsfw}
371 // Todo possibly create UserListing and CommunityListing
375 {this.state.searchResponse.communities.map(community => (
377 <div class="col-12">{this.communityListing(community)}</div>
384 communityListing(community: Community) {
388 <CommunityLink community={community} />
390 <span>{` - ${community.title} -
391 ${i18n.t('number_of_subscribers', {
392 count: community.number_of_subscribers,
402 {this.state.searchResponse.users.map(user => (
414 <span>{` - ${i18n.t('number_of_comments', {
415 count: user.number_of_comments,
427 {this.state.page > 1 && (
429 class="btn btn-secondary mr-1"
430 onClick={linkEvent(this, this.prevPage)}
436 {this.resultsCount() > 0 && (
438 class="btn btn-secondary"
439 onClick={linkEvent(this, this.nextPage)}
448 resultsCount(): number {
449 let res = this.state.searchResponse;
452 res.comments.length +
453 res.communities.length +
458 nextPage(i: Search) {
459 i.updateUrl({ page: i.state.page + 1 });
462 prevPage(i: Search) {
463 i.updateUrl({ page: i.state.page - 1 });
467 let form: SearchForm = {
469 type_: this.state.type_,
470 sort: this.state.sort,
471 page: this.state.page,
475 if (this.state.q != '') {
476 WebSocketService.Instance.search(form);
480 handleSortChange(val: SortType) {
481 this.updateUrl({ sort: val, page: 1 });
484 handleTypeChange(i: Search, event: any) {
486 type_: SearchType[event.target.value],
491 handleSearchSubmit(i: Search, event: any) {
492 event.preventDefault();
494 q: i.state.searchText,
495 type_: i.state.type_,
501 handleQChange(i: Search, event: any) {
502 i.setState({ searchText: event.target.value });
505 updateUrl(paramUpdates: UrlParams) {
506 const qStr = paramUpdates.q || this.state.q;
507 const qStrEncoded = encodeURIComponent(qStr);
508 const typeStr = paramUpdates.type_ || this.state.type_;
509 const sortStr = paramUpdates.sort || this.state.sort;
510 const page = paramUpdates.page || this.state.page;
511 this.props.history.push(
512 `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/page/${page}`
516 parseMessage(msg: WebSocketJsonResponse) {
518 let res = wsJsonToRes(msg);
520 toast(i18n.t(msg.error), 'danger');
522 } else if (res.op == UserOperation.Search) {
523 let data = res.data as SearchResponse;
524 this.state.searchResponse = data;
525 this.state.loading = false;
526 window.scrollTo(0, 0);
527 this.setState(this.state);
528 } else if (res.op == UserOperation.CreateCommentLike) {
529 let data = res.data as CommentResponse;
530 createCommentLikeRes(data, this.state.searchResponse.comments);
531 this.setState(this.state);
532 } else if (res.op == UserOperation.CreatePostLike) {
533 let data = res.data as PostResponse;
534 createPostLikeFindRes(data, this.state.searchResponse.posts);
535 this.setState(this.state);