X-Git-Url: http://these/git/?a=blobdiff_plain;f=src%2Fshared%2Fcomponents%2Fsearch.tsx;h=99b180356b87c5ab89f789a795c49c81f9ef8645;hb=53c3cfeade90150b07431386745a24aa699a25ec;hp=52cd73b78a25417b0ff62b17085973502c6e57d2;hpb=24548ccba8cbfe95c883e763f3e3644d3f02139c;p=lemmy-ui.git diff --git a/src/shared/components/search.tsx b/src/shared/components/search.tsx index 52cd73b..99b1803 100644 --- a/src/shared/components/search.tsx +++ b/src/shared/components/search.tsx @@ -1,7 +1,31 @@ -import { None, Option, Some } from "@sniptt/monads"; +import { + commentsToFlatNodes, + communityToChoice, + enableDownvotes, + enableNsfw, + fetchCommunities, + fetchUsers, + getUpdatedSearchId, + myAuth, + personToChoice, + setIsoData, + showLocal, +} from "@utils/app"; +import { restoreScrollPosition, saveScrollPosition } from "@utils/browser"; +import { + capitalizeFirstLetter, + debounce, + getIdFromString, + getPageFromString, + getQueryParams, + getQueryString, + numToSI, +} from "@utils/helpers"; +import type { QueryParams } from "@utils/types"; +import { Choice, RouteDataResponse } from "@utils/types"; +import type { NoOptionI18nKeys } from "i18next"; import { Component, linkEvent } from "inferno"; import { - CommentResponse, CommentView, CommunityView, GetCommunity, @@ -12,8 +36,7 @@ import { ListCommunities, ListCommunitiesResponse, ListingType, - PersonViewSafe, - PostResponse, + PersonView, PostView, ResolveObject, ResolveObjectResponse, @@ -21,584 +44,671 @@ import { SearchResponse, SearchType, SortType, - UserOperation, - wsJsonToRes, - wsUserOp, } from "lemmy-js-client"; -import { Subscription } from "rxjs"; -import { i18n } from "../i18next"; +import { fetchLimit } from "../config"; import { CommentViewType, InitialFetchRequest } from "../interfaces"; -import { WebSocketService } from "../services"; -import { - auth, - capitalizeFirstLetter, - choicesConfig, - commentsToFlatNodes, - communitySelectName, - communityToChoice, - createCommentLikeRes, - createPostLikeFindRes, - debounce, - enableDownvotes, - enableNsfw, - fetchCommunities, - fetchLimit, - fetchUsers, - isBrowser, - numToSI, - personSelectName, - personToChoice, - pushNotNull, - restoreScrollPosition, - routeListingTypeToEnum, - routeSearchTypeToEnum, - routeSortTypeToEnum, - saveScrollPosition, - setIsoData, - showLocal, - toast, - wsClient, - wsSubscribe, -} from "../utils"; +import { FirstLoadService, I18NextService } from "../services"; +import { HttpService, RequestState } from "../services/HttpService"; import { CommentNodes } from "./comment/comment-nodes"; import { HtmlTags } from "./common/html-tags"; 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; + q?: string; + type: SearchType; sort: SortType; listingType: ListingType; - communityId: number; - creatorId: number; + communityId?: number | null; + creatorId?: number | null; page: number; } +type SearchData = RouteDataResponse<{ + communityResponse: GetCommunityResponse; + listCommunitiesResponse: ListCommunitiesResponse; + creatorDetailsResponse: GetPersonDetailsResponse; + searchResponse: SearchResponse; + resolveObjectResponse: ResolveObjectResponse; +}>; + +type FilterType = "creator" | "community"; + interface SearchState { - q: string; - type_: SearchType; - sort: SortType; - listingType: ListingType; - communityId: number; - creatorId: number; - page: number; - searchResponse: Option; - communities: CommunityView[]; - creatorDetails: Option; - loading: boolean; + searchRes: RequestState; + resolveObjectRes: RequestState; + creatorDetailsRes: RequestState; + communitiesRes: RequestState; + communityRes: RequestState; siteRes: GetSiteResponse; - searchText: string; - resolveObjectResponse: Option; -} - -interface UrlParams { - q?: string; - type_?: SearchType; - sort?: SortType; - listingType?: ListingType; - communityId?: number; - creatorId?: number; - page?: number; + searchText?: string; + communitySearchOptions: Choice[]; + creatorSearchOptions: Choice[]; + searchCreatorLoading: boolean; + searchCommunitiesLoading: boolean; + isIsomorphic: boolean; } interface Combined { type_: string; - data: CommentView | PostView | CommunityView | PersonViewSafe; + data: CommentView | PostView | CommunityView | PersonView; published: string; } -export class Search extends Component { - private isoData = setIsoData( - this.context, - GetCommunityResponse, - ListCommunitiesResponse, - GetPersonDetailsResponse, - SearchResponse, - ResolveObjectResponse - ); - private communityChoices: any; - private creatorChoices: any; - private subscription: Subscription; - private emptyState: 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), - searchResponse: None, - resolveObjectResponse: None, - creatorDetails: None, - loading: true, - siteRes: this.isoData.site_res, - communities: [], +const defaultSearchType = "All"; +const defaultSortType = "TopAll"; +const defaultListingType = "All"; + +const searchTypes = ["All", "Comments", "Posts", "Communities", "Users", "Url"]; + +const getSearchQueryParams = () => + getQueryParams({ + q: getSearchQueryFromQuery, + type: getSearchTypeFromQuery, + sort: getSortTypeFromQuery, + listingType: getListingTypeFromQuery, + communityId: getIdFromString, + creatorId: getIdFromString, + page: getPageFromString, + }); + +const getSearchQueryFromQuery = (q?: string): string | undefined => + q ? decodeURIComponent(q) : undefined; + +function getSearchTypeFromQuery(type_?: string): SearchType { + return type_ ? (type_ as SearchType) : defaultSearchType; +} + +function getSortTypeFromQuery(sort?: string): SortType { + return sort ? (sort as SortType) : defaultSortType; +} + +function getListingTypeFromQuery(listingType?: string): ListingType { + return listingType ? (listingType as ListingType) : defaultListingType; +} + +function postViewToCombined(data: PostView): Combined { + return { + type_: "posts", + data, + published: data.post.published, }; +} - static getSearchQueryFromProps(q: string): string { - return decodeURIComponent(q) || ""; - } +function commentViewToCombined(data: CommentView): Combined { + return { + type_: "comments", + data, + published: data.comment.published, + }; +} - static getSearchTypeFromProps(type_: string): SearchType { - return type_ ? routeSearchTypeToEnum(type_) : SearchType.All; - } +function communityViewToCombined(data: CommunityView): Combined { + return { + type_: "communities", + data, + published: data.community.published, + }; +} - static getSortTypeFromProps(sort: string): SortType { - return sort ? routeSortTypeToEnum(sort) : SortType.TopAll; - } +function personViewSafeToCombined(data: PersonView): Combined { + return { + type_: "users", + data, + published: data.person.published, + }; +} - static getListingTypeFromProps(listingType: string): ListingType { - return listingType ? routeListingTypeToEnum(listingType) : ListingType.All; - } +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 ( +
+ + +
+ ); +}; + +const communityListing = ({ + community, + counts: { subscribers }, +}: CommunityView) => + getListing( + , + subscribers, + "number_of_subscribers" + ); - static getCommunityIdFromProps(id: string): number { - return id ? Number(id) : 0; - } +const personListing = ({ person, counts: { comment_count } }: PersonView) => + getListing( + , + comment_count, + "number_of_comments" + ); - static getCreatorIdFromProps(id: string): number { - return id ? Number(id) : 0; - } +function getListing( + listing: JSX.ElementClass, + count: number, + translationKey: "number_of_comments" | "number_of_subscribers" +) { + return ( + <> + {listing} + {` - ${I18NextService.i18n.t(translationKey, { + count: Number(count), + formattedCount: numToSI(count), + })}`} + + ); +} - static getPageFromProps(page: string): number { - return page ? Number(page) : 1; - } +export class Search extends Component { + private isoData = setIsoData(this.context); + + state: SearchState = { + resolveObjectRes: { state: "empty" }, + creatorDetailsRes: { state: "empty" }, + communitiesRes: { state: "empty" }, + communityRes: { state: "empty" }, + siteRes: this.isoData.site_res, + creatorSearchOptions: [], + communitySearchOptions: [], + searchRes: { state: "empty" }, + searchCreatorLoading: false, + searchCommunitiesLoading: false, + isIsomorphic: false, + }; constructor(props: any, context: any) { super(props, context); - this.state = this.emptyState; 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 = Some( - this.isoData.routeData[0] as GetCommunityResponse - ); - let communitiesRes = Some( - this.isoData.routeData[1] as ListCommunitiesResponse - ); + if (FirstLoadService.isFirstLoad) { + const { + communityResponse: communityRes, + creatorDetailsResponse: creatorDetailsRes, + listCommunitiesResponse: communitiesRes, + resolveObjectResponse: resolveObjectRes, + searchResponse: searchRes, + } = this.isoData.routeData; - // This can be single or multiple communities given - if (communitiesRes.isSome()) { + this.state = { + ...this.state, + isIsomorphic: true, + }; + + if (creatorDetailsRes?.state === "success") { this.state = { ...this.state, - communities: communitiesRes.unwrap().communities, + creatorSearchOptions: + creatorDetailsRes?.state === "success" + ? [personToChoice(creatorDetailsRes.data.person_view)] + : [], + creatorDetailsRes, }; } - if (communityRes.isSome()) { + if (communitiesRes?.state === "success") { this.state = { ...this.state, - communities: [communityRes.unwrap().community_view], + communitiesRes, }; } - this.state = { - ...this.state, - creatorDetails: Some( - this.isoData.routeData[2] as GetPersonDetailsResponse - ), - }; + if (communityRes?.state === "success") { + this.state = { + ...this.state, + communityRes, + }; + } - if (this.state.q != "") { + if (q !== "") { this.state = { ...this.state, - searchResponse: Some(this.isoData.routeData[3] as SearchResponse), - resolveObjectResponse: Some( - this.isoData.routeData[4] as ResolveObjectResponse - ), - loading: false, }; - } else { - this.search(); + + if (searchRes?.state === "success") { + this.state = { + ...this.state, + searchRes, + }; + } + + if (resolveObjectRes?.state === "success") { + this.state = { + ...this.state, + resolveObjectRes, + }; + } } - } else { - this.fetchCommunities(); - this.search(); } } - componentWillUnmount() { - this.subscription.unsubscribe(); - saveScrollPosition(this.context); - } + async componentDidMount() { + if (!this.state.isIsomorphic) { + const promises = [this.fetchCommunities()]; + if (this.state.searchText) { + promises.push(this.search()); + } - componentDidMount() { - this.setupCommunityFilter(); - this.setupCreatorFilter(); + await Promise.all(promises); + } } - static getDerivedStateFromProps(props: any): SearchProps { - return { - q: Search.getSearchQueryFromProps(props.match.params.q), - type_: Search.getSearchTypeFromProps(props.match.params.type), - sort: Search.getSortTypeFromProps(props.match.params.sort), - 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), - }; + async fetchCommunities() { + this.setState({ communitiesRes: { state: "loading" } }); + this.setState({ + communitiesRes: await HttpService.client.listCommunities({ + type_: defaultListingType, + sort: defaultSortType, + limit: fetchLimit, + auth: myAuth(), + }), + }); } - fetchCommunities() { - let listCommunitiesForm = new ListCommunities({ - type_: Some(ListingType.All), - sort: Some(SortType.TopAll), - limit: Some(fetchLimit), - page: None, - auth: auth(false).ok(), - }); - WebSocketService.Instance.send( - wsClient.listCommunities(listCommunitiesForm) - ); + componentWillUnmount() { + saveScrollPosition(this.context); } - static fetchInitialData(req: InitialFetchRequest): Promise[] { - let pathSplit = req.path.split("/"); - let promises: Promise[] = []; - - let communityId = this.getCommunityIdFromProps(pathSplit[11]); - let community_id: Option = - communityId == 0 ? None : Some(communityId); - community_id.match({ - some: id => { - let getCommunityForm = new GetCommunity({ - id: Some(id), - name: None, - auth: req.auth, - }); - promises.push(req.client.getCommunity(getCommunityForm)); - promises.push(Promise.resolve()); - }, - none: () => { - let listCommunitiesForm = new ListCommunities({ - type_: Some(ListingType.All), - sort: Some(SortType.TopAll), - limit: Some(fetchLimit), - page: None, - auth: req.auth, - }); - promises.push(Promise.resolve()); - promises.push(req.client.listCommunities(listCommunitiesForm)); - }, - }); + static async fetchInitialData({ + client, + auth, + query: { communityId, creatorId, q, type, sort, listingType, page }, + }: InitialFetchRequest>): Promise { + const community_id = getIdFromString(communityId); + let communityResponse: RequestState = { + state: "empty", + }; + let listCommunitiesResponse: RequestState = { + state: "empty", + }; + if (community_id) { + const getCommunityForm: GetCommunity = { + id: community_id, + auth, + }; - let creatorId = this.getCreatorIdFromProps(pathSplit[13]); - let creator_id: Option = creatorId == 0 ? None : Some(creatorId); - creator_id.match({ - some: id => { - let getCreatorForm = new GetPersonDetails({ - person_id: Some(id), - username: None, - sort: None, - page: None, - limit: None, - community_id: None, - saved_only: None, - auth: req.auth, - }); - promises.push(req.client.getPersonDetails(getCreatorForm)); - }, - none: () => { - promises.push(Promise.resolve()); - }, - }); + communityResponse = await client.getCommunity(getCommunityForm); + } else { + const listCommunitiesForm: ListCommunities = { + type_: defaultListingType, + sort: defaultSortType, + limit: fetchLimit, + auth, + }; - let form = new SearchForm({ - q: this.getSearchQueryFromProps(pathSplit[3]), - community_id, - community_name: None, - creator_id, - type_: Some(this.getSearchTypeFromProps(pathSplit[5])), - sort: Some(this.getSortTypeFromProps(pathSplit[7])), - listing_type: Some(this.getListingTypeFromProps(pathSplit[9])), - page: Some(this.getPageFromProps(pathSplit[15])), - limit: Some(fetchLimit), - auth: req.auth, - }); + listCommunitiesResponse = await client.listCommunities( + listCommunitiesForm + ); + } - let resolveObjectForm = new ResolveObject({ - q: this.getSearchQueryFromProps(pathSplit[3]), - auth: req.auth, - }); + const creator_id = getIdFromString(creatorId); + let creatorDetailsResponse: RequestState = { + state: "empty", + }; + if (creator_id) { + const getCreatorForm: GetPersonDetails = { + person_id: creator_id, + auth, + }; - if (form.q != "") { - promises.push(req.client.search(form)); - promises.push(req.client.resolveObject(resolveObjectForm)); - } else { - promises.push(Promise.resolve()); - promises.push(Promise.resolve()); + creatorDetailsResponse = await client.getPersonDetails(getCreatorForm); } - return promises; - } + const query = getSearchQueryFromQuery(q); - 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 - ) { - this.setState({ - loading: true, - searchText: this.state.q, - searchResponse: None, - resolveObjectResponse: None, - }); - this.search(); + let searchResponse: RequestState = { state: "empty" }; + let resolveObjectResponse: RequestState = { + state: "empty", + }; + + if (query) { + const form: SearchForm = { + q: query, + community_id, + creator_id, + type_: getSearchTypeFromQuery(type), + sort: getSortTypeFromQuery(sort), + listing_type: getListingTypeFromQuery(listingType), + page: getPageFromString(page), + limit: fetchLimit, + auth, + }; + + if (query !== "") { + searchResponse = await client.search(form); + if (auth) { + const resolveObjectForm: ResolveObject = { + q: query, + auth, + }; + resolveObjectResponse = await HttpService.silent_client.resolveObject( + resolveObjectForm + ); + + // If we return this object with a state of failed, the catch-all-handler will redirect + // to an error page, so we ignore it by covering up the error with the empty state. + if (resolveObjectResponse.state === "failed") { + resolveObjectResponse = { state: "empty" }; + } + } + } } + + return { + communityResponse, + creatorDetailsResponse, + listCommunitiesResponse, + resolveObjectResponse, + searchResponse, + }; } 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 `${I18NextService.i18n.t("search")} - ${q ? `${q} - ` : ""}${name}`; } render() { + const { type, page } = getSearchQueryParams(); + return ( -
+
-
{i18n.t("search")}
- {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 && {i18n.t("no_results")}} - +

{I18NextService.i18n.t("search")}

+ {this.selects} + {this.searchForm} + {this.displayResults(type)} + {this.resultsCount === 0 && + this.state.searchRes.state === "success" && ( + {I18NextService.i18n.t("no_results")} + )} +
); } - searchForm() { + displayResults(type: SearchType) { + switch (type) { + case "All": + return this.all; + case "Comments": + return this.comments; + case "Posts": + case "Url": + return this.posts; + case "Communities": + return this.communities; + case "Users": + return this.users; + default: + return <>; + } + } + + get searchForm() { return (
- - +
+ +
+
+ +
); } - selects() { + get selects() { + const { type, listingType, sort, communityId, creatorId } = + getSearchQueryParams(); + const { + communitySearchOptions, + creatorSearchOptions, + searchCommunitiesLoading, + searchCreatorLoading, + communitiesRes, + } = this.state; + + const hasCommunities = + communitiesRes.state == "success" && + communitiesRes.data.communities.length > 0; + return ( -
- - - - - - +
+
+ +
+
+ +
+
+ +
+
+
+ {hasCommunities && ( + + )} + - -
- {this.state.communities.length > 0 && this.communityFilter()} - {this.creatorFilter()}
-
+ ); } - 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 { + resolveObjectRes: resolveObjectResponse, + searchRes: searchResponse, + } = this.state; // Push the possible resolve / federated objects first - this.state.resolveObjectResponse.match({ - some: res => { - let resolveComment = res.comment; - if (resolveComment.isSome()) { - combined.push(this.commentViewToCombined(resolveComment.unwrap())); - } - let resolvePost = res.post; - if (resolvePost.isSome()) { - combined.push(this.postViewToCombined(resolvePost.unwrap())); - } - let resolveCommunity = res.community; - if (resolveCommunity.isSome()) { - combined.push( - this.communityViewToCombined(resolveCommunity.unwrap()) - ); - } - let resolveUser = res.person; - if (resolveUser.isSome()) { - combined.push(this.personViewSafeToCombined(resolveUser.unwrap())); - } - }, - none: void 0, - }); + if (resolveObjectResponse.state == "success") { + const { comment, post, community, person } = resolveObjectResponse.data; + + if (comment) { + combined.push(commentViewToCombined(comment)); + } + if (post) { + combined.push(postViewToCombined(post)); + } + if (community) { + combined.push(communityViewToCombined(community)); + } + if (person) { + combined.push(personViewSafeToCombined(person)); + } + } // Push the search results - this.state.searchResponse.match({ - some: res => { - pushNotNull( - combined, - res.comments?.map(e => this.commentViewToCombined(e)) - ); - pushNotNull( - combined, - res.posts?.map(e => this.postViewToCombined(e)) - ); - pushNotNull( - combined, - res.communities?.map(e => this.communityViewToCombined(e)) - ); - pushNotNull( - combined, - res.users?.map(e => this.personViewSafeToCombined(e)) - ); - }, - none: void 0, - }); + if (searchResponse.state === "success") { + const { comments, posts, communities, users } = searchResponse.data; + + 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 === "New") { combined.sort((a, b) => b.published.localeCompare(a.published)); } else { - combined.sort( - (a, b) => + combined.sort((a, b) => + Number( ((b.data as CommentView | PostView).counts.score | (b.data as CommunityView).counts.subscribers | - (b.data as PersonViewSafe).counts.comment_score) - - ((a.data as CommentView | PostView).counts.score | - (a.data as CommunityView).counts.subscribers | - (a.data as PersonViewSafe).counts.comment_score) + (b.data as PersonView).counts.comment_score) - + ((a.data as CommentView | PostView).counts.score | + (a.data as CommunityView).counts.subscribers | + (a.data as PersonView).counts.comment_score) + ) ); } + return combined; } - all() { - let combined = this.buildCombined(); + get all() { + const combined = this.buildCombined(); + return (
{combined.map(i => (
- {i.type_ == "posts" && ( + {i.type_ === "posts" && ( {}} + onPostVote={() => {}} + onPostReport={() => {}} + onBlockPerson={() => {}} + onLockPost={() => {}} + onDeletePost={() => {}} + onRemovePost={() => {}} + onSavePost={() => {}} + onFeaturePost={() => {}} + onPurgePerson={() => {}} + onPurgePost={() => {}} + onBanPersonFromCommunity={() => {}} + onBanPerson={() => {}} + onAddModToCommunity={() => {}} + onAddAdmin={() => {}} + onTransferCommunity={() => {}} /> )} - {i.type_ == "comments" && ( + {i.type_ === "comments" && ( { ]} viewType={CommentViewType.Flat} viewOnly - moderators={None} - admins={None} - maxCommentsShown={None} locked noIndent enableDownvotes={enableDownvotes(this.state.siteRes)} allLanguages={this.state.siteRes.all_languages} + siteLanguages={this.state.siteRes.discussion_languages} + // All of these are unused, since its viewonly + finished={new Map()} + onSaveComment={() => {}} + onBlockPerson={() => {}} + onDeleteComment={() => {}} + onRemoveComment={() => {}} + onCommentVote={() => {}} + onCommentReport={() => {}} + onDistinguishComment={() => {}} + onAddModToCommunity={() => {}} + onAddAdmin={() => {}} + onTransferCommunity={() => {}} + onPurgeComment={() => {}} + onPurgePerson={() => {}} + onCommentReplyRead={() => {}} + onPersonMentionRead={() => {}} + onBanPersonFromCommunity={() => {}} + onBanPerson={() => {}} + onCreateComment={() => Promise.resolve({ state: "empty" })} + onEditComment={() => Promise.resolve({ state: "empty" })} /> )} - {i.type_ == "communities" && ( -
{this.communityListing(i.data as CommunityView)}
+ {i.type_ === "communities" && ( +
{communityListing(i.data as CommunityView)}
)} - {i.type_ == "users" && ( -
{this.personListing(i.data as PersonViewSafe)}
+ {i.type_ === "users" && ( +
{personListing(i.data as PersonView)}
)}
@@ -632,17 +760,21 @@ export class Search extends Component { ); } - comments() { - let comments: CommentView[] = []; + get comments() { + const { + searchRes: searchResponse, + resolveObjectRes: resolveObjectResponse, + siteRes, + } = this.state; + const comments = + searchResponse.state === "success" ? searchResponse.data.comments : []; - this.state.resolveObjectResponse.match({ - some: res => pushNotNull(comments, res.comment), - none: void 0, - }); - this.state.searchResponse.match({ - some: res => pushNotNull(comments, res.comments), - none: void 0, - }); + if ( + resolveObjectResponse.state === "success" && + resolveObjectResponse.data.comment + ) { + comments.unshift(resolveObjectResponse.data.comment); + } return ( { viewOnly locked noIndent - moderators={None} - admins={None} - maxCommentsShown={None} - enableDownvotes={enableDownvotes(this.state.siteRes)} - allLanguages={this.state.siteRes.all_languages} + enableDownvotes={enableDownvotes(siteRes)} + allLanguages={siteRes.all_languages} + siteLanguages={siteRes.discussion_languages} + // All of these are unused, since its viewonly + finished={new Map()} + onSaveComment={() => {}} + onBlockPerson={() => {}} + onDeleteComment={() => {}} + onRemoveComment={() => {}} + onCommentVote={() => {}} + onCommentReport={() => {}} + onDistinguishComment={() => {}} + onAddModToCommunity={() => {}} + onAddAdmin={() => {}} + onTransferCommunity={() => {}} + onPurgeComment={() => {}} + onPurgePerson={() => {}} + onCommentReplyRead={() => {}} + onPersonMentionRead={() => {}} + onBanPersonFromCommunity={() => {}} + onBanPerson={() => {}} + onCreateComment={() => Promise.resolve({ state: "empty" })} + onEditComment={() => Promise.resolve({ state: "empty" })} /> ); } - posts() { - let posts: PostView[] = []; + get posts() { + const { + searchRes: searchResponse, + resolveObjectRes: resolveObjectResponse, + siteRes, + } = this.state; + const posts = + searchResponse.state === "success" ? searchResponse.data.posts : []; - this.state.resolveObjectResponse.match({ - some: res => pushNotNull(posts, res.post), - none: void 0, - }); - this.state.searchResponse.match({ - some: res => pushNotNull(posts, res.posts), - none: void 0, - }); + if ( + resolveObjectResponse.state === "success" && + resolveObjectResponse.data.post + ) { + posts.unshift(resolveObjectResponse.data.post); + } return ( <> @@ -680,13 +834,28 @@ export class Search extends Component { {}} + onPostVote={() => {}} + onPostReport={() => {}} + onBlockPerson={() => {}} + onLockPost={() => {}} + onDeletePost={() => {}} + onRemovePost={() => {}} + onSavePost={() => {}} + onFeaturePost={() => {}} + onPurgePerson={() => {}} + onPurgePost={() => {}} + onBanPersonFromCommunity={() => {}} + onBanPerson={() => {}} + onAddModToCommunity={() => {}} + onAddAdmin={() => {}} + onTransferCommunity={() => {}} />
@@ -695,293 +864,214 @@ export class Search extends Component { ); } - communities() { - let communities: CommunityView[] = []; + get communities() { + const { + searchRes: searchResponse, + resolveObjectRes: resolveObjectResponse, + } = this.state; + const communities = + searchResponse.state === "success" ? searchResponse.data.communities : []; - this.state.resolveObjectResponse.match({ - some: res => pushNotNull(communities, res.community), - none: void 0, - }); - this.state.searchResponse.match({ - some: res => pushNotNull(communities, res.communities), - none: void 0, - }); + if ( + resolveObjectResponse.state === "success" && + resolveObjectResponse.data.community + ) { + communities.unshift(resolveObjectResponse.data.community); + } return ( <> {communities.map(cv => (
-
{this.communityListing(cv)}
+
{communityListing(cv)}
))} ); } - users() { - let users: PersonViewSafe[] = []; + get users() { + const { + searchRes: searchResponse, + resolveObjectRes: resolveObjectResponse, + } = this.state; + const users = + searchResponse.state === "success" ? searchResponse.data.users : []; - this.state.resolveObjectResponse.match({ - some: res => pushNotNull(users, res.person), - none: void 0, - }); - this.state.searchResponse.match({ - some: res => pushNotNull(users, res.users), - none: void 0, - }); + if ( + resolveObjectResponse.state === "success" && + resolveObjectResponse.data.person + ) { + users.unshift(resolveObjectResponse.data.person); + } return ( <> {users.map(pvs => (
-
{this.personListing(pvs)}
+
{personListing(pvs)}
))} ); } - communityListing(community_view: CommunityView) { - return ( - <> - - - - {` - - ${i18n.t("number_of_subscribers", { - count: community_view.counts.subscribers, - formattedCount: numToSI(community_view.counts.subscribers), - })} - `} - - ); - } + get resultsCount(): number { + const { searchRes: r, resolveObjectRes: resolveRes } = this.state; + + const searchCount = + r.state === "success" + ? r.data.posts.length + + r.data.comments.length + + r.data.communities.length + + r.data.users.length + : 0; + + const resObjCount = + resolveRes.state === "success" + ? resolveRes.data.post || + resolveRes.data.person || + resolveRes.data.community || + resolveRes.data.comment + ? 1 + : 0 + : 0; - personListing(person_view: PersonViewSafe) { - return ( - <> - - - - {` - ${i18n.t("number_of_comments", { - count: person_view.counts.comment_count, - formattedCount: numToSI(person_view.counts.comment_count), - })}`} - - ); + return resObjCount + searchCount; } - communityFilter() { - return ( -
- -
- -
-
- ); - } + async search() { + const auth = myAuth(); + const { searchText: q } = this.state; + const { communityId, creatorId, type, sort, listingType, page } = + getSearchQueryParams(); - creatorFilter() { - return ( -
- -
- -
-
- ); + if (q) { + this.setState({ searchRes: { state: "loading" } }); + this.setState({ + searchRes: await HttpService.client.search({ + q, + community_id: communityId ?? undefined, + creator_id: creatorId ?? undefined, + type_: type, + sort, + listing_type: listingType, + page, + limit: fetchLimit, + auth, + }), + }); + window.scrollTo(0, 0); + restoreScrollPosition(this.context); + + if (auth) { + this.setState({ resolveObjectRes: { state: "loading" } }); + this.setState({ + resolveObjectRes: await HttpService.silent_client.resolveObject({ + q, + auth, + }), + }); + } + } } - resultsCount(): number { - let searchCount = this.state.searchResponse - .map( - r => - r.posts?.length + - r.comments?.length + - r.communities?.length + - r.users?.length - ) - .unwrapOr(0); - - let resObjCount = this.state.resolveObjectResponse - .map(r => (r.post || r.person || r.community || r.comment ? 1 : 0)) - .unwrapOr(0); + handleCreatorSearch = debounce(async (text: string) => { + const { creatorId } = getSearchQueryParams(); + const { creatorSearchOptions } = this.state; + const newOptions: Choice[] = []; - return resObjCount + searchCount; - } + this.setState({ searchCreatorLoading: true }); - handlePageChange(page: number) { - this.updateUrl({ page }); - } + const selectedChoice = creatorSearchOptions.find( + choice => getIdFromString(choice.value) === creatorId + ); + + if (selectedChoice) { + newOptions.push(selectedChoice); + } + + if (text.length > 0) { + newOptions.push(...(await fetchUsers(text)).map(personToChoice)); + } - search() { - let community_id: Option = - this.state.communityId == 0 ? None : Some(this.state.communityId); - let creator_id: Option = - this.state.creatorId == 0 ? None : Some(this.state.creatorId); - - let form = new SearchForm({ - q: this.state.q, - community_id, - community_name: None, - creator_id, - type_: Some(this.state.type_), - sort: Some(this.state.sort), - listing_type: Some(this.state.listingType), - page: Some(this.state.page), - limit: Some(fetchLimit), - auth: auth(false).ok(), + this.setState({ + searchCreatorLoading: false, + creatorSearchOptions: newOptions, }); + }); - let resolveObjectForm = new ResolveObject({ - q: this.state.q, - auth: auth(false).ok(), + handleCommunitySearch = debounce(async (text: string) => { + const { communityId } = getSearchQueryParams(); + const { communitySearchOptions } = this.state; + this.setState({ + searchCommunitiesLoading: true, }); - if (this.state.q != "") { - this.setState({ - searchResponse: None, - resolveObjectResponse: None, - loading: true, - }); - WebSocketService.Instance.send(wsClient.search(form)); - WebSocketService.Instance.send(wsClient.resolveObject(resolveObjectForm)); - } - } + const newOptions: Choice[] = []; - 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 - ); - } + const selectedChoice = communitySearchOptions.find( + choice => getIdFromString(choice.value) === communityId + ); + + 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 fetchCommunities(text)).map(communityToChoice)); } - } - handleSortChange(val: SortType) { - this.updateUrl({ sort: val, page: 1 }); + this.setState({ + searchCommunitiesLoading: false, + communitySearchOptions: newOptions, + }); + }); + + handleSortChange(sort: SortType) { + this.updateUrl({ sort, page: 1 }); } handleTypeChange(i: Search, event: any) { + const type = event.target.value as SearchType; + i.updateUrl({ - type_: SearchType[event.target.value], + type, page: 1, }); } - handleListingTypeChange(val: ListingType) { + handlePageChange(page: number) { + this.updateUrl({ page }); + } + + handleListingTypeChange(listingType: ListingType) { this.updateUrl({ - listingType: val, + listingType, page: 1, }); } - 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, }); } @@ -989,84 +1079,41 @@ export class Search extends Component { 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}` - ); - } - - parseMessage(msg: any) { - console.log(msg); - let op = wsUserOp(msg); - if (msg.error) { - if (msg.error == "couldnt_find_object") { - this.setState({ - resolveObjectResponse: Some({ - comment: None, - post: None, - community: None, - person: None, - }), - }); - this.checkFinishedLoading(); - } else { - toast(i18n.t(msg.error), "danger"); - return; - } - } else if (op == UserOperation.Search) { - let data = wsJsonToRes(msg, SearchResponse); - this.setState({ searchResponse: Some(data) }); - window.scrollTo(0, 0); - this.checkFinishedLoading(); - restoreScrollPosition(this.context); - } else if (op == UserOperation.CreateCommentLike) { - let data = wsJsonToRes(msg, CommentResponse); - createCommentLikeRes( - data.comment_view, - this.state.searchResponse.map(r => r.comments).unwrapOr([]) - ); - this.setState(this.state); - } else if (op == UserOperation.CreatePostLike) { - let data = wsJsonToRes(msg, PostResponse); - createPostLikeFindRes( - data.post_view, - this.state.searchResponse.map(r => r.posts).unwrapOr([]) - ); - this.setState(this.state); - } else if (op == UserOperation.ListCommunities) { - let data = wsJsonToRes( - msg, - ListCommunitiesResponse - ); - this.setState({ communities: data.communities }); - this.setupCommunityFilter(); - } else if (op == UserOperation.ResolveObject) { - let data = wsJsonToRes(msg, ResolveObjectResponse); - this.setState({ resolveObjectResponse: Some(data) }); - this.checkFinishedLoading(); + async updateUrl({ + q, + type, + listingType, + sort, + communityId, + creatorId, + page, + }: Partial) { + 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); } - } - checkFinishedLoading() { - if ( - this.state.searchResponse.isSome() && - this.state.resolveObjectResponse.isSome() - ) { - this.setState({ loading: false }); - } + const queryParams: QueryParams = { + 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)}`); } }