X-Git-Url: http://these/git/?a=blobdiff_plain;f=src%2Fshared%2Fcomponents%2Fsearch.tsx;h=99b180356b87c5ab89f789a795c49c81f9ef8645;hb=53c3cfeade90150b07431386745a24aa699a25ec;hp=e36c9ad18df6df8ba6404eb64e12d496f3da1117;hpb=fe0ebbeec1d8422ab1812cadf06550da97ad0573;p=lemmy-ui.git diff --git a/src/shared/components/search.tsx b/src/shared/components/search.tsx index e36c9ad..99b1803 100644 --- a/src/shared/components/search.tsx +++ b/src/shared/components/search.tsx @@ -1,6 +1,31 @@ +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, @@ -11,8 +36,7 @@ import { ListCommunities, ListCommunitiesResponse, ListingType, - PersonViewSafe, - PostResponse, + PersonView, PostView, ResolveObject, ResolveObjectResponse, @@ -20,153 +44,213 @@ 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 { - capitalizeFirstLetter, - choicesConfig, - commentsToFlatNodes, - communitySelectName, - communityToChoice, - createCommentLikeRes, - createPostLikeFindRes, - debounce, - enableDownvotes, - enableNsfw, - fetchCommunities, - fetchLimit, - fetchUsers, - isBrowser, - myAuth, - 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; + 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?: SearchResponse; - communities: CommunityView[]; - creatorDetails?: GetPersonDetailsResponse; - loading: boolean; + searchRes: RequestState; + resolveObjectRes: RequestState; + creatorDetailsRes: RequestState; + communitiesRes: RequestState; + communityRes: RequestState; 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[]; + 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); - 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, - siteRes: this.isoData.site_res, - communities: [], - }; +const defaultSearchType = "All"; +const defaultSortType = "TopAll"; +const defaultListingType = "All"; - static getSearchQueryFromProps(q?: string): string | undefined { - return q ? decodeURIComponent(q) : undefined; - } +const searchTypes = ["All", "Comments", "Posts", "Communities", "Users", "Url"]; - static getSearchTypeFromProps(type_: string): SearchType { - return type_ ? routeSearchTypeToEnum(type_) : SearchType.All; - } +const getSearchQueryParams = () => + getQueryParams({ + q: getSearchQueryFromQuery, + type: getSearchTypeFromQuery, + sort: getSortTypeFromQuery, + listingType: getListingTypeFromQuery, + communityId: getIdFromString, + creatorId: getIdFromString, + page: getPageFromString, + }); - static getSortTypeFromProps(sort: string): SortType { - return sort ? routeSortTypeToEnum(sort) : SortType.TopAll; - } +const getSearchQueryFromQuery = (q?: string): string | undefined => + q ? decodeURIComponent(q) : undefined; - static getListingTypeFromProps(listingType: string): ListingType { - return listingType ? routeListingTypeToEnum(listingType) : ListingType.All; - } +function getSearchTypeFromQuery(type_?: string): SearchType { + return type_ ? (type_ as SearchType) : defaultSearchType; +} - static getCommunityIdFromProps(id: string): number { - return id ? Number(id) : 0; - } +function getSortTypeFromQuery(sort?: string): SortType { + return sort ? (sort as SortType) : defaultSortType; +} - static getCreatorIdFromProps(id: string): number { - return id ? Number(id) : 0; - } +function getListingTypeFromQuery(listingType?: string): ListingType { + return listingType ? (listingType as ListingType) : defaultListingType; +} - static getPageFromProps(page: string): number { - return page ? Number(page) : 1; - } +function postViewToCombined(data: PostView): Combined { + return { + type_: "posts", + data, + published: data.post.published, + }; +} + +function commentViewToCombined(data: CommentView): Combined { + return { + type_: "comments", + data, + published: data.comment.published, + }; +} + +function communityViewToCombined(data: CommunityView): Combined { + return { + type_: "communities", + data, + published: data.community.published, + }; +} + +function personViewSafeToCombined(data: PersonView): Combined { + return { + 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 ( +
+ + +
+ ); +}; + +const communityListing = ({ + community, + counts: { subscribers }, +}: CommunityView) => + getListing( + , + subscribers, + "number_of_subscribers" + ); + +const personListing = ({ person, counts: { comment_count } }: PersonView) => + getListing( + , + comment_count, + "number_of_comments" + ); + +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), + })}`} + + ); +} + +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); @@ -174,390 +258,428 @@ export class Search extends Component { 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 - | GetCommunityResponse - | undefined; - let communitiesRes = this.isoData.routeData[1] as - | ListCommunitiesResponse - | undefined; - // This can be single or multiple communities given - if (communitiesRes) { + if (FirstLoadService.isFirstLoad) { + const { + communityResponse: communityRes, + creatorDetailsResponse: creatorDetailsRes, + listCommunitiesResponse: communitiesRes, + resolveObjectResponse: resolveObjectRes, + searchResponse: searchRes, + } = this.isoData.routeData; + + this.state = { + ...this.state, + isIsomorphic: true, + }; + + if (creatorDetailsRes?.state === "success") { this.state = { ...this.state, - communities: communitiesRes.communities, + creatorSearchOptions: + creatorDetailsRes?.state === "success" + ? [personToChoice(creatorDetailsRes.data.person_view)] + : [], + creatorDetailsRes, }; } - if (communityRes) { + if (communitiesRes?.state === "success") { this.state = { ...this.state, - communities: [communityRes.community_view], + communitiesRes, }; } - this.state = { - ...this.state, - creatorDetails: this.isoData.routeData[2] as GetPersonDetailsResponse, - }; - - if (this.state.q != "") { + if (communityRes?.state === "success") { this.state = { ...this.state, - searchResponse: this.isoData.routeData[3] as SearchResponse, - resolveObjectResponse: this.isoData - .routeData[4] as ResolveObjectResponse, - loading: false, + communityRes, }; - } else { - this.search(); } - } else { - this.fetchCommunities(); - if (this.state.q) { - this.search(); + if (q !== "") { + this.state = { + ...this.state, + }; + + if (searchRes?.state === "success") { + this.state = { + ...this.state, + searchRes, + }; + } + + if (resolveObjectRes?.state === "success") { + this.state = { + ...this.state, + resolveObjectRes, + }; + } } } } - 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, - 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), - }; + 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: ListCommunities = { - type_: ListingType.All, - sort: SortType.TopAll, - limit: fetchLimit, - auth: myAuth(false), - }; - WebSocketService.Instance.send( - wsClient.listCommunities(listCommunitiesForm) - ); + componentWillUnmount() { + saveScrollPosition(this.context); } - static fetchInitialData(req: InitialFetchRequest): Promise[] { - let pathSplit = req.path.split("/"); - let promises: Promise[] = []; - let auth = req.auth; - - let communityId = this.getCommunityIdFromProps(pathSplit[11]); - let community_id = communityId == 0 ? undefined : communityId; + 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) { - let getCommunityForm: GetCommunity = { + const getCommunityForm: GetCommunity = { id: community_id, auth, }; - promises.push(req.client.getCommunity(getCommunityForm)); - promises.push(Promise.resolve()); + + communityResponse = await client.getCommunity(getCommunityForm); } 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)); + + listCommunitiesResponse = await client.listCommunities( + listCommunitiesForm + ); } - let creatorId = this.getCreatorIdFromProps(pathSplit[13]); - let creator_id = creatorId == 0 ? undefined : creatorId; + const creator_id = getIdFromString(creatorId); + let creatorDetailsResponse: RequestState = { + state: "empty", + }; if (creator_id) { - let getCreatorForm: GetPersonDetails = { + const getCreatorForm: GetPersonDetails = { person_id: creator_id, - auth: req.auth, + auth, }; - promises.push(req.client.getPersonDetails(getCreatorForm)); - } else { - promises.push(Promise.resolve()); + + creatorDetailsResponse = await client.getPersonDetails(getCreatorForm); } - let q = this.getSearchQueryFromProps(pathSplit[3]); + const query = getSearchQueryFromQuery(q); - if (q) { - let form: SearchForm = { - q, + let searchResponse: RequestState = { state: "empty" }; + let resolveObjectResponse: RequestState = { + state: "empty", + }; + + 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: getPageFromString(page), limit: fetchLimit, - auth: req.auth, - }; - - let resolveObjectForm: ResolveObject = { - q, - auth: req.auth, + 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()); + 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 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(); - } - } + 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; - 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.state == "success") { + const { comment, post, community, person } = resolveObjectResponse.data; + + 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.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" && ( { allLanguages={this.state.siteRes.all_languages} siteLanguages={this.state.siteRes.discussion_languages} viewOnly + // All of these are unused, since its view only + onPostEdit={() => {}} + onPostVote={() => {}} + onPostReport={() => {}} + onBlockPerson={() => {}} + onLockPost={() => {}} + onDeletePost={() => {}} + onRemovePost={() => {}} + onSavePost={() => {}} + onFeaturePost={() => {}} + onPurgePerson={() => {}} + onPurgePost={() => {}} + onBanPersonFromCommunity={() => {}} + onBanPerson={() => {}} + onAddModToCommunity={() => {}} + onAddAdmin={() => {}} + onTransferCommunity={() => {}} /> )} - {i.type_ == "comments" && ( + {i.type_ === "comments" && ( { 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)}
)}
@@ -601,10 +760,21 @@ export class Search extends Component { ); } - comments() { - let comments: CommentView[] = []; - pushNotNull(comments, this.state.resolveObjectResponse?.comment); - pushNotNull(comments, this.state.searchResponse?.comments); + get comments() { + const { + searchRes: searchResponse, + resolveObjectRes: resolveObjectResponse, + siteRes, + } = this.state; + const comments = + searchResponse.state === "success" ? searchResponse.data.comments : []; + + if ( + resolveObjectResponse.state === "success" && + resolveObjectResponse.data.comment + ) { + comments.unshift(resolveObjectResponse.data.comment); + } return ( { 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} + // 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 : []; - pushNotNull(posts, this.state.resolveObjectResponse?.post); - pushNotNull(posts, this.state.searchResponse?.posts); + if ( + resolveObjectResponse.state === "success" && + resolveObjectResponse.data.post + ) { + posts.unshift(resolveObjectResponse.data.post); + } return ( <> @@ -634,11 +834,28 @@ export class Search extends Component { {}} + onPostVote={() => {}} + onPostReport={() => {}} + onBlockPerson={() => {}} + onLockPost={() => {}} + onDeletePost={() => {}} + onRemovePost={() => {}} + onSavePost={() => {}} + onFeaturePost={() => {}} + onPurgePerson={() => {}} + onPurgePost={() => {}} + onBanPersonFromCommunity={() => {}} + onBanPerson={() => {}} + onAddModToCommunity={() => {}} + onAddAdmin={() => {}} + onTransferCommunity={() => {}} />
@@ -647,290 +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 : []; - pushNotNull(communities, this.state.resolveObjectResponse?.community); - pushNotNull(communities, this.state.searchResponse?.communities); + 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 : []; - pushNotNull(users, this.state.resolveObjectResponse?.person); - pushNotNull(users, this.state.searchResponse?.users); + 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(); + + 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, + }), + }); + } + } } - creatorFilter() { - let creatorPv = this.state.creatorDetails?.person_view; - return ( -
- -
- -
-
+ handleCreatorSearch = debounce(async (text: string) => { + const { creatorId } = getSearchQueryParams(); + const { creatorSearchOptions } = this.state; + const newOptions: Choice[] = []; + + this.setState({ searchCreatorLoading: true }); + + const selectedChoice = creatorSearchOptions.find( + choice => getIdFromString(choice.value) === creatorId ); - } - resultsCount(): number { - let r = this.state.searchResponse; - - let searchCount = r - ? r.posts?.length + - r.comments?.length + - r.communities?.length + - r.users?.length - : 0; - - let resolveRes = this.state.resolveObjectResponse; - let resObjCount = resolveRes - ? resolveRes.post || - resolveRes.person || - resolveRes.community || - resolveRes.comment - ? 1 - : 0 - : 0; + if (selectedChoice) { + newOptions.push(selectedChoice); + } - return resObjCount + searchCount; - } + if (text.length > 0) { + newOptions.push(...(await fetchUsers(text)).map(personToChoice)); + } - handlePageChange(page: number) { - this.updateUrl({ page }); - } + this.setState({ + searchCreatorLoading: false, + creatorSearchOptions: newOptions, + }); + }); - search() { - let community_id = - this.state.communityId == 0 ? undefined : this.state.communityId; - let creator_id = - this.state.creatorId == 0 ? undefined : this.state.creatorId; + handleCommunitySearch = debounce(async (text: string) => { + const { communityId } = getSearchQueryParams(); + const { communitySearchOptions } = this.state; + this.setState({ + searchCommunitiesLoading: true, + }); - 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, - limit: fetchLimit, - auth, - }; + const newOptions: Choice[] = []; - let resolveObjectForm: ResolveObject = { - q: this.state.q, - auth, - }; + const selectedChoice = communitySearchOptions.find( + choice => getIdFromString(choice.value) === communityId + ); - this.setState({ - searchResponse: undefined, - resolveObjectResponse: undefined, - loading: true, - }); - WebSocketService.Instance.send(wsClient.search(form)); - WebSocketService.Instance.send(wsClient.resolveObject(resolveObjectForm)); + if (selectedChoice) { + newOptions.push(selectedChoice); } - } - 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 - ); - } + if (text.length > 0) { + newOptions.push(...(await fetchCommunities(text)).map(communityToChoice)); } - } - 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 - ); - } - } - } + this.setState({ + searchCommunitiesLoading: false, + communitySearchOptions: newOptions, + }); + }); - handleSortChange(val: SortType) { - const updateObj = { sort: val, page: 1 }; - this.setState(updateObj); - this.updateUrl(updateObj); + handleSortChange(sort: SortType) { + this.updateUrl({ sort, page: 1 }); } handleTypeChange(i: Search, event: any) { - const updateObj = { - type_: SearchType[event.target.value], + const type = event.target.value as SearchType; + + i.updateUrl({ + type, page: 1, - }; - i.setState(updateObj); - i.updateUrl(updateObj); + }); + } + + handlePageChange(page: number) { + this.updateUrl({ page }); } - handleListingTypeChange(val: ListingType) { - const updateObj = { - listingType: val, + 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, }); } @@ -938,70 +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: {}, - }); - this.checkFinishedLoading(); - } else { - toast(i18n.t(msg.error), "danger"); - return; - } - } else if (op == UserOperation.Search) { - let data = wsJsonToRes(msg); - this.setState({ searchResponse: data }); - window.scrollTo(0, 0); - this.checkFinishedLoading(); - restoreScrollPosition(this.context); - } else if (op == UserOperation.CreateCommentLike) { - let data = wsJsonToRes(msg); - createCommentLikeRes( - data.comment_view, - this.state.searchResponse?.comments - ); - this.setState(this.state); - } else if (op == UserOperation.CreatePostLike) { - let data = wsJsonToRes(msg); - createPostLikeFindRes(data.post_view, this.state.searchResponse?.posts); - this.setState(this.state); - } else if (op == UserOperation.ListCommunities) { - let data = wsJsonToRes(msg); - this.setState({ communities: data.communities }); - this.setupCommunityFilter(); - } else if (op == UserOperation.ResolveObject) { - let data = wsJsonToRes(msg); - this.setState({ resolveObjectResponse: 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 && this.state.resolveObjectResponse) { - 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)}`); } }