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 { i18n } from "../i18next";
62 import { InitialFetchRequest } from "shared/interfaces";
66 Choices = require("choices.js");
69 interface SearchProps {
73 listingType: ListingType;
79 interface SearchState {
83 listingType: ListingType;
87 searchResponse: SearchResponse;
88 communities: CommunityView[];
89 creator?: PersonViewSafe;
99 listingType?: ListingType;
100 communityId?: number;
105 export class Search extends Component<any, SearchState> {
106 private isoData = setIsoData(this.context);
107 private communityChoices: any;
108 private creatorChoices: any;
109 private subscription: Subscription;
110 private emptyState: SearchState = {
111 q: Search.getSearchQueryFromProps(this.props.match.params.q),
112 type_: Search.getSearchTypeFromProps(this.props.match.params.type),
113 sort: Search.getSortTypeFromProps(this.props.match.params.sort),
114 listingType: Search.getListingTypeFromProps(
115 this.props.match.params.listing_type
117 page: Search.getPageFromProps(this.props.match.params.page),
118 searchText: Search.getSearchQueryFromProps(this.props.match.params.q),
119 communityId: Search.getCommunityIdFromProps(
120 this.props.match.params.community_id
122 creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id),
131 site: this.isoData.site_res.site_view.site,
135 static getSearchQueryFromProps(q: string): string {
136 return decodeURIComponent(q) || "";
139 static getSearchTypeFromProps(type_: string): SearchType {
140 return type_ ? routeSearchTypeToEnum(type_) : SearchType.All;
143 static getSortTypeFromProps(sort: string): SortType {
144 return sort ? routeSortTypeToEnum(sort) : SortType.TopAll;
147 static getListingTypeFromProps(listingType: string): ListingType {
148 return listingType ? routeListingTypeToEnum(listingType) : ListingType.All;
151 static getCommunityIdFromProps(id: string): number {
152 return id ? Number(id) : 0;
155 static getCreatorIdFromProps(id: string): number {
156 return id ? Number(id) : 0;
159 static getPageFromProps(page: string): number {
160 return page ? Number(page) : 1;
163 constructor(props: any, context: any) {
164 super(props, context);
166 this.state = this.emptyState;
167 this.handleSortChange = this.handleSortChange.bind(this);
168 this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
170 this.parseMessage = this.parseMessage.bind(this);
171 this.subscription = wsSubscribe(this.parseMessage);
173 // Only fetch the data if coming from another route
174 if (this.isoData.path == this.context.router.route.match.url) {
175 let singleOrMultipleCommunities = this.isoData.routeData[0];
176 if (singleOrMultipleCommunities.communities) {
177 this.state.communities = this.isoData.routeData[0].communities;
179 this.state.communities = [this.isoData.routeData[0].community_view];
182 let creator = this.isoData.routeData[1];
183 if (creator?.person_view) {
184 this.state.creator = this.isoData.routeData[1].person_view;
186 if (this.state.q != "") {
187 this.state.searchResponse = this.isoData.routeData[2];
188 this.state.loading = false;
193 this.fetchCommunities();
198 componentWillUnmount() {
199 this.subscription.unsubscribe();
200 saveScrollPosition(this.context);
203 componentDidMount() {
204 this.setupCommunityFilter();
205 this.setupCreatorFilter();
208 static getDerivedStateFromProps(props: any): SearchProps {
210 q: Search.getSearchQueryFromProps(props.match.params.q),
211 type_: Search.getSearchTypeFromProps(props.match.params.type),
212 sort: Search.getSortTypeFromProps(props.match.params.sort),
213 listingType: Search.getListingTypeFromProps(
214 props.match.params.listing_type
216 communityId: Search.getCommunityIdFromProps(
217 props.match.params.community_id
219 creatorId: Search.getCreatorIdFromProps(props.match.params.creator_id),
220 page: Search.getPageFromProps(props.match.params.page),
225 let listCommunitiesForm: ListCommunities = {
226 type_: ListingType.All,
227 sort: SortType.TopAll,
229 auth: authField(false),
231 WebSocketService.Instance.send(
232 wsClient.listCommunities(listCommunitiesForm)
236 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
237 let pathSplit = req.path.split("/");
238 let promises: Promise<any>[] = [];
240 let communityId = this.getCommunityIdFromProps(pathSplit[11]);
241 if (communityId !== 0) {
242 let getCommunityForm: GetCommunity = {
245 setOptionalAuth(getCommunityForm, req.auth);
246 promises.push(req.client.getCommunity(getCommunityForm));
248 let listCommunitiesForm: ListCommunities = {
249 type_: ListingType.All,
250 sort: SortType.TopAll,
253 setOptionalAuth(listCommunitiesForm, req.auth);
254 promises.push(req.client.listCommunities(listCommunitiesForm));
257 let creatorId = this.getCreatorIdFromProps(pathSplit[13]);
258 if (creatorId !== 0) {
259 let getCreatorForm: GetPersonDetails = {
260 person_id: creatorId,
262 setOptionalAuth(getCreatorForm, req.auth);
263 promises.push(req.client.getPersonDetails(getCreatorForm));
265 promises.push(Promise.resolve());
268 let form: SearchForm = {
269 q: this.getSearchQueryFromProps(pathSplit[3]),
270 type_: this.getSearchTypeFromProps(pathSplit[5]),
271 sort: this.getSortTypeFromProps(pathSplit[7]),
272 listing_type: this.getListingTypeFromProps(pathSplit[9]),
273 page: this.getPageFromProps(pathSplit[15]),
276 if (communityId !== 0) {
277 form.community_id = communityId;
279 if (creatorId !== 0) {
280 form.creator_id = creatorId;
282 setOptionalAuth(form, req.auth);
285 promises.push(req.client.search(form));
291 componentDidUpdate(_: any, lastState: SearchState) {
293 lastState.q !== this.state.q ||
294 lastState.type_ !== this.state.type_ ||
295 lastState.sort !== this.state.sort ||
296 lastState.listingType !== this.state.listingType ||
297 lastState.communityId !== this.state.communityId ||
298 lastState.creatorId !== this.state.creatorId ||
299 lastState.page !== this.state.page
301 this.setState({ loading: true, searchText: this.state.q });
306 get documentTitle(): string {
308 return `${i18n.t("search")} - ${this.state.q} - ${this.state.site.name}`;
310 return `${i18n.t("search")} - ${this.state.site.name}`;
316 <div class="container">
318 title={this.documentTitle}
319 path={this.context.router.route.match.url}
321 <h5>{i18n.t("search")}</h5>
324 {this.state.type_ == SearchType.All && this.all()}
325 {this.state.type_ == SearchType.Comments && this.comments()}
326 {this.state.type_ == SearchType.Posts && this.posts()}
327 {this.state.type_ == SearchType.Communities && this.communities()}
328 {this.state.type_ == SearchType.Users && this.users()}
329 {this.state.type_ == SearchType.Url && this.posts()}
330 {this.resultsCount() == 0 && <span>{i18n.t("no_results")}</span>}
340 onSubmit={linkEvent(this, this.handleSearchSubmit)}
344 class="form-control mr-2 mb-2"
345 value={this.state.searchText}
346 placeholder={`${i18n.t("search")}...`}
347 aria-label={i18n.t("search")}
348 onInput={linkEvent(this, this.handleQChange)}
352 <button type="submit" class="btn btn-secondary mr-2 mb-2">
353 {this.state.loading ? <Spinner /> : <span>{i18n.t("search")}</span>}
361 <div className="mb-2">
363 value={this.state.type_}
364 onChange={linkEvent(this, this.handleTypeChange)}
365 class="custom-select w-auto mb-2"
366 aria-label={i18n.t("type")}
368 <option disabled aria-hidden="true">
371 <option value={SearchType.All}>{i18n.t("all")}</option>
372 <option value={SearchType.Comments}>{i18n.t("comments")}</option>
373 <option value={SearchType.Posts}>{i18n.t("posts")}</option>
374 <option value={SearchType.Communities}>
375 {i18n.t("communities")}
377 <option value={SearchType.Users}>{i18n.t("users")}</option>
378 <option value={SearchType.Url}>{i18n.t("url")}</option>
382 type_={this.state.listingType}
383 showLocal={showLocal(this.isoData)}
384 onChange={this.handleListingTypeChange}
389 sort={this.state.sort}
390 onChange={this.handleSortChange}
395 <div class="form-row">
396 {this.state.communities.length > 0 && this.communityFilter()}
397 {this.creatorFilter()}
406 data: CommentView | PostView | CommunityView | PersonViewSafe;
409 let comments = this.state.searchResponse.comments.map(e => {
410 return { type_: "comments", data: e, published: e.comment.published };
412 let posts = this.state.searchResponse.posts.map(e => {
413 return { type_: "posts", data: e, published: e.post.published };
415 let communities = this.state.searchResponse.communities.map(e => {
417 type_: "communities",
419 published: e.community.published,
422 let users = this.state.searchResponse.users.map(e => {
423 return { type_: "users", data: e, published: e.person.published };
426 combined.push(...comments);
427 combined.push(...posts);
428 combined.push(...communities);
429 combined.push(...users);
432 if (this.state.sort == SortType.New) {
433 combined.sort((a, b) => b.published.localeCompare(a.published));
437 ((b.data as CommentView | PostView).counts.score |
438 (b.data as CommunityView).counts.subscribers |
439 (b.data as PersonViewSafe).counts.comment_score) -
440 ((a.data as CommentView | PostView).counts.score |
441 (a.data as CommunityView).counts.subscribers |
442 (a.data as PersonViewSafe).counts.comment_score)
451 {i.type_ == "posts" && (
453 key={(i.data as PostView).post.id}
454 post_view={i.data as PostView}
456 enableDownvotes={this.state.site.enable_downvotes}
457 enableNsfw={this.state.site.enable_nsfw}
460 {i.type_ == "comments" && (
462 key={(i.data as CommentView).comment.id}
463 nodes={[{ comment_view: i.data as CommentView }]}
466 enableDownvotes={this.state.site.enable_downvotes}
469 {i.type_ == "communities" && (
470 <div>{this.communityListing(i.data as CommunityView)}</div>
472 {i.type_ == "users" && (
473 <div>{this.userListing(i.data as PersonViewSafe)}</div>
485 nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
488 enableDownvotes={this.state.site.enable_downvotes}
496 {this.state.searchResponse.posts.map(post => (
502 enableDownvotes={this.state.site.enable_downvotes}
503 enableNsfw={this.state.site.enable_nsfw}
515 {this.state.searchResponse.communities.map(community => (
517 <div class="col-12">{this.communityListing(community)}</div>
524 communityListing(community_view: CommunityView) {
528 <CommunityLink community={community_view.community} />
531 ${i18n.t("number_of_subscribers", {
532 count: community_view.counts.subscribers,
539 userListing(person_view: PersonViewSafe) {
542 <PersonListing person={person_view.person} showApubName />
544 <span>{` - ${i18n.t("number_of_comments", {
545 count: person_view.counts.comment_count,
553 {this.state.searchResponse.users.map(user => (
555 <div class="col-12">{this.userListing(user)}</div>
564 <div class="form-group col-sm-6">
565 <label class="col-form-label" htmlFor="community-filter">
566 {i18n.t("community")}
571 id="community-filter"
572 value={this.state.communityId}
574 <option value="0">{i18n.t("all")}</option>
575 {this.state.communities.map(cv => (
576 <option value={cv.community.id}>{communitySelectName(cv)}</option>
586 <div class="form-group col-sm-6">
587 <label class="col-form-label" htmlFor="creator-filter">
588 {capitalizeFirstLetter(i18n.t("creator"))}
594 value={this.state.creatorId}
596 <option value="0">{i18n.t("all")}</option>
597 {this.state.creator && (
598 <option value={this.state.creator.person.id}>
599 {personSelectName(this.state.creator)}
611 {this.state.page > 1 && (
613 class="btn btn-secondary mr-1"
614 onClick={linkEvent(this, this.prevPage)}
620 {this.resultsCount() > 0 && (
622 class="btn btn-secondary"
623 onClick={linkEvent(this, this.nextPage)}
632 resultsCount(): number {
633 let res = this.state.searchResponse;
636 res.comments.length +
637 res.communities.length +
642 nextPage(i: Search) {
643 i.updateUrl({ page: i.state.page + 1 });
646 prevPage(i: Search) {
647 i.updateUrl({ page: i.state.page - 1 });
651 let form: SearchForm = {
653 type_: this.state.type_,
654 sort: this.state.sort,
655 listing_type: this.state.listingType,
656 page: this.state.page,
658 auth: authField(false),
660 if (this.state.communityId !== 0) {
661 form.community_id = this.state.communityId;
663 if (this.state.creatorId !== 0) {
664 form.creator_id = this.state.creatorId;
667 if (this.state.q != "") {
668 WebSocketService.Instance.send(wsClient.search(form));
672 setupCommunityFilter() {
674 let selectId: any = document.getElementById("community-filter");
676 this.communityChoices = new Choices(selectId, choicesConfig);
677 this.communityChoices.passedElement.element.addEventListener(
680 this.handleCommunityFilterChange(Number(e.detail.choice.value));
684 this.communityChoices.passedElement.element.addEventListener(
686 debounce(async (e: any) => {
687 let communities = (await fetchCommunities(e.detail.value))
689 let choices = communities.map(cv => communityToChoice(cv));
690 choices.unshift({ value: "0", label: i18n.t("all") });
691 this.communityChoices.setChoices(choices, "value", "label", true);
699 setupCreatorFilter() {
701 let selectId: any = document.getElementById("creator-filter");
703 this.creatorChoices = new Choices(selectId, choicesConfig);
704 this.creatorChoices.passedElement.element.addEventListener(
707 this.handleCreatorFilterChange(Number(e.detail.choice.value));
711 this.creatorChoices.passedElement.element.addEventListener(
713 debounce(async (e: any) => {
714 let creators = (await fetchUsers(e.detail.value)).users;
715 let choices = creators.map(pvs => personToChoice(pvs));
716 choices.unshift({ value: "0", label: i18n.t("all") });
717 this.creatorChoices.setChoices(choices, "value", "label", true);
725 handleSortChange(val: SortType) {
726 this.updateUrl({ sort: val, page: 1 });
729 handleTypeChange(i: Search, event: any) {
731 type_: SearchType[event.target.value],
736 handleListingTypeChange(val: ListingType) {
743 handleCommunityFilterChange(communityId: number) {
750 handleCreatorFilterChange(creatorId: number) {
757 handleSearchSubmit(i: Search, event: any) {
758 event.preventDefault();
760 q: i.state.searchText,
761 type_: i.state.type_,
762 listingType: i.state.listingType,
763 communityId: i.state.communityId,
764 creatorId: i.state.creatorId,
770 handleQChange(i: Search, event: any) {
771 i.setState({ searchText: event.target.value });
774 updateUrl(paramUpdates: UrlParams) {
775 const qStr = paramUpdates.q || this.state.q;
776 const qStrEncoded = encodeURIComponent(qStr);
777 const typeStr = paramUpdates.type_ || this.state.type_;
778 const listingTypeStr = paramUpdates.listingType || this.state.listingType;
779 const sortStr = paramUpdates.sort || this.state.sort;
781 paramUpdates.communityId == 0
783 : paramUpdates.communityId || this.state.communityId;
785 paramUpdates.creatorId == 0
787 : paramUpdates.creatorId || this.state.creatorId;
788 const page = paramUpdates.page || this.state.page;
789 this.props.history.push(
790 `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/listing_type/${listingTypeStr}/community_id/${communityId}/creator_id/${creatorId}/page/${page}`
794 parseMessage(msg: any) {
796 let op = wsUserOp(msg);
798 toast(i18n.t(msg.error), "danger");
800 } else if (op == UserOperation.Search) {
801 let data = wsJsonToRes<SearchResponse>(msg).data;
802 this.state.searchResponse = data;
803 this.state.loading = false;
804 window.scrollTo(0, 0);
805 this.setState(this.state);
806 restoreScrollPosition(this.context);
807 } else if (op == UserOperation.CreateCommentLike) {
808 let data = wsJsonToRes<CommentResponse>(msg).data;
809 createCommentLikeRes(
811 this.state.searchResponse.comments
813 this.setState(this.state);
814 } else if (op == UserOperation.CreatePostLike) {
815 let data = wsJsonToRes<PostResponse>(msg).data;
816 createPostLikeFindRes(data.post_view, this.state.searchResponse.posts);
817 this.setState(this.state);
818 } else if (op == UserOperation.ListCommunities) {
819 let data = wsJsonToRes<ListCommunitiesResponse>(msg).data;
820 this.state.communities = data.communities;
821 this.setState(this.state);
822 this.setupCommunityFilter();