+import type { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import {
CommentResponse,
import { WebSocketService } from "../services";
import {
capitalizeFirstLetter,
- choicesConfig,
+ Choice,
commentsToFlatNodes,
- communitySelectName,
communityToChoice,
createCommentLikeRes,
createPostLikeFindRes,
fetchCommunities,
fetchLimit,
fetchUsers,
- isBrowser,
+ getIdFromString,
+ getPageFromString,
+ getQueryParams,
+ getQueryString,
+ getUpdatedSearchId,
myAuth,
numToSI,
- personSelectName,
personToChoice,
- pushNotNull,
+ QueryParams,
restoreScrollPosition,
routeListingTypeToEnum,
routeSearchTypeToEnum,
import { Spinner } from "./common/icon";
import { ListingTypeSelect } from "./common/listing-type-select";
import { Paginator } from "./common/paginator";
+import { SearchableSelect } from "./common/searchable-select";
import { SortSelect } from "./common/sort-select";
import { CommunityLink } from "./community/community-link";
import { PersonListing } from "./person/person-listing";
import { PostListing } from "./post/post-listing";
-var Choices: any;
-if (isBrowser()) {
- Choices = require("choices.js");
-}
-
interface SearchProps {
q?: string;
- type_: SearchType;
+ type: SearchType;
sort: SortType;
listingType: ListingType;
- communityId: number;
- creatorId: number;
+ communityId?: number | null;
+ creatorId?: number | null;
page: number;
}
+type FilterType = "creator" | "community";
+
interface SearchState {
- q?: string;
- type_: SearchType;
- sort: SortType;
- listingType: ListingType;
- communityId: number;
- creatorId: number;
- page: number;
searchResponse?: SearchResponse;
communities: CommunityView[];
creatorDetails?: GetPersonDetailsResponse;
- loading: boolean;
+ searchLoading: boolean;
+ searchCommunitiesLoading: boolean;
+ searchCreatorLoading: boolean;
siteRes: GetSiteResponse;
searchText?: string;
resolveObjectResponse?: ResolveObjectResponse;
-}
-
-interface UrlParams {
- q?: string;
- type_?: SearchType;
- sort?: SortType;
- listingType?: ListingType;
- communityId?: number;
- creatorId?: number;
- page?: number;
+ communitySearchOptions: Choice[];
+ creatorSearchOptions: Choice[];
}
interface Combined {
published: string;
}
+const defaultSearchType = SearchType.All;
+const defaultSortType = SortType.TopAll;
+const defaultListingType = ListingType.All;
+
+const searchTypes = [
+ SearchType.All,
+ SearchType.Comments,
+ SearchType.Posts,
+ SearchType.Communities,
+ SearchType.Users,
+ SearchType.Url,
+];
+
+const getSearchQueryParams = () =>
+ getQueryParams<SearchProps>({
+ q: getSearchQueryFromQuery,
+ type: getSearchTypeFromQuery,
+ sort: getSortTypeFromQuery,
+ listingType: getListingTypeFromQuery,
+ communityId: getIdFromString,
+ creatorId: getIdFromString,
+ page: getPageFromString,
+ });
+
+const getSearchQueryFromQuery = (q?: string): string | undefined =>
+ q ? decodeURIComponent(q) : undefined;
+
+const getSearchTypeFromQuery = (type_?: string): SearchType =>
+ routeSearchTypeToEnum(type_ ?? "", defaultSearchType);
+
+const getSortTypeFromQuery = (sort?: string): SortType =>
+ routeSortTypeToEnum(sort ?? "", defaultSortType);
+
+const getListingTypeFromQuery = (listingType?: string): ListingType =>
+ routeListingTypeToEnum(listingType ?? "", defaultListingType);
+
+const postViewToCombined = (data: PostView): Combined => ({
+ type_: "posts",
+ data,
+ published: data.post.published,
+});
+
+const commentViewToCombined = (data: CommentView): Combined => ({
+ type_: "comments",
+ data,
+ published: data.comment.published,
+});
+
+const communityViewToCombined = (data: CommunityView): Combined => ({
+ type_: "communities",
+ data,
+ published: data.community.published,
+});
+
+const personViewSafeToCombined = (data: PersonViewSafe): Combined => ({
+ type_: "users",
+ data,
+ published: data.person.published,
+});
+
+const Filter = ({
+ filterType,
+ options,
+ onChange,
+ onSearch,
+ value,
+ loading,
+}: {
+ filterType: FilterType;
+ options: Choice[];
+ onSearch: (text: string) => void;
+ onChange: (choice: Choice) => void;
+ value?: number | null;
+ loading: boolean;
+}) => {
+ return (
+ <div className="form-group col-sm-6">
+ <label className="col-form-label" htmlFor={`${filterType}-filter`}>
+ {capitalizeFirstLetter(i18n.t(filterType))}
+ </label>
+ <SearchableSelect
+ id={`${filterType}-filter`}
+ options={[
+ {
+ label: i18n.t("all"),
+ value: "0",
+ },
+ ].concat(options)}
+ value={value ?? 0}
+ onSearch={onSearch}
+ onChange={onChange}
+ loading={loading}
+ />
+ </div>
+ );
+};
+
+const communityListing = ({
+ community,
+ counts: { subscribers },
+}: CommunityView) =>
+ getListing(
+ <CommunityLink community={community} />,
+ subscribers,
+ "number_of_subscribers"
+ );
+
+const personListing = ({ person, counts: { comment_count } }: PersonViewSafe) =>
+ getListing(
+ <PersonListing person={person} showApubName />,
+ comment_count,
+ "number_of_comments"
+ );
+
+const getListing = (
+ listing: JSX.Element,
+ count: number,
+ translationKey: "number_of_comments" | "number_of_subscribers"
+) => (
+ <>
+ <span>{listing}</span>
+ <span>{` - ${i18n.t(translationKey, {
+ count,
+ formattedCount: numToSI(count),
+ })}`}</span>
+ </>
+);
+
export class Search extends Component<any, SearchState> {
private isoData = setIsoData(this.context);
- private communityChoices: any;
- private creatorChoices: any;
private subscription?: Subscription;
state: SearchState = {
- q: Search.getSearchQueryFromProps(this.props.match.params.q),
- type_: Search.getSearchTypeFromProps(this.props.match.params.type),
- sort: Search.getSortTypeFromProps(this.props.match.params.sort),
- listingType: Search.getListingTypeFromProps(
- this.props.match.params.listing_type
- ),
- page: Search.getPageFromProps(this.props.match.params.page),
- searchText: Search.getSearchQueryFromProps(this.props.match.params.q),
- communityId: Search.getCommunityIdFromProps(
- this.props.match.params.community_id
- ),
- creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id),
- loading: false,
+ searchLoading: false,
siteRes: this.isoData.site_res,
communities: [],
+ searchCommunitiesLoading: false,
+ searchCreatorLoading: false,
+ creatorSearchOptions: [],
+ communitySearchOptions: [],
};
- static getSearchQueryFromProps(q?: string): string | undefined {
- return q ? decodeURIComponent(q) : undefined;
- }
-
- static getSearchTypeFromProps(type_: string): SearchType {
- return type_ ? routeSearchTypeToEnum(type_) : SearchType.All;
- }
-
- static getSortTypeFromProps(sort: string): SortType {
- return sort ? routeSortTypeToEnum(sort) : SortType.TopAll;
- }
-
- static getListingTypeFromProps(listingType: string): ListingType {
- return listingType ? routeListingTypeToEnum(listingType) : ListingType.All;
- }
-
- static getCommunityIdFromProps(id: string): number {
- return id ? Number(id) : 0;
- }
-
- static getCreatorIdFromProps(id: string): number {
- return id ? Number(id) : 0;
- }
-
- static getPageFromProps(page: string): number {
- return page ? Number(page) : 1;
- }
-
constructor(props: any, context: any) {
super(props, context);
this.handleSortChange = this.handleSortChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
+ this.handleCommunityFilterChange =
+ this.handleCommunityFilterChange.bind(this);
+ this.handleCreatorFilterChange = this.handleCreatorFilterChange.bind(this);
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
+ const { q } = getSearchQueryParams();
+
+ this.state = {
+ ...this.state,
+ searchText: q,
+ };
+
// Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
- let communityRes = this.isoData.routeData[0] as
+ if (this.isoData.path === this.context.router.route.match.url) {
+ const communityRes = this.isoData.routeData[0] as
| GetCommunityResponse
| undefined;
- let communitiesRes = this.isoData.routeData[1] as
+ const communitiesRes = this.isoData.routeData[1] as
| ListCommunitiesResponse
| undefined;
// This can be single or multiple communities given
communities: communitiesRes.communities,
};
}
-
if (communityRes) {
this.state = {
...this.state,
communities: [communityRes.community_view],
+ communitySearchOptions: [
+ communityToChoice(communityRes.community_view),
+ ],
};
}
+ const creatorRes = this.isoData.routeData[2] as GetPersonDetailsResponse;
+
this.state = {
...this.state,
- creatorDetails: this.isoData.routeData[2] as GetPersonDetailsResponse,
+ creatorDetails: creatorRes,
+ creatorSearchOptions: creatorRes
+ ? [personToChoice(creatorRes.person_view)]
+ : [],
};
- if (this.state.q != "") {
+ if (q !== "") {
this.state = {
...this.state,
searchResponse: this.isoData.routeData[3] as SearchResponse,
resolveObjectResponse: this.isoData
.routeData[4] as ResolveObjectResponse,
- loading: false,
+ searchLoading: false,
};
} else {
this.search();
}
} else {
- this.fetchCommunities();
+ const listCommunitiesForm: ListCommunities = {
+ type_: defaultListingType,
+ sort: defaultSortType,
+ limit: fetchLimit,
+ auth: myAuth(false),
+ };
- if (this.state.q) {
+ WebSocketService.Instance.send(
+ wsClient.listCommunities(listCommunitiesForm)
+ );
+
+ if (q) {
this.search();
}
}
saveScrollPosition(this.context);
}
- componentDidMount() {
- this.setupCommunityFilter();
- this.setupCreatorFilter();
- }
+ static fetchInitialData({
+ client,
+ auth,
+ query: { communityId, creatorId, q, type, sort, listingType, page },
+ }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<any>[] {
+ const promises: Promise<any>[] = [];
- static getDerivedStateFromProps(
- props: any,
- prevState: SearchState
- ): SearchProps {
- return {
- q: Search.getSearchQueryFromProps(props.match.params.q),
- type_:
- prevState.type_ ??
- Search.getSearchTypeFromProps(props.match.params.type),
- sort:
- prevState.sort ?? Search.getSortTypeFromProps(props.match.params.sort),
- listingType:
- prevState.listingType ??
- Search.getListingTypeFromProps(props.match.params.listing_type),
- communityId: Search.getCommunityIdFromProps(
- props.match.params.community_id
- ),
- creatorId: Search.getCreatorIdFromProps(props.match.params.creator_id),
- page: Search.getPageFromProps(props.match.params.page),
- };
- }
-
- fetchCommunities() {
- let listCommunitiesForm: ListCommunities = {
- type_: ListingType.All,
- sort: SortType.TopAll,
- limit: fetchLimit,
- auth: myAuth(false),
- };
- WebSocketService.Instance.send(
- wsClient.listCommunities(listCommunitiesForm)
- );
- }
-
- static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
- let pathSplit = req.path.split("/");
- let promises: Promise<any>[] = [];
- let auth = req.auth;
-
- let communityId = this.getCommunityIdFromProps(pathSplit[11]);
- let community_id = communityId == 0 ? undefined : communityId;
+ const community_id = getIdFromString(communityId);
if (community_id) {
- let getCommunityForm: GetCommunity = {
+ const getCommunityForm: GetCommunity = {
id: community_id,
auth,
};
- promises.push(req.client.getCommunity(getCommunityForm));
+ promises.push(client.getCommunity(getCommunityForm));
promises.push(Promise.resolve());
} else {
- let listCommunitiesForm: ListCommunities = {
- type_: ListingType.All,
- sort: SortType.TopAll,
+ const listCommunitiesForm: ListCommunities = {
+ type_: defaultListingType,
+ sort: defaultSortType,
limit: fetchLimit,
- auth: req.auth,
+ auth,
};
promises.push(Promise.resolve());
- promises.push(req.client.listCommunities(listCommunitiesForm));
+ promises.push(client.listCommunities(listCommunitiesForm));
}
- let creatorId = this.getCreatorIdFromProps(pathSplit[13]);
- let creator_id = creatorId == 0 ? undefined : creatorId;
+ const creator_id = getIdFromString(creatorId);
if (creator_id) {
- let getCreatorForm: GetPersonDetails = {
+ const getCreatorForm: GetPersonDetails = {
person_id: creator_id,
- auth: req.auth,
+ auth,
};
- promises.push(req.client.getPersonDetails(getCreatorForm));
+ promises.push(client.getPersonDetails(getCreatorForm));
} else {
promises.push(Promise.resolve());
}
- let q = this.getSearchQueryFromProps(pathSplit[3]);
+ const query = getSearchQueryFromQuery(q);
- if (q) {
- let form: SearchForm = {
- q,
+ if (query) {
+ const form: SearchForm = {
+ q: query,
community_id,
creator_id,
- type_: this.getSearchTypeFromProps(pathSplit[5]),
- sort: this.getSortTypeFromProps(pathSplit[7]),
- listing_type: this.getListingTypeFromProps(pathSplit[9]),
- page: this.getPageFromProps(pathSplit[15]),
+ type_: getSearchTypeFromQuery(type),
+ sort: getSortTypeFromQuery(sort),
+ listing_type: getListingTypeFromQuery(listingType),
+ page: getIdFromString(page),
limit: fetchLimit,
- auth: req.auth,
+ auth,
};
- let resolveObjectForm: ResolveObject = {
- q,
- auth: req.auth,
+ const resolveObjectForm: ResolveObject = {
+ q: query,
+ auth,
};
- if (form.q != "") {
- promises.push(req.client.search(form));
- promises.push(req.client.resolveObject(resolveObjectForm));
+ if (query !== "") {
+ promises.push(client.search(form));
+ promises.push(client.resolveObject(resolveObjectForm));
} else {
promises.push(Promise.resolve());
promises.push(Promise.resolve());
return promises;
}
- componentDidUpdate(_: any, lastState: SearchState) {
- if (
- lastState.q !== this.state.q ||
- lastState.type_ !== this.state.type_ ||
- lastState.sort !== this.state.sort ||
- lastState.listingType !== this.state.listingType ||
- lastState.communityId !== this.state.communityId ||
- lastState.creatorId !== this.state.creatorId ||
- lastState.page !== this.state.page
- ) {
- if (this.state.q) {
- this.setState({
- loading: true,
- searchText: this.state.q,
- });
- this.search();
- }
- }
- }
-
get documentTitle(): string {
- let siteName = this.state.siteRes.site_view.site.name;
- return this.state.q
- ? `${i18n.t("search")} - ${this.state.q} - ${siteName}`
- : `${i18n.t("search")} - ${siteName}`;
+ const { q } = getSearchQueryParams();
+ const name = this.state.siteRes.site_view.site.name;
+ return `${i18n.t("search")} - ${q ? `${q} - ` : ""}${name}`;
}
render() {
+ const { type, page } = getSearchQueryParams();
+
return (
<div className="container-lg">
<HtmlTags
path={this.context.router.route.match.url}
/>
<h5>{i18n.t("search")}</h5>
- {this.selects()}
- {this.searchForm()}
- {this.state.type_ == SearchType.All && this.all()}
- {this.state.type_ == SearchType.Comments && this.comments()}
- {this.state.type_ == SearchType.Posts && this.posts()}
- {this.state.type_ == SearchType.Communities && this.communities()}
- {this.state.type_ == SearchType.Users && this.users()}
- {this.state.type_ == SearchType.Url && this.posts()}
- {this.resultsCount() == 0 && <span>{i18n.t("no_results")}</span>}
- <Paginator page={this.state.page} onChange={this.handlePageChange} />
+ {this.selects}
+ {this.searchForm}
+ {this.displayResults(type)}
+ {this.resultsCount === 0 && <span>{i18n.t("no_results")}</span>}
+ <Paginator page={page} onChange={this.handlePageChange} />
</div>
);
}
- searchForm() {
+ displayResults(type: SearchType) {
+ switch (type) {
+ case SearchType.All:
+ return this.all;
+ case SearchType.Comments:
+ return this.comments;
+ case SearchType.Posts:
+ case SearchType.Url:
+ return this.posts;
+ case SearchType.Communities:
+ return this.communities;
+ case SearchType.Users:
+ return this.users;
+ default:
+ return <></>;
+ }
+ }
+
+ get searchForm() {
return (
<form
className="form-inline"
minLength={1}
/>
<button type="submit" className="btn btn-secondary mr-2 mb-2">
- {this.state.loading ? <Spinner /> : <span>{i18n.t("search")}</span>}
+ {this.state.searchLoading ? (
+ <Spinner />
+ ) : (
+ <span>{i18n.t("search")}</span>
+ )}
</button>
</form>
);
}
- selects() {
+ get selects() {
+ const { type, listingType, sort, communityId, creatorId } =
+ getSearchQueryParams();
+ const {
+ communitySearchOptions,
+ creatorSearchOptions,
+ searchCommunitiesLoading,
+ searchCreatorLoading,
+ } = this.state;
+
return (
<div className="mb-2">
<select
- value={this.state.type_}
+ value={type}
onChange={linkEvent(this, this.handleTypeChange)}
className="custom-select w-auto mb-2"
aria-label={i18n.t("type")}
<option disabled aria-hidden="true">
{i18n.t("type")}
</option>
- <option value={SearchType.All}>{i18n.t("all")}</option>
- <option value={SearchType.Comments}>{i18n.t("comments")}</option>
- <option value={SearchType.Posts}>{i18n.t("posts")}</option>
- <option value={SearchType.Communities}>
- {i18n.t("communities")}
- </option>
- <option value={SearchType.Users}>{i18n.t("users")}</option>
- <option value={SearchType.Url}>{i18n.t("url")}</option>
+ {searchTypes.map(option => (
+ <option value={option} key={option}>
+ {i18n.t(option.toString().toLowerCase() as NoOptionI18nKeys)}
+ </option>
+ ))}
</select>
<span className="ml-2">
<ListingTypeSelect
- type_={this.state.listingType}
+ type_={listingType}
showLocal={showLocal(this.isoData)}
showSubscribed
onChange={this.handleListingTypeChange}
</span>
<span className="ml-2">
<SortSelect
- sort={this.state.sort}
+ sort={sort}
onChange={this.handleSortChange}
hideHot
hideMostComments
/>
</span>
<div className="form-row">
- {this.state.communities.length > 0 && this.communityFilter()}
- {this.creatorFilter()}
+ {this.state.communities.length > 0 && (
+ <Filter
+ filterType="community"
+ onChange={this.handleCommunityFilterChange}
+ onSearch={this.handleCommunitySearch}
+ options={communitySearchOptions}
+ loading={searchCommunitiesLoading}
+ value={communityId}
+ />
+ )}
+ <Filter
+ filterType="creator"
+ onChange={this.handleCreatorFilterChange}
+ onSearch={this.handleCreatorSearch}
+ options={creatorSearchOptions}
+ loading={searchCreatorLoading}
+ value={creatorId}
+ />
</div>
</div>
);
}
- postViewToCombined(postView: PostView): Combined {
- return {
- type_: "posts",
- data: postView,
- published: postView.post.published,
- };
- }
-
- commentViewToCombined(commentView: CommentView): Combined {
- return {
- type_: "comments",
- data: commentView,
- published: commentView.comment.published,
- };
- }
-
- communityViewToCombined(communityView: CommunityView): Combined {
- return {
- type_: "communities",
- data: communityView,
- published: communityView.community.published,
- };
- }
-
- personViewSafeToCombined(personViewSafe: PersonViewSafe): Combined {
- return {
- type_: "users",
- data: personViewSafe,
- published: personViewSafe.person.published,
- };
- }
-
buildCombined(): Combined[] {
- let combined: Combined[] = [];
+ const combined: Combined[] = [];
+ const { resolveObjectResponse, searchResponse } = this.state;
- let resolveRes = this.state.resolveObjectResponse;
// Push the possible resolve / federated objects first
- if (resolveRes) {
- let resolveComment = resolveRes.comment;
- if (resolveComment) {
- combined.push(this.commentViewToCombined(resolveComment));
+ if (resolveObjectResponse) {
+ const { comment, post, community, person } = resolveObjectResponse;
+
+ if (comment) {
+ combined.push(commentViewToCombined(comment));
}
- let resolvePost = resolveRes.post;
- if (resolvePost) {
- combined.push(this.postViewToCombined(resolvePost));
+ if (post) {
+ combined.push(postViewToCombined(post));
}
- let resolveCommunity = resolveRes.community;
- if (resolveCommunity) {
- combined.push(this.communityViewToCombined(resolveCommunity));
+ if (community) {
+ combined.push(communityViewToCombined(community));
}
- let resolveUser = resolveRes.person;
- if (resolveUser) {
- combined.push(this.personViewSafeToCombined(resolveUser));
+ if (person) {
+ combined.push(personViewSafeToCombined(person));
}
}
// Push the search results
- let searchRes = this.state.searchResponse;
- if (searchRes) {
- pushNotNull(
- combined,
- searchRes.comments?.map(e => this.commentViewToCombined(e))
- );
- pushNotNull(
- combined,
- searchRes.posts?.map(e => this.postViewToCombined(e))
- );
- pushNotNull(
- combined,
- searchRes.communities?.map(e => this.communityViewToCombined(e))
- );
- pushNotNull(
- combined,
- searchRes.users?.map(e => this.personViewSafeToCombined(e))
+ if (searchResponse) {
+ const { comments, posts, communities, users } = searchResponse;
+
+ combined.push(
+ ...[
+ ...(comments?.map(commentViewToCombined) ?? []),
+ ...(posts?.map(postViewToCombined) ?? []),
+ ...(communities?.map(communityViewToCombined) ?? []),
+ ...(users?.map(personViewSafeToCombined) ?? []),
+ ]
);
}
+ const { sort } = getSearchQueryParams();
+
// Sort it
- if (this.state.sort == SortType.New) {
+ if (sort === SortType.New) {
combined.sort((a, b) => b.published.localeCompare(a.published));
} else {
combined.sort(
(a.data as PersonViewSafe).counts.comment_score)
);
}
+
return combined;
}
- all() {
- let combined = this.buildCombined();
+ get all() {
+ const combined = this.buildCombined();
+
return (
<div>
{combined.map(i => (
<div key={i.published} className="row">
<div className="col-12">
- {i.type_ == "posts" && (
+ {i.type_ === "posts" && (
<PostListing
key={(i.data as PostView).post.id}
post_view={i.data as PostView}
viewOnly
/>
)}
- {i.type_ == "comments" && (
+ {i.type_ === "comments" && (
<CommentNodes
key={(i.data as CommentView).comment.id}
nodes={[
siteLanguages={this.state.siteRes.discussion_languages}
/>
)}
- {i.type_ == "communities" && (
- <div>{this.communityListing(i.data as CommunityView)}</div>
+ {i.type_ === "communities" && (
+ <div>{communityListing(i.data as CommunityView)}</div>
)}
- {i.type_ == "users" && (
- <div>{this.personListing(i.data as PersonViewSafe)}</div>
+ {i.type_ === "users" && (
+ <div>{personListing(i.data as PersonViewSafe)}</div>
)}
</div>
</div>
);
}
- comments() {
- let comments: CommentView[] = [];
- pushNotNull(comments, this.state.resolveObjectResponse?.comment);
- pushNotNull(comments, this.state.searchResponse?.comments);
+ get comments() {
+ const { searchResponse, resolveObjectResponse, siteRes } = this.state;
+ const comments = searchResponse?.comments ?? [];
+
+ if (resolveObjectResponse?.comment) {
+ comments.unshift(resolveObjectResponse?.comment);
+ }
return (
<CommentNodes
viewOnly
locked
noIndent
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
+ enableDownvotes={enableDownvotes(siteRes)}
+ allLanguages={siteRes.all_languages}
+ siteLanguages={siteRes.discussion_languages}
/>
);
}
- posts() {
- let posts: PostView[] = [];
+ get posts() {
+ const { searchResponse, resolveObjectResponse, siteRes } = this.state;
+ const posts = searchResponse?.posts ?? [];
- pushNotNull(posts, this.state.resolveObjectResponse?.post);
- pushNotNull(posts, this.state.searchResponse?.posts);
+ if (resolveObjectResponse?.post) {
+ posts.unshift(resolveObjectResponse.post);
+ }
return (
<>
<PostListing
post_view={pv}
showCommunity
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- enableNsfw={enableNsfw(this.state.siteRes)}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
+ enableDownvotes={enableDownvotes(siteRes)}
+ enableNsfw={enableNsfw(siteRes)}
+ allLanguages={siteRes.all_languages}
+ siteLanguages={siteRes.discussion_languages}
viewOnly
/>
</div>
);
}
- communities() {
- let communities: CommunityView[] = [];
+ get communities() {
+ const { searchResponse, resolveObjectResponse } = this.state;
+ const communities = searchResponse?.communities ?? [];
- pushNotNull(communities, this.state.resolveObjectResponse?.community);
- pushNotNull(communities, this.state.searchResponse?.communities);
+ if (resolveObjectResponse?.community) {
+ communities.unshift(resolveObjectResponse.community);
+ }
return (
<>
{communities.map(cv => (
<div key={cv.community.id} className="row">
- <div className="col-12">{this.communityListing(cv)}</div>
+ <div className="col-12">{communityListing(cv)}</div>
</div>
))}
</>
);
}
- users() {
- let users: PersonViewSafe[] = [];
+ get users() {
+ const { searchResponse, resolveObjectResponse } = this.state;
+ const users = searchResponse?.users ?? [];
- pushNotNull(users, this.state.resolveObjectResponse?.person);
- pushNotNull(users, this.state.searchResponse?.users);
+ if (resolveObjectResponse?.person) {
+ users.unshift(resolveObjectResponse.person);
+ }
return (
<>
{users.map(pvs => (
<div key={pvs.person.id} className="row">
- <div className="col-12">{this.personListing(pvs)}</div>
+ <div className="col-12">{personListing(pvs)}</div>
</div>
))}
</>
);
}
- communityListing(community_view: CommunityView) {
- return (
- <>
- <span>
- <CommunityLink community={community_view.community} />
- </span>
- <span>{` -
- ${i18n.t("number_of_subscribers", {
- count: community_view.counts.subscribers,
- formattedCount: numToSI(community_view.counts.subscribers),
- })}
- `}</span>
- </>
- );
- }
-
- personListing(person_view: PersonViewSafe) {
- return (
- <>
- <span>
- <PersonListing person={person_view.person} showApubName />
- </span>
- <span>{` - ${i18n.t("number_of_comments", {
- count: person_view.counts.comment_count,
- formattedCount: numToSI(person_view.counts.comment_count),
- })}`}</span>
- </>
- );
- }
-
- communityFilter() {
- return (
- <div className="form-group col-sm-6">
- <label className="col-form-label" htmlFor="community-filter">
- {i18n.t("community")}
- </label>
- <div>
- <select
- className="form-control"
- id="community-filter"
- value={this.state.communityId}
- >
- <option value="0">{i18n.t("all")}</option>
- {this.state.communities.map(cv => (
- <option key={cv.community.id} value={cv.community.id}>
- {communitySelectName(cv)}
- </option>
- ))}
- </select>
- </div>
- </div>
- );
- }
-
- creatorFilter() {
- let creatorPv = this.state.creatorDetails?.person_view;
- return (
- <div className="form-group col-sm-6">
- <label className="col-form-label" htmlFor="creator-filter">
- {capitalizeFirstLetter(i18n.t("creator"))}
- </label>
- <div>
- <select
- className="form-control"
- id="creator-filter"
- value={this.state.creatorId}
- >
- <option value="0">{i18n.t("all")}</option>
- {creatorPv && (
- <option value={creatorPv.person.id}>
- {personSelectName(creatorPv)}
- </option>
- )}
- </select>
- </div>
- </div>
- );
- }
-
- resultsCount(): number {
- let r = this.state.searchResponse;
+ get resultsCount(): number {
+ const { searchResponse: r, resolveObjectResponse: resolveRes } = this.state;
- let searchCount = r
- ? r.posts?.length +
- r.comments?.length +
- r.communities?.length +
- r.users?.length
+ const searchCount = r
+ ? r.posts.length +
+ r.comments.length +
+ r.communities.length +
+ r.users.length
: 0;
- let resolveRes = this.state.resolveObjectResponse;
- let resObjCount = resolveRes
+ const resObjCount = resolveRes
? resolveRes.post ||
resolveRes.person ||
resolveRes.community ||
return resObjCount + searchCount;
}
- handlePageChange(page: number) {
- this.updateUrl({ page });
- }
-
search() {
- let community_id =
- this.state.communityId == 0 ? undefined : this.state.communityId;
- let creator_id =
- this.state.creatorId == 0 ? undefined : this.state.creatorId;
-
- let auth = myAuth(false);
- if (this.state.q && this.state.q != "") {
- let form: SearchForm = {
- q: this.state.q,
- community_id,
- creator_id,
- type_: this.state.type_,
- sort: this.state.sort,
- listing_type: this.state.listingType,
- page: this.state.page,
+ const auth = myAuth(false);
+ const { searchText: q } = this.state;
+ const { communityId, creatorId, type, sort, listingType, page } =
+ getSearchQueryParams();
+
+ if (q && q !== "") {
+ const form: SearchForm = {
+ q,
+ community_id: communityId ?? undefined,
+ creator_id: creatorId ?? undefined,
+ type_: type,
+ sort,
+ listing_type: listingType,
+ page,
limit: fetchLimit,
auth,
};
- let resolveObjectForm: ResolveObject = {
- q: this.state.q,
+ const resolveObjectForm: ResolveObject = {
+ q,
auth,
};
this.setState({
searchResponse: undefined,
resolveObjectResponse: undefined,
- loading: true,
+ searchLoading: true,
});
+
WebSocketService.Instance.send(wsClient.search(form));
WebSocketService.Instance.send(wsClient.resolveObject(resolveObjectForm));
}
}
- setupCommunityFilter() {
- if (isBrowser()) {
- let selectId: any = document.getElementById("community-filter");
- if (selectId) {
- this.communityChoices = new Choices(selectId, choicesConfig);
- this.communityChoices.passedElement.element.addEventListener(
- "choice",
- (e: any) => {
- this.handleCommunityFilterChange(Number(e.detail.choice.value));
- },
- false
- );
- this.communityChoices.passedElement.element.addEventListener(
- "search",
- debounce(async (e: any) => {
- try {
- let communities = (await fetchCommunities(e.detail.value))
- .communities;
- let choices = communities.map(cv => communityToChoice(cv));
- choices.unshift({ value: "0", label: i18n.t("all") });
- this.communityChoices.setChoices(choices, "value", "label", true);
- } catch (err) {
- console.error(err);
- }
- }),
- false
- );
- }
+ handleCreatorSearch = debounce(async (text: string) => {
+ const { creatorId } = getSearchQueryParams();
+ const { creatorSearchOptions } = this.state;
+ this.setState({
+ searchCreatorLoading: true,
+ });
+
+ const newOptions: Choice[] = [];
+
+ const selectedChoice = creatorSearchOptions.find(
+ choice => getIdFromString(choice.value) === creatorId
+ );
+
+ if (selectedChoice) {
+ newOptions.push(selectedChoice);
}
- }
- setupCreatorFilter() {
- if (isBrowser()) {
- let selectId: any = document.getElementById("creator-filter");
- if (selectId) {
- this.creatorChoices = new Choices(selectId, choicesConfig);
- this.creatorChoices.passedElement.element.addEventListener(
- "choice",
- (e: any) => {
- this.handleCreatorFilterChange(Number(e.detail.choice.value));
- },
- false
- );
- this.creatorChoices.passedElement.element.addEventListener(
- "search",
- debounce(async (e: any) => {
- try {
- let creators = (await fetchUsers(e.detail.value)).users;
- let choices = creators.map(pvs => personToChoice(pvs));
- choices.unshift({ value: "0", label: i18n.t("all") });
- this.creatorChoices.setChoices(choices, "value", "label", true);
- } catch (err) {
- console.log(err);
- }
- }),
- false
- );
- }
+ if (text.length > 0) {
+ newOptions.push(...(await fetchUsers(text)).users.map(personToChoice));
}
- }
- handleSortChange(val: SortType) {
- const updateObj = { sort: val, page: 1 };
- this.setState(updateObj);
- this.updateUrl(updateObj);
+ this.setState({
+ searchCreatorLoading: false,
+ creatorSearchOptions: newOptions,
+ });
+ });
+
+ handleCommunitySearch = debounce(async (text: string) => {
+ const { communityId } = getSearchQueryParams();
+ const { communitySearchOptions } = this.state;
+ this.setState({
+ searchCommunitiesLoading: true,
+ });
+
+ const newOptions: Choice[] = [];
+
+ const selectedChoice = communitySearchOptions.find(
+ choice => getIdFromString(choice.value) === communityId
+ );
+
+ if (selectedChoice) {
+ newOptions.push(selectedChoice);
+ }
+
+ if (text.length > 0) {
+ newOptions.push(
+ ...(await fetchCommunities(text)).communities.map(communityToChoice)
+ );
+ }
+
+ this.setState({
+ searchCommunitiesLoading: false,
+ communitySearchOptions: newOptions,
+ });
+ });
+
+ handleSortChange(sort: SortType) {
+ this.updateUrl({ sort, page: 1 });
}
handleTypeChange(i: Search, event: any) {
- const updateObj = {
- type_: SearchType[event.target.value],
+ const type = SearchType[event.target.value];
+
+ i.updateUrl({
+ type,
page: 1,
- };
- i.setState(updateObj);
- i.updateUrl(updateObj);
+ });
}
- handleListingTypeChange(val: ListingType) {
- const updateObj = {
- listingType: val,
+ handlePageChange(page: number) {
+ this.updateUrl({ page });
+ }
+
+ handleListingTypeChange(listingType: ListingType) {
+ this.updateUrl({
+ listingType,
page: 1,
- };
- this.setState(updateObj);
- this.updateUrl(updateObj);
+ });
}
- handleCommunityFilterChange(communityId: number) {
+ handleCommunityFilterChange({ value }: Choice) {
this.updateUrl({
- communityId,
+ communityId: getIdFromString(value) ?? null,
page: 1,
});
}
- handleCreatorFilterChange(creatorId: number) {
+ handleCreatorFilterChange({ value }: Choice) {
this.updateUrl({
- creatorId,
+ creatorId: getIdFromString(value) ?? null,
page: 1,
});
}
handleSearchSubmit(i: Search, event: any) {
event.preventDefault();
+
i.updateUrl({
q: i.state.searchText,
- type_: i.state.type_,
- listingType: i.state.listingType,
- communityId: i.state.communityId,
- creatorId: i.state.creatorId,
- sort: i.state.sort,
- page: i.state.page,
+ page: 1,
});
}
i.setState({ searchText: event.target.value });
}
- updateUrl(paramUpdates: UrlParams) {
- const qStr = paramUpdates.q || this.state.q;
- const qStrEncoded = encodeURIComponent(qStr || "");
- const typeStr = paramUpdates.type_ || this.state.type_;
- const listingTypeStr = paramUpdates.listingType || this.state.listingType;
- const sortStr = paramUpdates.sort || this.state.sort;
- const communityId =
- paramUpdates.communityId == 0
- ? 0
- : paramUpdates.communityId || this.state.communityId;
- const creatorId =
- paramUpdates.creatorId == 0
- ? 0
- : paramUpdates.creatorId || this.state.creatorId;
- const page = paramUpdates.page || this.state.page;
- this.props.history.push(
- `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/listing_type/${listingTypeStr}/community_id/${communityId}/creator_id/${creatorId}/page/${page}`
- );
+ updateUrl({
+ q,
+ type,
+ listingType,
+ sort,
+ communityId,
+ creatorId,
+ page,
+ }: Partial<SearchProps>) {
+ const {
+ q: urlQ,
+ type: urlType,
+ listingType: urlListingType,
+ communityId: urlCommunityId,
+ sort: urlSort,
+ creatorId: urlCreatorId,
+ page: urlPage,
+ } = getSearchQueryParams();
+
+ let query = q ?? this.state.searchText ?? urlQ;
+
+ if (query && query.length > 0) {
+ query = encodeURIComponent(query);
+ }
+
+ const queryParams: QueryParams<SearchProps> = {
+ q: query,
+ type: type ?? urlType,
+ listingType: listingType ?? urlListingType,
+ communityId: getUpdatedSearchId(communityId, urlCommunityId),
+ creatorId: getUpdatedSearchId(creatorId, urlCreatorId),
+ page: (page ?? urlPage).toString(),
+ sort: sort ?? urlSort,
+ };
+
+ this.props.history.push(`/search${getQueryString(queryParams)}`);
+
+ this.search();
}
parseMessage(msg: any) {
console.log(msg);
- let op = wsUserOp(msg);
+ const op = wsUserOp(msg);
if (msg.error) {
- if (msg.error == "couldnt_find_object") {
+ if (msg.error === "couldnt_find_object") {
this.setState({
resolveObjectResponse: {},
});
this.checkFinishedLoading();
} else {
toast(i18n.t(msg.error), "danger");
- return;
}
- } else if (op == UserOperation.Search) {
- let data = wsJsonToRes<SearchResponse>(msg);
- this.setState({ searchResponse: data });
- window.scrollTo(0, 0);
- this.checkFinishedLoading();
- restoreScrollPosition(this.context);
- } else if (op == UserOperation.CreateCommentLike) {
- let data = wsJsonToRes<CommentResponse>(msg);
- createCommentLikeRes(
- data.comment_view,
- this.state.searchResponse?.comments
- );
- this.setState(this.state);
- } else if (op == UserOperation.CreatePostLike) {
- let data = wsJsonToRes<PostResponse>(msg);
- createPostLikeFindRes(data.post_view, this.state.searchResponse?.posts);
- this.setState(this.state);
- } else if (op == UserOperation.ListCommunities) {
- let data = wsJsonToRes<ListCommunitiesResponse>(msg);
- this.setState({ communities: data.communities });
- this.setupCommunityFilter();
- } else if (op == UserOperation.ResolveObject) {
- let data = wsJsonToRes<ResolveObjectResponse>(msg);
- this.setState({ resolveObjectResponse: data });
- this.checkFinishedLoading();
+ } else {
+ switch (op) {
+ case UserOperation.Search: {
+ const searchResponse = wsJsonToRes<SearchResponse>(msg);
+ this.setState({ searchResponse });
+ window.scrollTo(0, 0);
+ this.checkFinishedLoading();
+ restoreScrollPosition(this.context);
+
+ break;
+ }
+
+ case UserOperation.CreateCommentLike: {
+ const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+ createCommentLikeRes(
+ comment_view,
+ this.state.searchResponse?.comments
+ );
+
+ break;
+ }
+
+ case UserOperation.CreatePostLike: {
+ const { post_view } = wsJsonToRes<PostResponse>(msg);
+ createPostLikeFindRes(post_view, this.state.searchResponse?.posts);
+
+ break;
+ }
+
+ case UserOperation.ListCommunities: {
+ const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
+ this.setState({ communities });
+
+ break;
+ }
+
+ case UserOperation.ResolveObject: {
+ const resolveObjectResponse = wsJsonToRes<ResolveObjectResponse>(msg);
+ this.setState({ resolveObjectResponse });
+ this.checkFinishedLoading();
+
+ break;
+ }
+ }
}
}
checkFinishedLoading() {
if (this.state.searchResponse && this.state.resolveObjectResponse) {
- this.setState({ loading: false });
+ this.setState({ searchLoading: false });
}
}
}