1 import { Component, linkEvent } from "inferno";
2 import { Subscription } from "rxjs";
18 ListCommunitiesResponse,
21 } from "lemmy-js-client";
22 import { WebSocketService } from "../services";
26 routeSearchTypeToEnum,
30 createPostLikeFindRes,
39 restoreScrollPosition,
40 routeListingTypeToEnum,
49 capitalizeFirstLetter,
53 import { PostListing } from "./post-listing";
54 import { HtmlTags } from "./html-tags";
55 import { Spinner } from "./icon";
56 import { PersonListing } from "./person-listing";
57 import { CommunityLink } from "./community-link";
58 import { SortSelect } from "./sort-select";
59 import { ListingTypeSelect } from "./listing-type-select";
60 import { CommentNodes } from "./comment-nodes";
61 import { Paginator } from "./paginator";
62 import { i18n } from "../i18next";
63 import { InitialFetchRequest } from "shared/interfaces";
67 Choices = require("choices.js");
70 interface SearchProps {
74 listingType: ListingType;
80 interface SearchState {
84 listingType: ListingType;
88 searchResponse: SearchResponse;
89 communities: CommunityView[];
90 creator?: PersonViewSafe;
100 listingType?: ListingType;
101 communityId?: number;
106 export class Search extends Component<any, SearchState> {
107 private isoData = setIsoData(this.context);
108 private communityChoices: any;
109 private creatorChoices: any;
110 private subscription: Subscription;
111 private emptyState: SearchState = {
112 q: Search.getSearchQueryFromProps(this.props.match.params.q),
113 type_: Search.getSearchTypeFromProps(this.props.match.params.type),
114 sort: Search.getSortTypeFromProps(this.props.match.params.sort),
115 listingType: Search.getListingTypeFromProps(
116 this.props.match.params.listing_type
118 page: Search.getPageFromProps(this.props.match.params.page),
119 searchText: Search.getSearchQueryFromProps(this.props.match.params.q),
120 communityId: Search.getCommunityIdFromProps(
121 this.props.match.params.community_id
123 creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id),
132 site: this.isoData.site_res.site_view.site,
136 static getSearchQueryFromProps(q: string): string {
137 return decodeURIComponent(q) || "";
140 static getSearchTypeFromProps(type_: string): SearchType {
141 return type_ ? routeSearchTypeToEnum(type_) : SearchType.All;
144 static getSortTypeFromProps(sort: string): SortType {
145 return sort ? routeSortTypeToEnum(sort) : SortType.TopAll;
148 static getListingTypeFromProps(listingType: string): ListingType {
149 return listingType ? routeListingTypeToEnum(listingType) : ListingType.All;
152 static getCommunityIdFromProps(id: string): number {
153 return id ? Number(id) : 0;
156 static getCreatorIdFromProps(id: string): number {
157 return id ? Number(id) : 0;
160 static getPageFromProps(page: string): number {
161 return page ? Number(page) : 1;
164 constructor(props: any, context: any) {
165 super(props, context);
167 this.state = this.emptyState;
168 this.handleSortChange = this.handleSortChange.bind(this);
169 this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
170 this.handlePageChange = this.handlePageChange.bind(this);
172 this.parseMessage = this.parseMessage.bind(this);
173 this.subscription = wsSubscribe(this.parseMessage);
175 // Only fetch the data if coming from another route
176 if (this.isoData.path == this.context.router.route.match.url) {
177 let singleOrMultipleCommunities = this.isoData.routeData[0];
178 if (singleOrMultipleCommunities.communities) {
179 this.state.communities = this.isoData.routeData[0].communities;
181 this.state.communities = [this.isoData.routeData[0].community_view];
184 let creator = this.isoData.routeData[1];
185 if (creator?.person_view) {
186 this.state.creator = this.isoData.routeData[1].person_view;
188 if (this.state.q != "") {
189 this.state.searchResponse = this.isoData.routeData[2];
190 this.state.loading = false;
195 this.fetchCommunities();
200 componentWillUnmount() {
201 this.subscription.unsubscribe();
202 saveScrollPosition(this.context);
205 componentDidMount() {
206 this.setupCommunityFilter();
207 this.setupCreatorFilter();
210 static getDerivedStateFromProps(props: any): SearchProps {
212 q: Search.getSearchQueryFromProps(props.match.params.q),
213 type_: Search.getSearchTypeFromProps(props.match.params.type),
214 sort: Search.getSortTypeFromProps(props.match.params.sort),
215 listingType: Search.getListingTypeFromProps(
216 props.match.params.listing_type
218 communityId: Search.getCommunityIdFromProps(
219 props.match.params.community_id
221 creatorId: Search.getCreatorIdFromProps(props.match.params.creator_id),
222 page: Search.getPageFromProps(props.match.params.page),
227 let listCommunitiesForm: ListCommunities = {
228 type_: ListingType.All,
229 sort: SortType.TopAll,
231 auth: authField(false),
233 WebSocketService.Instance.send(
234 wsClient.listCommunities(listCommunitiesForm)
238 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
239 let pathSplit = req.path.split("/");
240 let promises: Promise<any>[] = [];
242 let communityId = this.getCommunityIdFromProps(pathSplit[11]);
243 if (communityId !== 0) {
244 let getCommunityForm: GetCommunity = {
247 setOptionalAuth(getCommunityForm, req.auth);
248 promises.push(req.client.getCommunity(getCommunityForm));
250 let listCommunitiesForm: ListCommunities = {
251 type_: ListingType.All,
252 sort: SortType.TopAll,
255 setOptionalAuth(listCommunitiesForm, req.auth);
256 promises.push(req.client.listCommunities(listCommunitiesForm));
259 let creatorId = this.getCreatorIdFromProps(pathSplit[13]);
260 if (creatorId !== 0) {
261 let getCreatorForm: GetPersonDetails = {
262 person_id: creatorId,
264 setOptionalAuth(getCreatorForm, req.auth);
265 promises.push(req.client.getPersonDetails(getCreatorForm));
267 promises.push(Promise.resolve());
270 let form: SearchForm = {
271 q: this.getSearchQueryFromProps(pathSplit[3]),
272 type_: this.getSearchTypeFromProps(pathSplit[5]),
273 sort: this.getSortTypeFromProps(pathSplit[7]),
274 listing_type: this.getListingTypeFromProps(pathSplit[9]),
275 page: this.getPageFromProps(pathSplit[15]),
278 if (communityId !== 0) {
279 form.community_id = communityId;
281 if (creatorId !== 0) {
282 form.creator_id = creatorId;
284 setOptionalAuth(form, req.auth);
287 promises.push(req.client.search(form));
293 componentDidUpdate(_: any, lastState: SearchState) {
295 lastState.q !== this.state.q ||
296 lastState.type_ !== this.state.type_ ||
297 lastState.sort !== this.state.sort ||
298 lastState.listingType !== this.state.listingType ||
299 lastState.communityId !== this.state.communityId ||
300 lastState.creatorId !== this.state.creatorId ||
301 lastState.page !== this.state.page
303 this.setState({ loading: true, searchText: this.state.q });
308 get documentTitle(): string {
310 return `${i18n.t("search")} - ${this.state.q} - ${this.state.site.name}`;
312 return `${i18n.t("search")} - ${this.state.site.name}`;
318 <div class="container">
320 title={this.documentTitle}
321 path={this.context.router.route.match.url}
323 <h5>{i18n.t("search")}</h5>
326 {this.state.type_ == SearchType.All && this.all()}
327 {this.state.type_ == SearchType.Comments && this.comments()}
328 {this.state.type_ == SearchType.Posts && this.posts()}
329 {this.state.type_ == SearchType.Communities && this.communities()}
330 {this.state.type_ == SearchType.Users && this.users()}
331 {this.state.type_ == SearchType.Url && this.posts()}
332 {this.resultsCount() == 0 && <span>{i18n.t("no_results")}</span>}
333 <Paginator page={this.state.page} onChange={this.handlePageChange} />
342 onSubmit={linkEvent(this, this.handleSearchSubmit)}
346 class="form-control mr-2 mb-2"
347 value={this.state.searchText}
348 placeholder={`${i18n.t("search")}...`}
349 aria-label={i18n.t("search")}
350 onInput={linkEvent(this, this.handleQChange)}
354 <button type="submit" class="btn btn-secondary mr-2 mb-2">
355 {this.state.loading ? <Spinner /> : <span>{i18n.t("search")}</span>}
363 <div className="mb-2">
365 value={this.state.type_}
366 onChange={linkEvent(this, this.handleTypeChange)}
367 class="custom-select w-auto mb-2"
368 aria-label={i18n.t("type")}
370 <option disabled aria-hidden="true">
373 <option value={SearchType.All}>{i18n.t("all")}</option>
374 <option value={SearchType.Comments}>{i18n.t("comments")}</option>
375 <option value={SearchType.Posts}>{i18n.t("posts")}</option>
376 <option value={SearchType.Communities}>
377 {i18n.t("communities")}
379 <option value={SearchType.Users}>{i18n.t("users")}</option>
380 <option value={SearchType.Url}>{i18n.t("url")}</option>
384 type_={this.state.listingType}
385 showLocal={showLocal(this.isoData)}
386 onChange={this.handleListingTypeChange}
391 sort={this.state.sort}
392 onChange={this.handleSortChange}
397 <div class="form-row">
398 {this.state.communities.length > 0 && this.communityFilter()}
399 {this.creatorFilter()}
408 data: CommentView | PostView | CommunityView | PersonViewSafe;
411 let comments = this.state.searchResponse.comments.map(e => {
412 return { type_: "comments", data: e, published: e.comment.published };
414 let posts = this.state.searchResponse.posts.map(e => {
415 return { type_: "posts", data: e, published: e.post.published };
417 let communities = this.state.searchResponse.communities.map(e => {
419 type_: "communities",
421 published: e.community.published,
424 let users = this.state.searchResponse.users.map(e => {
425 return { type_: "users", data: e, published: e.person.published };
428 combined.push(...comments);
429 combined.push(...posts);
430 combined.push(...communities);
431 combined.push(...users);
434 if (this.state.sort == SortType.New) {
435 combined.sort((a, b) => b.published.localeCompare(a.published));
439 ((b.data as CommentView | PostView).counts.score |
440 (b.data as CommunityView).counts.subscribers |
441 (b.data as PersonViewSafe).counts.comment_score) -
442 ((a.data as CommentView | PostView).counts.score |
443 (a.data as CommunityView).counts.subscribers |
444 (a.data as PersonViewSafe).counts.comment_score)
453 {i.type_ == "posts" && (
455 key={(i.data as PostView).post.id}
456 post_view={i.data as PostView}
458 enableDownvotes={this.state.site.enable_downvotes}
459 enableNsfw={this.state.site.enable_nsfw}
462 {i.type_ == "comments" && (
464 key={(i.data as CommentView).comment.id}
465 nodes={[{ comment_view: i.data as CommentView }]}
468 enableDownvotes={this.state.site.enable_downvotes}
471 {i.type_ == "communities" && (
472 <div>{this.communityListing(i.data as CommunityView)}</div>
474 {i.type_ == "users" && (
475 <div>{this.userListing(i.data as PersonViewSafe)}</div>
487 nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
490 enableDownvotes={this.state.site.enable_downvotes}
498 {this.state.searchResponse.posts.map(post => (
504 enableDownvotes={this.state.site.enable_downvotes}
505 enableNsfw={this.state.site.enable_nsfw}
517 {this.state.searchResponse.communities.map(community => (
519 <div class="col-12">{this.communityListing(community)}</div>
526 communityListing(community_view: CommunityView) {
530 <CommunityLink community={community_view.community} />
533 ${i18n.t("number_of_subscribers", {
534 count: community_view.counts.subscribers,
541 userListing(person_view: PersonViewSafe) {
544 <PersonListing person={person_view.person} showApubName />
546 <span>{` - ${i18n.t("number_of_comments", {
547 count: person_view.counts.comment_count,
555 {this.state.searchResponse.users.map(user => (
557 <div class="col-12">{this.userListing(user)}</div>
566 <div class="form-group col-sm-6">
567 <label class="col-form-label" htmlFor="community-filter">
568 {i18n.t("community")}
573 id="community-filter"
574 value={this.state.communityId}
576 <option value="0">{i18n.t("all")}</option>
577 {this.state.communities.map(cv => (
578 <option value={cv.community.id}>{communitySelectName(cv)}</option>
588 <div class="form-group col-sm-6">
589 <label class="col-form-label" htmlFor="creator-filter">
590 {capitalizeFirstLetter(i18n.t("creator"))}
596 value={this.state.creatorId}
598 <option value="0">{i18n.t("all")}</option>
599 {this.state.creator && (
600 <option value={this.state.creator.person.id}>
601 {personSelectName(this.state.creator)}
610 resultsCount(): number {
611 let res = this.state.searchResponse;
614 res.comments.length +
615 res.communities.length +
620 handlePageChange(page: number) {
621 this.updateUrl({ page });
625 let form: SearchForm = {
627 type_: this.state.type_,
628 sort: this.state.sort,
629 listing_type: this.state.listingType,
630 page: this.state.page,
632 auth: authField(false),
634 if (this.state.communityId !== 0) {
635 form.community_id = this.state.communityId;
637 if (this.state.creatorId !== 0) {
638 form.creator_id = this.state.creatorId;
641 if (this.state.q != "") {
642 WebSocketService.Instance.send(wsClient.search(form));
646 setupCommunityFilter() {
648 let selectId: any = document.getElementById("community-filter");
650 this.communityChoices = new Choices(selectId, choicesConfig);
651 this.communityChoices.passedElement.element.addEventListener(
654 this.handleCommunityFilterChange(Number(e.detail.choice.value));
658 this.communityChoices.passedElement.element.addEventListener(
660 debounce(async (e: any) => {
661 let communities = (await fetchCommunities(e.detail.value))
663 let choices = communities.map(cv => communityToChoice(cv));
664 choices.unshift({ value: "0", label: i18n.t("all") });
665 this.communityChoices.setChoices(choices, "value", "label", true);
673 setupCreatorFilter() {
675 let selectId: any = document.getElementById("creator-filter");
677 this.creatorChoices = new Choices(selectId, choicesConfig);
678 this.creatorChoices.passedElement.element.addEventListener(
681 this.handleCreatorFilterChange(Number(e.detail.choice.value));
685 this.creatorChoices.passedElement.element.addEventListener(
687 debounce(async (e: any) => {
688 let creators = (await fetchUsers(e.detail.value)).users;
689 let choices = creators.map(pvs => personToChoice(pvs));
690 choices.unshift({ value: "0", label: i18n.t("all") });
691 this.creatorChoices.setChoices(choices, "value", "label", true);
699 handleSortChange(val: SortType) {
700 this.updateUrl({ sort: val, page: 1 });
703 handleTypeChange(i: Search, event: any) {
705 type_: SearchType[event.target.value],
710 handleListingTypeChange(val: ListingType) {
717 handleCommunityFilterChange(communityId: number) {
724 handleCreatorFilterChange(creatorId: number) {
731 handleSearchSubmit(i: Search, event: any) {
732 event.preventDefault();
734 q: i.state.searchText,
735 type_: i.state.type_,
736 listingType: i.state.listingType,
737 communityId: i.state.communityId,
738 creatorId: i.state.creatorId,
744 handleQChange(i: Search, event: any) {
745 i.setState({ searchText: event.target.value });
748 updateUrl(paramUpdates: UrlParams) {
749 const qStr = paramUpdates.q || this.state.q;
750 const qStrEncoded = encodeURIComponent(qStr);
751 const typeStr = paramUpdates.type_ || this.state.type_;
752 const listingTypeStr = paramUpdates.listingType || this.state.listingType;
753 const sortStr = paramUpdates.sort || this.state.sort;
755 paramUpdates.communityId == 0
757 : paramUpdates.communityId || this.state.communityId;
759 paramUpdates.creatorId == 0
761 : paramUpdates.creatorId || this.state.creatorId;
762 const page = paramUpdates.page || this.state.page;
763 this.props.history.push(
764 `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/listing_type/${listingTypeStr}/community_id/${communityId}/creator_id/${creatorId}/page/${page}`
768 parseMessage(msg: any) {
770 let op = wsUserOp(msg);
772 toast(i18n.t(msg.error), "danger");
774 } else if (op == UserOperation.Search) {
775 let data = wsJsonToRes<SearchResponse>(msg).data;
776 this.state.searchResponse = data;
777 this.state.loading = false;
778 window.scrollTo(0, 0);
779 this.setState(this.state);
780 restoreScrollPosition(this.context);
781 } else if (op == UserOperation.CreateCommentLike) {
782 let data = wsJsonToRes<CommentResponse>(msg).data;
783 createCommentLikeRes(
785 this.state.searchResponse.comments
787 this.setState(this.state);
788 } else if (op == UserOperation.CreatePostLike) {
789 let data = wsJsonToRes<PostResponse>(msg).data;
790 createPostLikeFindRes(data.post_view, this.state.searchResponse.posts);
791 this.setState(this.state);
792 } else if (op == UserOperation.ListCommunities) {
793 let data = wsJsonToRes<ListCommunitiesResponse>(msg).data;
794 this.state.communities = data.communities;
795 this.setState(this.state);
796 this.setupCommunityFilter();