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,
50 capitalizeFirstLetter,
52 import { PostListing } from "./post-listing";
53 import { HtmlTags } from "./html-tags";
54 import { Spinner } from "./icon";
55 import { PersonListing } from "./person-listing";
56 import { CommunityLink } from "./community-link";
57 import { SortSelect } from "./sort-select";
58 import { ListingTypeSelect } from "./listing-type-select";
59 import { CommentNodes } from "./comment-nodes";
60 import { i18n } from "../i18next";
61 import { InitialFetchRequest } from "shared/interfaces";
65 Choices = require("choices.js");
68 interface SearchProps {
72 listingType: ListingType;
78 interface SearchState {
82 listingType: ListingType;
86 searchResponse: SearchResponse;
87 communities: CommunityView[];
88 creator?: PersonViewSafe;
98 listingType?: ListingType;
104 export class Search extends Component<any, SearchState> {
105 private isoData = setIsoData(this.context);
106 private communityChoices: any;
107 private creatorChoices: any;
108 private subscription: Subscription;
109 private emptyState: SearchState = {
110 q: Search.getSearchQueryFromProps(this.props.match.params.q),
111 type_: Search.getSearchTypeFromProps(this.props.match.params.type),
112 sort: Search.getSortTypeFromProps(this.props.match.params.sort),
113 listingType: Search.getListingTypeFromProps(
114 this.props.match.params.listing_type
116 page: Search.getPageFromProps(this.props.match.params.page),
117 searchText: Search.getSearchQueryFromProps(this.props.match.params.q),
118 communityId: Search.getCommunityIdFromProps(
119 this.props.match.params.community_id
121 creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id),
130 site: this.isoData.site_res.site_view.site,
134 static getSearchQueryFromProps(q: string): string {
135 return decodeURIComponent(q) || "";
138 static getSearchTypeFromProps(type_: string): SearchType {
139 return type_ ? routeSearchTypeToEnum(type_) : SearchType.All;
142 static getSortTypeFromProps(sort: string): SortType {
143 return sort ? routeSortTypeToEnum(sort) : SortType.TopAll;
146 static getListingTypeFromProps(listingType: string): ListingType {
147 return listingType ? routeListingTypeToEnum(listingType) : ListingType.All;
150 static getCommunityIdFromProps(id: string): number {
151 return id ? Number(id) : 0;
154 static getCreatorIdFromProps(id: string): number {
155 return id ? Number(id) : 0;
158 static getPageFromProps(page: string): number {
159 return page ? Number(page) : 1;
162 constructor(props: any, context: any) {
163 super(props, context);
165 this.state = this.emptyState;
166 this.handleSortChange = this.handleSortChange.bind(this);
167 this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
169 this.parseMessage = this.parseMessage.bind(this);
170 this.subscription = wsSubscribe(this.parseMessage);
172 // Only fetch the data if coming from another route
173 if (this.isoData.path == this.context.router.route.match.url) {
174 let singleOrMultipleCommunities = this.isoData.routeData[0];
175 if (singleOrMultipleCommunities.communities) {
176 this.state.communities = this.isoData.routeData[0].communities;
178 this.state.communities = [this.isoData.routeData[0].community_view];
181 let creator = this.isoData.routeData[1];
182 if (creator?.person_view) {
183 this.state.creator = this.isoData.routeData[1].person_view;
185 if (this.state.q != "") {
186 this.state.searchResponse = this.isoData.routeData[2];
187 this.state.loading = false;
192 this.fetchCommunities();
197 componentWillUnmount() {
198 this.subscription.unsubscribe();
199 saveScrollPosition(this.context);
202 componentDidMount() {
203 this.setupCommunityFilter();
204 this.setupCreatorFilter();
207 static getDerivedStateFromProps(props: any): SearchProps {
209 q: Search.getSearchQueryFromProps(props.match.params.q),
210 type_: Search.getSearchTypeFromProps(props.match.params.type),
211 sort: Search.getSortTypeFromProps(props.match.params.sort),
212 listingType: Search.getListingTypeFromProps(
213 props.match.params.listing_type
215 communityId: Search.getCommunityIdFromProps(
216 props.match.params.community_id
218 creatorId: Search.getCreatorIdFromProps(props.match.params.creator_id),
219 page: Search.getPageFromProps(props.match.params.page),
224 let listCommunitiesForm: ListCommunities = {
225 type_: ListingType.All,
226 sort: SortType.TopAll,
228 auth: authField(false),
230 WebSocketService.Instance.send(
231 wsClient.listCommunities(listCommunitiesForm)
235 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
236 let pathSplit = req.path.split("/");
237 let promises: Promise<any>[] = [];
239 let communityId = this.getCommunityIdFromProps(pathSplit[11]);
240 if (communityId !== 0) {
241 let getCommunityForm: GetCommunity = {
244 setOptionalAuth(getCommunityForm, req.auth);
245 promises.push(req.client.getCommunity(getCommunityForm));
247 let listCommunitiesForm: ListCommunities = {
248 type_: ListingType.All,
249 sort: SortType.TopAll,
252 setOptionalAuth(listCommunitiesForm, req.auth);
253 promises.push(req.client.listCommunities(listCommunitiesForm));
256 let creatorId = this.getCreatorIdFromProps(pathSplit[13]);
257 if (creatorId !== 0) {
258 let getCreatorForm: GetPersonDetails = {
259 person_id: creatorId,
261 setOptionalAuth(getCreatorForm, req.auth);
262 promises.push(req.client.getPersonDetails(getCreatorForm));
264 promises.push(Promise.resolve());
267 let form: SearchForm = {
268 q: this.getSearchQueryFromProps(pathSplit[3]),
269 type_: this.getSearchTypeFromProps(pathSplit[5]),
270 sort: this.getSortTypeFromProps(pathSplit[7]),
271 listing_type: this.getListingTypeFromProps(pathSplit[9]),
272 page: this.getPageFromProps(pathSplit[15]),
275 if (communityId !== 0) {
276 form.community_id = communityId;
278 if (creatorId !== 0) {
279 form.creator_id = creatorId;
281 setOptionalAuth(form, req.auth);
284 promises.push(req.client.search(form));
290 componentDidUpdate(_: any, lastState: SearchState) {
292 lastState.q !== this.state.q ||
293 lastState.type_ !== this.state.type_ ||
294 lastState.sort !== this.state.sort ||
295 lastState.listingType !== this.state.listingType ||
296 lastState.communityId !== this.state.communityId ||
297 lastState.creatorId !== this.state.creatorId ||
298 lastState.page !== this.state.page
300 this.setState({ loading: true, searchText: this.state.q });
305 get documentTitle(): string {
307 return `${i18n.t("search")} - ${this.state.q} - ${this.state.site.name}`;
309 return `${i18n.t("search")} - ${this.state.site.name}`;
315 <div class="container">
317 title={this.documentTitle}
318 path={this.context.router.route.match.url}
320 <h5>{i18n.t("search")}</h5>
323 {this.state.type_ == SearchType.All && this.all()}
324 {this.state.type_ == SearchType.Comments && this.comments()}
325 {this.state.type_ == SearchType.Posts && this.posts()}
326 {this.state.type_ == SearchType.Communities && this.communities()}
327 {this.state.type_ == SearchType.Users && this.users()}
328 {this.resultsCount() == 0 && <span>{i18n.t("no_results")}</span>}
338 onSubmit={linkEvent(this, this.handleSearchSubmit)}
342 class="form-control mr-2 mb-2"
343 value={this.state.searchText}
344 placeholder={`${i18n.t("search")}...`}
345 aria-label={i18n.t("search")}
346 onInput={linkEvent(this, this.handleQChange)}
350 <button type="submit" class="btn btn-secondary mr-2 mb-2">
351 {this.state.loading ? <Spinner /> : <span>{i18n.t("search")}</span>}
359 <div className="mb-2">
361 value={this.state.type_}
362 onChange={linkEvent(this, this.handleTypeChange)}
363 class="custom-select w-auto mb-2"
364 aria-label={i18n.t("type")}
366 <option disabled aria-hidden="true">
369 <option value={SearchType.All}>{i18n.t("all")}</option>
370 <option value={SearchType.Comments}>{i18n.t("comments")}</option>
371 <option value={SearchType.Posts}>{i18n.t("posts")}</option>
372 <option value={SearchType.Communities}>
373 {i18n.t("communities")}
375 <option value={SearchType.Users}>{i18n.t("users")}</option>
379 type_={this.state.listingType}
380 showLocal={showLocal(this.isoData)}
381 onChange={this.handleListingTypeChange}
386 sort={this.state.sort}
387 onChange={this.handleSortChange}
392 <div class="form-row">
393 {this.state.communities.length > 0 && this.communityFilter()}
394 {this.creatorFilter()}
403 data: CommentView | PostView | CommunityView | PersonViewSafe;
406 let comments = this.state.searchResponse.comments.map(e => {
407 return { type_: "comments", data: e, published: e.comment.published };
409 let posts = this.state.searchResponse.posts.map(e => {
410 return { type_: "posts", data: e, published: e.post.published };
412 let communities = this.state.searchResponse.communities.map(e => {
414 type_: "communities",
416 published: e.community.published,
419 let users = this.state.searchResponse.users.map(e => {
420 return { type_: "users", data: e, published: e.person.published };
423 combined.push(...comments);
424 combined.push(...posts);
425 combined.push(...communities);
426 combined.push(...users);
429 if (this.state.sort == SortType.New) {
430 combined.sort((a, b) => b.published.localeCompare(a.published));
434 ((b.data as CommentView | PostView).counts.score |
435 (b.data as CommunityView).counts.subscribers |
436 (b.data as PersonViewSafe).counts.comment_score) -
437 ((a.data as CommentView | PostView).counts.score |
438 (a.data as CommunityView).counts.subscribers |
439 (a.data as PersonViewSafe).counts.comment_score)
448 {i.type_ == "posts" && (
450 key={(i.data as PostView).post.id}
451 post_view={i.data as PostView}
453 enableDownvotes={this.state.site.enable_downvotes}
454 enableNsfw={this.state.site.enable_nsfw}
457 {i.type_ == "comments" && (
459 key={(i.data as CommentView).comment.id}
460 nodes={[{ comment_view: i.data as CommentView }]}
463 enableDownvotes={this.state.site.enable_downvotes}
466 {i.type_ == "communities" && (
467 <div>{this.communityListing(i.data as CommunityView)}</div>
469 {i.type_ == "users" && (
470 <div>{this.userListing(i.data as PersonViewSafe)}</div>
482 nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
485 enableDownvotes={this.state.site.enable_downvotes}
493 {this.state.searchResponse.posts.map(post => (
499 enableDownvotes={this.state.site.enable_downvotes}
500 enableNsfw={this.state.site.enable_nsfw}
512 {this.state.searchResponse.communities.map(community => (
514 <div class="col-12">{this.communityListing(community)}</div>
521 communityListing(community_view: CommunityView) {
525 <CommunityLink community={community_view.community} />
528 ${i18n.t("number_of_subscribers", {
529 count: community_view.counts.subscribers,
536 userListing(person_view: PersonViewSafe) {
539 <PersonListing person={person_view.person} showApubName />
541 <span>{` - ${i18n.t("number_of_comments", {
542 count: person_view.counts.comment_count,
550 {this.state.searchResponse.users.map(user => (
552 <div class="col-12">{this.userListing(user)}</div>
561 <div class="form-group col-sm-6">
562 <label class="col-form-label" htmlFor="community-filter">
563 {i18n.t("community")}
568 id="community-filter"
569 value={this.state.communityId}
571 <option value="0">{i18n.t("all")}</option>
572 {this.state.communities.map(cv => (
573 <option value={cv.community.id}>
576 : `${hostname(cv.community.actor_id)}/${cv.community.name}`}
587 <div class="form-group col-sm-6">
588 <label class="col-form-label" htmlFor="creator-filter">
589 {capitalizeFirstLetter(i18n.t("creator"))}
595 value={this.state.creatorId}
597 <option value="0">{i18n.t("all")}</option>
598 {this.state.creator && (
599 <option value={this.state.creator.person.id}>
600 {this.state.creator.person.local
601 ? this.state.creator.person.name
602 : `${hostname(this.state.creator.person.actor_id)}/${
603 this.state.creator.person.name
616 {this.state.page > 1 && (
618 class="btn btn-secondary mr-1"
619 onClick={linkEvent(this, this.prevPage)}
625 {this.resultsCount() > 0 && (
627 class="btn btn-secondary"
628 onClick={linkEvent(this, this.nextPage)}
637 resultsCount(): number {
638 let res = this.state.searchResponse;
641 res.comments.length +
642 res.communities.length +
647 nextPage(i: Search) {
648 i.updateUrl({ page: i.state.page + 1 });
651 prevPage(i: Search) {
652 i.updateUrl({ page: i.state.page - 1 });
656 let form: SearchForm = {
658 type_: this.state.type_,
659 sort: this.state.sort,
660 listing_type: this.state.listingType,
661 page: this.state.page,
663 auth: authField(false),
665 if (this.state.communityId !== 0) {
666 form.community_id = this.state.communityId;
668 if (this.state.creatorId !== 0) {
669 form.creator_id = this.state.creatorId;
672 if (this.state.q != "") {
673 WebSocketService.Instance.send(wsClient.search(form));
677 setupCommunityFilter() {
679 let selectId: any = document.getElementById("community-filter");
681 this.communityChoices = new Choices(selectId, choicesConfig);
682 this.communityChoices.passedElement.element.addEventListener(
685 this.handleCommunityFilterChange(Number(e.detail.choice.value));
689 this.communityChoices.passedElement.element.addEventListener(
691 debounce(async (e: any) => {
692 let communities = (await fetchCommunities(e.detail.value))
694 let choices = communities.map(cv => communityToChoice(cv));
695 choices.unshift({ value: "0", label: i18n.t("all") });
696 this.communityChoices.setChoices(choices, "value", "label", true);
704 setupCreatorFilter() {
706 let selectId: any = document.getElementById("creator-filter");
708 this.creatorChoices = new Choices(selectId, choicesConfig);
709 this.creatorChoices.passedElement.element.addEventListener(
712 this.handleCreatorFilterChange(Number(e.detail.choice.value));
716 this.creatorChoices.passedElement.element.addEventListener(
718 debounce(async (e: any) => {
719 let creators = (await fetchUsers(e.detail.value)).users;
720 let choices = creators.map(pvs => personToChoice(pvs));
721 choices.unshift({ value: "0", label: i18n.t("all") });
722 this.creatorChoices.setChoices(choices, "value", "label", true);
730 handleSortChange(val: SortType) {
731 this.updateUrl({ sort: val, page: 1 });
734 handleTypeChange(i: Search, event: any) {
736 type_: SearchType[event.target.value],
741 handleListingTypeChange(val: ListingType) {
748 handleCommunityFilterChange(communityId: number) {
755 handleCreatorFilterChange(creatorId: number) {
762 handleSearchSubmit(i: Search, event: any) {
763 event.preventDefault();
765 q: i.state.searchText,
766 type_: i.state.type_,
767 listingType: i.state.listingType,
768 communityId: i.state.communityId,
769 creatorId: i.state.creatorId,
775 handleQChange(i: Search, event: any) {
776 i.setState({ searchText: event.target.value });
779 updateUrl(paramUpdates: UrlParams) {
780 const qStr = paramUpdates.q || this.state.q;
781 const qStrEncoded = encodeURIComponent(qStr);
782 const typeStr = paramUpdates.type_ || this.state.type_;
783 const listingTypeStr = paramUpdates.listingType || this.state.listingType;
784 const sortStr = paramUpdates.sort || this.state.sort;
786 paramUpdates.communityId == 0
788 : paramUpdates.communityId || this.state.communityId;
790 paramUpdates.creatorId == 0
792 : paramUpdates.creatorId || this.state.creatorId;
793 const page = paramUpdates.page || this.state.page;
794 this.props.history.push(
795 `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/listing_type/${listingTypeStr}/community_id/${communityId}/creator_id/${creatorId}/page/${page}`
799 parseMessage(msg: any) {
801 let op = wsUserOp(msg);
803 toast(i18n.t(msg.error), "danger");
805 } else if (op == UserOperation.Search) {
806 let data = wsJsonToRes<SearchResponse>(msg).data;
807 this.state.searchResponse = data;
808 this.state.loading = false;
809 window.scrollTo(0, 0);
810 this.setState(this.state);
811 restoreScrollPosition(this.context);
812 } else if (op == UserOperation.CreateCommentLike) {
813 let data = wsJsonToRes<CommentResponse>(msg).data;
814 createCommentLikeRes(
816 this.state.searchResponse.comments
818 this.setState(this.state);
819 } else if (op == UserOperation.CreatePostLike) {
820 let data = wsJsonToRes<PostResponse>(msg).data;
821 createPostLikeFindRes(data.post_view, this.state.searchResponse.posts);
822 this.setState(this.state);
823 } else if (op == UserOperation.ListCommunities) {
824 let data = wsJsonToRes<ListCommunitiesResponse>(msg).data;
825 this.state.communities = data.communities;
826 this.setState(this.state);
827 this.setupCommunityFilter();