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 { Spinner } from "./icon";
39 import { UserListing } from "./user-listing";
40 import { CommunityLink } from "./community-link";
41 import { SortSelect } from "./sort-select";
42 import { CommentNodes } from "./comment-nodes";
43 import { i18n } from "../i18next";
44 import { InitialFetchRequest } from "shared/interfaces";
46 interface SearchProps {
53 interface SearchState {
58 searchResponse: SearchResponse;
71 export class Search extends Component<any, SearchState> {
72 private isoData = setIsoData(this.context);
73 private subscription: Subscription;
74 private emptyState: SearchState = {
75 q: Search.getSearchQueryFromProps(this.props.match.params.q),
76 type_: Search.getSearchTypeFromProps(this.props.match.params.type),
77 sort: Search.getSortTypeFromProps(this.props.match.params.sort),
78 page: Search.getPageFromProps(this.props.match.params.page),
79 searchText: Search.getSearchQueryFromProps(this.props.match.params.q),
88 site: this.isoData.site_res.site_view.site,
91 static getSearchQueryFromProps(q: string): string {
92 return decodeURIComponent(q) || "";
95 static getSearchTypeFromProps(type_: string): SearchType {
96 return type_ ? routeSearchTypeToEnum(type_) : SearchType.All;
99 static getSortTypeFromProps(sort: string): SortType {
100 return sort ? routeSortTypeToEnum(sort) : SortType.TopAll;
103 static getPageFromProps(page: string): number {
104 return page ? Number(page) : 1;
107 constructor(props: any, context: any) {
108 super(props, context);
110 this.state = this.emptyState;
111 this.handleSortChange = this.handleSortChange.bind(this);
113 this.parseMessage = this.parseMessage.bind(this);
114 this.subscription = wsSubscribe(this.parseMessage);
116 // Only fetch the data if coming from another route
117 if (this.state.q != "") {
118 if (this.isoData.path == this.context.router.route.match.url) {
119 this.state.searchResponse = this.isoData.routeData[0];
120 this.state.loading = false;
127 componentWillUnmount() {
128 this.subscription.unsubscribe();
129 saveScrollPosition(this.context);
132 static getDerivedStateFromProps(props: any): SearchProps {
134 q: Search.getSearchQueryFromProps(props.match.params.q),
135 type_: Search.getSearchTypeFromProps(props.match.params.type),
136 sort: Search.getSortTypeFromProps(props.match.params.sort),
137 page: Search.getPageFromProps(props.match.params.page),
141 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
142 let pathSplit = req.path.split("/");
143 let promises: Promise<any>[] = [];
145 let form: SearchForm = {
146 q: this.getSearchQueryFromProps(pathSplit[3]),
147 type_: this.getSearchTypeFromProps(pathSplit[5]),
148 sort: this.getSortTypeFromProps(pathSplit[7]),
149 page: this.getPageFromProps(pathSplit[9]),
152 setOptionalAuth(form, req.auth);
155 promises.push(req.client.search(form));
161 componentDidUpdate(_: any, lastState: SearchState) {
163 lastState.q !== this.state.q ||
164 lastState.type_ !== this.state.type_ ||
165 lastState.sort !== this.state.sort ||
166 lastState.page !== this.state.page
168 this.setState({ loading: true, searchText: this.state.q });
173 get documentTitle(): string {
175 return `${i18n.t("search")} - ${this.state.q} - ${this.state.site.name}`;
177 return `${i18n.t("search")} - ${this.state.site.name}`;
183 <div class="container">
185 title={this.documentTitle}
186 path={this.context.router.route.match.url}
188 <h5>{i18n.t("search")}</h5>
191 {this.state.type_ == SearchType.All && this.all()}
192 {this.state.type_ == SearchType.Comments && this.comments()}
193 {this.state.type_ == SearchType.Posts && this.posts()}
194 {this.state.type_ == SearchType.Communities && this.communities()}
195 {this.state.type_ == SearchType.Users && this.users()}
196 {this.resultsCount() == 0 && <span>{i18n.t("no_results")}</span>}
206 onSubmit={linkEvent(this, this.handleSearchSubmit)}
210 class="form-control mr-2 mb-2"
211 value={this.state.searchText}
212 placeholder={`${i18n.t("search")}...`}
213 aria-label={i18n.t("search")}
214 onInput={linkEvent(this, this.handleQChange)}
218 <button type="submit" class="btn btn-secondary mr-2 mb-2">
219 {this.state.loading ? <Spinner /> : <span>{i18n.t("search")}</span>}
227 <div className="mb-2">
229 value={this.state.type_}
230 onChange={linkEvent(this, this.handleTypeChange)}
231 class="custom-select w-auto mb-2"
232 aria-label={i18n.t("type")}
234 <option disabled aria-hidden="true">
237 <option value={SearchType.All}>{i18n.t("all")}</option>
238 <option value={SearchType.Comments}>{i18n.t("comments")}</option>
239 <option value={SearchType.Posts}>{i18n.t("posts")}</option>
240 <option value={SearchType.Communities}>
241 {i18n.t("communities")}
243 <option value={SearchType.Users}>{i18n.t("users")}</option>
247 sort={this.state.sort}
248 onChange={this.handleSortChange}
260 data: CommentView | PostView | CommunityView | UserViewSafe;
263 let comments = this.state.searchResponse.comments.map(e => {
264 return { type_: "comments", data: e, published: e.comment.published };
266 let posts = this.state.searchResponse.posts.map(e => {
267 return { type_: "posts", data: e, published: e.post.published };
269 let communities = this.state.searchResponse.communities.map(e => {
271 type_: "communities",
273 published: e.community.published,
276 let users = this.state.searchResponse.users.map(e => {
277 return { type_: "users", data: e, published: e.user.published };
280 combined.push(...comments);
281 combined.push(...posts);
282 combined.push(...communities);
283 combined.push(...users);
286 if (this.state.sort == SortType.New) {
287 combined.sort((a, b) => b.published.localeCompare(a.published));
291 ((b.data as CommentView | PostView).counts.score |
292 (b.data as CommunityView).counts.subscribers |
293 (b.data as UserViewSafe).counts.comment_score) -
294 ((a.data as CommentView | PostView).counts.score |
295 (a.data as CommunityView).counts.subscribers |
296 (a.data as UserViewSafe).counts.comment_score)
305 {i.type_ == "posts" && (
307 key={(i.data as PostView).post.id}
308 post_view={i.data as PostView}
310 enableDownvotes={this.state.site.enable_downvotes}
311 enableNsfw={this.state.site.enable_nsfw}
314 {i.type_ == "comments" && (
316 key={(i.data as CommentView).comment.id}
317 nodes={[{ comment_view: i.data as CommentView }]}
320 enableDownvotes={this.state.site.enable_downvotes}
323 {i.type_ == "communities" && (
324 <div>{this.communityListing(i.data as CommunityView)}</div>
326 {i.type_ == "users" && (
327 <div>{this.userListing(i.data as UserViewSafe)}</div>
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}
369 {this.state.searchResponse.communities.map(community => (
371 <div class="col-12">{this.communityListing(community)}</div>
378 communityListing(community_view: CommunityView) {
382 <CommunityLink community={community_view.community} />
385 ${i18n.t("number_of_subscribers", {
386 count: community_view.counts.subscribers,
393 userListing(user_view: UserViewSafe) {
396 <UserListing user={user_view.user} showApubName />
398 <span>{` - ${i18n.t("number_of_comments", {
399 count: user_view.counts.comment_count,
407 {this.state.searchResponse.users.map(user => (
409 <div class="col-12">{this.userListing(user)}</div>
419 {this.state.page > 1 && (
421 class="btn btn-secondary mr-1"
422 onClick={linkEvent(this, this.prevPage)}
428 {this.resultsCount() > 0 && (
430 class="btn btn-secondary"
431 onClick={linkEvent(this, this.nextPage)}
440 resultsCount(): number {
441 let res = this.state.searchResponse;
444 res.comments.length +
445 res.communities.length +
450 nextPage(i: Search) {
451 i.updateUrl({ page: i.state.page + 1 });
454 prevPage(i: Search) {
455 i.updateUrl({ page: i.state.page - 1 });
459 let form: SearchForm = {
461 type_: this.state.type_,
462 sort: this.state.sort,
463 page: this.state.page,
465 auth: authField(false),
468 if (this.state.q != "") {
469 WebSocketService.Instance.send(wsClient.search(form));
473 handleSortChange(val: SortType) {
474 this.updateUrl({ sort: val, page: 1 });
477 handleTypeChange(i: Search, event: any) {
479 type_: SearchType[event.target.value],
484 handleSearchSubmit(i: Search, event: any) {
485 event.preventDefault();
487 q: i.state.searchText,
488 type_: i.state.type_,
494 handleQChange(i: Search, event: any) {
495 i.setState({ searchText: event.target.value });
498 updateUrl(paramUpdates: UrlParams) {
499 const qStr = paramUpdates.q || this.state.q;
500 const qStrEncoded = encodeURIComponent(qStr);
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/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/page/${page}`
509 parseMessage(msg: any) {
511 let op = wsUserOp(msg);
513 toast(i18n.t(msg.error), "danger");
515 } else if (op == UserOperation.Search) {
516 let data = wsJsonToRes<SearchResponse>(msg).data;
517 this.state.searchResponse = data;
518 this.state.loading = false;
519 window.scrollTo(0, 0);
520 this.setState(this.state);
521 restoreScrollPosition(this.context);
522 } else if (op == UserOperation.CreateCommentLike) {
523 let data = wsJsonToRes<CommentResponse>(msg).data;
524 createCommentLikeRes(
526 this.state.searchResponse.comments
528 this.setState(this.state);
529 } else if (op == UserOperation.CreatePostLike) {
530 let data = wsJsonToRes<PostResponse>(msg).data;
531 createPostLikeFindRes(data.post_view, this.state.searchResponse.posts);
532 this.setState(this.state);