1 import { Component, linkEvent } from 'inferno';
2 import { Subscription } from 'rxjs';
16 } from 'lemmy-js-client';
17 import { UserService, WebSocketService } from '../services';
21 routeSearchTypeToEnum,
25 createPostLikeFindRes,
31 import { PostListing } from './post-listing';
32 import { HtmlTags } from './html-tags';
33 import { UserListing } from './user-listing';
34 import { CommunityLink } from './community-link';
35 import { SortSelect } from './sort-select';
36 import { CommentNodes } from './comment-nodes';
37 import { i18n } from '../i18next';
38 import { InitialFetchRequest } from 'shared/interfaces';
40 interface SearchProps {
47 interface SearchState {
52 searchResponse: SearchResponse;
65 export class Search extends Component<any, SearchState> {
66 private isoData = setIsoData(this.context);
67 private subscription: Subscription;
68 private emptyState: SearchState = {
69 q: Search.getSearchQueryFromProps(this.props.match.params.q),
70 type_: Search.getSearchTypeFromProps(this.props.match.params.type),
71 sort: Search.getSortTypeFromProps(this.props.match.params.sort),
72 page: Search.getPageFromProps(this.props.match.params.page),
73 searchText: Search.getSearchQueryFromProps(this.props.match.params.q),
82 site: this.isoData.site_res.site_view.site,
85 static getSearchQueryFromProps(q: string): string {
86 return decodeURIComponent(q) || '';
89 static getSearchTypeFromProps(type_: string): SearchType {
90 return type_ ? routeSearchTypeToEnum(type_) : SearchType.All;
93 static getSortTypeFromProps(sort: string): SortType {
94 return sort ? routeSortTypeToEnum(sort) : SortType.TopAll;
97 static getPageFromProps(page: string): number {
98 return page ? Number(page) : 1;
101 constructor(props: any, context: any) {
102 super(props, context);
104 this.state = this.emptyState;
105 this.handleSortChange = this.handleSortChange.bind(this);
107 this.parseMessage = this.parseMessage.bind(this);
108 this.subscription = wsSubscribe(this.parseMessage);
110 // Only fetch the data if coming from another route
111 if (this.state.q != '') {
112 if (this.isoData.path == this.context.router.route.match.url) {
113 this.state.searchResponse = this.isoData.routeData[0];
114 this.state.loading = false;
121 componentWillUnmount() {
122 this.subscription.unsubscribe();
125 static getDerivedStateFromProps(props: any): SearchProps {
127 q: Search.getSearchQueryFromProps(props.match.params.q),
128 type_: Search.getSearchTypeFromProps(props.match.params.type),
129 sort: Search.getSortTypeFromProps(props.match.params.sort),
130 page: Search.getPageFromProps(props.match.params.page),
134 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
135 let pathSplit = req.path.split('/');
136 let promises: Promise<any>[] = [];
138 let form: SearchForm = {
139 q: this.getSearchQueryFromProps(pathSplit[3]),
140 type_: this.getSearchTypeFromProps(pathSplit[5]),
141 sort: this.getSortTypeFromProps(pathSplit[7]),
142 page: this.getPageFromProps(pathSplit[9]),
148 promises.push(req.client.search(form));
154 componentDidUpdate(_: any, lastState: SearchState) {
156 lastState.q !== this.state.q ||
157 lastState.type_ !== this.state.type_ ||
158 lastState.sort !== this.state.sort ||
159 lastState.page !== this.state.page
161 this.setState({ loading: true, searchText: this.state.q });
166 get documentTitle(): string {
168 return `${i18n.t('search')} - ${this.state.q} - ${this.state.site.name}`;
170 return `${i18n.t('search')} - ${this.state.site.name}`;
176 <div class="container">
178 title={this.documentTitle}
179 path={this.context.router.route.match.url}
181 <h5>{i18n.t('search')}</h5>
184 {this.state.type_ == SearchType.All && this.all()}
185 {this.state.type_ == SearchType.Comments && this.comments()}
186 {this.state.type_ == SearchType.Posts && this.posts()}
187 {this.state.type_ == SearchType.Communities && this.communities()}
188 {this.state.type_ == SearchType.Users && this.users()}
189 {this.resultsCount() == 0 && <span>{i18n.t('no_results')}</span>}
199 onSubmit={linkEvent(this, this.handleSearchSubmit)}
203 class="form-control mr-2 mb-2"
204 value={this.state.searchText}
205 placeholder={`${i18n.t('search')}...`}
206 onInput={linkEvent(this, this.handleQChange)}
210 <button type="submit" class="btn btn-secondary mr-2 mb-2">
211 {this.state.loading ? (
212 <svg class="icon icon-spinner spin">
213 <use xlinkHref="#icon-spinner"></use>
216 <span>{i18n.t('search')}</span>
225 <div className="mb-2">
227 value={this.state.type_}
228 onChange={linkEvent(this, this.handleTypeChange)}
229 class="custom-select w-auto mb-2"
231 <option disabled>{i18n.t('type')}</option>
232 <option value={SearchType.All}>{i18n.t('all')}</option>
233 <option value={SearchType.Comments}>{i18n.t('comments')}</option>
234 <option value={SearchType.Posts}>{i18n.t('posts')}</option>
235 <option value={SearchType.Communities}>
236 {i18n.t('communities')}
238 <option value={SearchType.Users}>{i18n.t('users')}</option>
242 sort={this.state.sort}
243 onChange={this.handleSortChange}
254 data: CommentView | PostView | CommunityView | UserViewSafe;
257 let comments = this.state.searchResponse.comments.map(e => {
258 return { type_: 'comments', data: e, published: e.comment.published };
260 let posts = this.state.searchResponse.posts.map(e => {
261 return { type_: 'posts', data: e, published: e.post.published };
263 let communities = this.state.searchResponse.communities.map(e => {
265 type_: 'communities',
267 published: e.community.published,
270 let users = this.state.searchResponse.users.map(e => {
271 return { type_: 'users', data: e, published: e.user.published };
274 combined.push(...comments);
275 combined.push(...posts);
276 combined.push(...communities);
277 combined.push(...users);
280 if (this.state.sort == SortType.New) {
281 combined.sort((a, b) => b.published.localeCompare(a.published));
285 ((b.data as CommentView | PostView).counts.score |
286 (b.data as CommunityView).counts.subscribers |
287 (b.data as UserViewSafe).counts.comment_score) -
288 ((a.data as CommentView | PostView).counts.score |
289 (a.data as CommunityView).counts.subscribers |
290 (a.data as UserViewSafe).counts.comment_score)
299 {i.type_ == 'posts' && (
301 key={(i.data as PostView).post.id}
302 post_view={i.data as PostView}
304 enableDownvotes={this.state.site.enable_downvotes}
305 enableNsfw={this.state.site.enable_nsfw}
308 {i.type_ == 'comments' && (
310 key={(i.data as CommentView).comment.id}
311 nodes={[{ comment_view: i.data as CommentView }]}
314 enableDownvotes={this.state.site.enable_downvotes}
317 {i.type_ == 'communities' && (
318 <div>{this.communityListing(i.data as CommunityView)}</div>
320 {i.type_ == 'users' && (
321 <div>{this.userListing(i.data as UserViewSafe)}</div>
333 nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
336 enableDownvotes={this.state.site.enable_downvotes}
344 {this.state.searchResponse.posts.map(post => (
350 enableDownvotes={this.state.site.enable_downvotes}
351 enableNsfw={this.state.site.enable_nsfw}
363 {this.state.searchResponse.communities.map(community => (
365 <div class="col-12">{this.communityListing(community)}</div>
372 communityListing(community_view: CommunityView) {
376 <CommunityLink community={community_view.community} />
378 <span>{` - ${community_view.community.title} -
379 ${i18n.t('number_of_subscribers', {
380 count: community_view.counts.subscribers,
387 userListing(user_view: UserViewSafe) {
390 <UserListing user={user_view.user} showApubName />
392 <span>{` - ${i18n.t('number_of_comments', {
393 count: user_view.counts.comment_count,
401 {this.state.searchResponse.users.map(user => (
403 <div class="col-12">{this.userListing(user)}</div>
413 {this.state.page > 1 && (
415 class="btn btn-secondary mr-1"
416 onClick={linkEvent(this, this.prevPage)}
422 {this.resultsCount() > 0 && (
424 class="btn btn-secondary"
425 onClick={linkEvent(this, this.nextPage)}
434 resultsCount(): number {
435 let res = this.state.searchResponse;
438 res.comments.length +
439 res.communities.length +
444 nextPage(i: Search) {
445 i.updateUrl({ page: i.state.page + 1 });
448 prevPage(i: Search) {
449 i.updateUrl({ page: i.state.page - 1 });
453 let form: SearchForm = {
455 type_: this.state.type_,
456 sort: this.state.sort,
457 page: this.state.page,
459 auth: UserService.Instance.authField(false),
462 if (this.state.q != '') {
463 WebSocketService.Instance.client.search(form);
467 handleSortChange(val: SortType) {
468 this.updateUrl({ sort: val, page: 1 });
471 handleTypeChange(i: Search, event: any) {
473 type_: SearchType[event.target.value],
478 handleSearchSubmit(i: Search, event: any) {
479 event.preventDefault();
481 q: i.state.searchText,
482 type_: i.state.type_,
488 handleQChange(i: Search, event: any) {
489 i.setState({ searchText: event.target.value });
492 updateUrl(paramUpdates: UrlParams) {
493 const qStr = paramUpdates.q || this.state.q;
494 const qStrEncoded = encodeURIComponent(qStr);
495 const typeStr = paramUpdates.type_ || this.state.type_;
496 const sortStr = paramUpdates.sort || this.state.sort;
497 const page = paramUpdates.page || this.state.page;
498 this.props.history.push(
499 `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/page/${page}`
503 parseMessage(msg: any) {
505 let op = wsUserOp(msg);
507 toast(i18n.t(msg.error), 'danger');
509 } else if (op == UserOperation.Search) {
510 let data = wsJsonToRes<SearchResponse>(msg).data;
511 this.state.searchResponse = data;
512 this.state.loading = false;
513 window.scrollTo(0, 0);
514 this.setState(this.state);
515 } else if (op == UserOperation.CreateCommentLike) {
516 let data = wsJsonToRes<CommentResponse>(msg).data;
517 createCommentLikeRes(
519 this.state.searchResponse.comments
521 this.setState(this.state);
522 } else if (op == UserOperation.CreatePostLike) {
523 let data = wsJsonToRes<PostResponse>(msg).data;
524 createPostLikeFindRes(data.post_view, this.state.searchResponse.posts);
525 this.setState(this.state);