diff --git a/src/shared/components/communities.tsx b/src/shared/components/communities.tsx index 51b2105a..c52c4d32 100644 --- a/src/shared/components/communities.tsx +++ b/src/shared/components/communities.tsx @@ -273,7 +273,7 @@ export class Communities extends Component { handleSearchSubmit(i: Communities) { const searchParamEncoded = encodeURIComponent(i.state.searchText); i.context.router.history.push( - `/search/q/${searchParamEncoded}/type/Communities/sort/TopAll/listing_type/All/page/1` + `/search/q/${searchParamEncoded}/type/Communities/sort/TopAll/listing_type/All/community_id/0/page/1` ); } diff --git a/src/shared/components/create-post.tsx b/src/shared/components/create-post.tsx index a0d851a3..72d81705 100644 --- a/src/shared/components/create-post.tsx +++ b/src/shared/components/create-post.tsx @@ -25,6 +25,8 @@ import { SortType, ListingType, PostView, + GetCommunity, + GetCommunityResponse, } from "lemmy-js-client"; import { i18n } from "../i18next"; import { InitialFetchRequest, PostFormParams } from "shared/interfaces"; @@ -67,15 +69,27 @@ export class CreatePost extends Component { } refetch() { - let listCommunitiesForm: ListCommunities = { - type_: ListingType.All, - sort: SortType.TopAll, - limit: fetchLimit, - auth: authField(false), - }; - WebSocketService.Instance.send( - wsClient.listCommunities(listCommunitiesForm) - ); + if (this.params.community_id) { + let form: GetCommunity = { + id: this.params.community_id, + }; + WebSocketService.Instance.send(wsClient.getCommunity(form)); + } else if (this.params.community_name) { + let form: GetCommunity = { + name: this.params.community_name, + }; + WebSocketService.Instance.send(wsClient.getCommunity(form)); + } else { + let listCommunitiesForm: ListCommunities = { + type_: ListingType.All, + sort: SortType.TopAll, + limit: fetchLimit, + auth: authField(false), + }; + WebSocketService.Instance.send( + wsClient.listCommunities(listCommunitiesForm) + ); + } } componentWillUnmount() { @@ -181,6 +195,11 @@ export class CreatePost extends Component { this.state.communities = data.communities; this.state.loading = false; this.setState(this.state); + } else if (op == UserOperation.GetCommunity) { + let data = wsJsonToRes(msg).data; + this.state.communities = [data.community_view]; + this.state.loading = false; + this.setState(this.state); } } } diff --git a/src/shared/components/navbar.tsx b/src/shared/components/navbar.tsx index ccbe850a..efab35e6 100644 --- a/src/shared/components/navbar.tsx +++ b/src/shared/components/navbar.tsx @@ -134,7 +134,7 @@ export class Navbar extends Component { } else { const searchParamEncoded = encodeURIComponent(searchParam); this.context.router.history.push( - `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/page/1` + `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/page/1` ); } } diff --git a/src/shared/components/post-form.tsx b/src/shared/components/post-form.tsx index e4bf07f2..c8c42ff4 100644 --- a/src/shared/components/post-form.tsx +++ b/src/shared/components/post-form.tsx @@ -16,7 +16,6 @@ import { SearchType, SearchResponse, ListingType, - LemmyHttp, } from "lemmy-js-client"; import { WebSocketService, UserService } from "../services"; import { PostFormParams } from "../interfaces"; @@ -39,7 +38,8 @@ import { wsClient, authField, communityToChoice, - fetchLimit, + fetchCommunities, + choicesConfig, } from "../utils"; import autosize from "autosize"; @@ -49,7 +49,7 @@ if (isBrowser()) { } import { i18n } from "../i18next"; -import { httpBase, pictrsUri } from "../env"; +import { pictrsUri } from "../env"; const MAX_POST_TITLE_LENGTH = 200; @@ -456,20 +456,6 @@ export class PostForm extends Component { this.setState(this.state); } - async fetchCommunities(q: string) { - let form: Search = { - q, - type_: SearchType.Communities, - sort: SortType.TopAll, - listing_type: ListingType.All, - page: 1, - limit: fetchLimit, - auth: authField(false), - }; - let client = new LemmyHttp(httpBase); - return client.search(form); - } - handlePostBodyChange(val: string) { this.state.postForm.body = val; this.setState(this.state); @@ -556,37 +542,7 @@ export class PostForm extends Component { if (isBrowser()) { let selectId: any = document.getElementById("post-community"); if (selectId) { - this.choices = new Choices(selectId, { - shouldSort: false, - classNames: { - containerOuter: "choices", - containerInner: "choices__inner bg-light border-0", - input: "form-control", - inputCloned: "choices__input--cloned", - list: "choices__list", - listItems: "choices__list--multiple", - listSingle: "choices__list--single", - listDropdown: "choices__list--dropdown", - item: "choices__item bg-light", - itemSelectable: "choices__item--selectable", - itemDisabled: "choices__item--disabled", - itemChoice: "choices__item--choice", - placeholder: "choices__placeholder", - group: "choices__group", - groupHeading: "choices__heading", - button: "choices__button", - activeState: "is-active", - focusState: "is-focused", - openState: "is-open", - disabledState: "is-disabled", - highlightedState: "text-info", - selectedState: "text-info", - flippedState: "is-flipped", - loadingState: "is-loading", - noResults: "has-no-results", - noChoices: "has-no-choices", - }, - }); + this.choices = new Choices(selectId, choicesConfig); this.choices.passedElement.element.addEventListener( "choice", (e: any) => { @@ -598,7 +554,7 @@ export class PostForm extends Component { this.choices.passedElement.element.addEventListener( "search", debounce(async (e: any) => { - let communities = (await this.fetchCommunities(e.detail.value)) + let communities = (await fetchCommunities(e.detail.value)) .communities; this.choices.setChoices( communities.map(cv => communityToChoice(cv)), diff --git a/src/shared/components/search.tsx b/src/shared/components/search.tsx index b3029001..87699bd2 100644 --- a/src/shared/components/search.tsx +++ b/src/shared/components/search.tsx @@ -14,6 +14,9 @@ import { CommentResponse, Site, ListingType, + ListCommunities, + ListCommunitiesResponse, + GetCommunity, } from "lemmy-js-client"; import { WebSocketService } from "../services"; import { @@ -35,6 +38,12 @@ import { restoreScrollPosition, routeListingTypeToEnum, showLocal, + isBrowser, + choicesConfig, + debounce, + fetchCommunities, + communityToChoice, + hostname, } from "../utils"; import { PostListing } from "./post-listing"; import { HtmlTags } from "./html-tags"; @@ -47,11 +56,17 @@ import { CommentNodes } from "./comment-nodes"; import { i18n } from "../i18next"; import { InitialFetchRequest } from "shared/interfaces"; +var Choices; +if (isBrowser()) { + Choices = require("choices.js"); +} + interface SearchProps { q: string; type_: SearchType; sort: SortType; listingType: ListingType; + communityId: number; page: number; } @@ -60,8 +75,10 @@ interface SearchState { type_: SearchType; sort: SortType; listingType: ListingType; + communityId: number; page: number; searchResponse: SearchResponse; + communities: CommunityView[]; loading: boolean; site: Site; searchText: string; @@ -72,11 +89,13 @@ interface UrlParams { type_?: SearchType; sort?: SortType; listingType?: ListingType; + communityId?: number; page?: number; } export class Search extends Component { private isoData = setIsoData(this.context); + private choices: any; private subscription: Subscription; private emptyState: SearchState = { q: Search.getSearchQueryFromProps(this.props.match.params.q), @@ -87,6 +106,9 @@ export class Search extends Component { ), 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 + ), searchResponse: { type_: null, posts: [], @@ -96,6 +118,7 @@ export class Search extends Component { }, loading: true, site: this.isoData.site_res.site_view.site, + communities: [], }; static getSearchQueryFromProps(q: string): string { @@ -114,6 +137,10 @@ export class Search extends Component { return listingType ? routeListingTypeToEnum(listingType) : ListingType.All; } + static getCommunityIdFromProps(id: string): number { + return id ? Number(id) : 0; + } + static getPageFromProps(page: string): number { return page ? Number(page) : 1; } @@ -129,13 +156,22 @@ export class Search extends Component { this.subscription = wsSubscribe(this.parseMessage); // Only fetch the data if coming from another route - if (this.state.q != "") { - if (this.isoData.path == this.context.router.route.match.url) { - this.state.searchResponse = this.isoData.routeData[0]; + if (this.isoData.path == this.context.router.route.match.url) { + let singleOrMultipleCommunities = this.isoData.routeData[0]; + if (singleOrMultipleCommunities.communities) { + this.state.communities = this.isoData.routeData[0].communities; + } else { + this.state.communities = [this.isoData.routeData[0].community_view]; + } + if (this.state.q != "") { + this.state.searchResponse = this.isoData.routeData[1]; this.state.loading = false; } else { this.search(); } + } else { + this.fetchCommunities(); + this.search(); } } @@ -144,6 +180,10 @@ export class Search extends Component { saveScrollPosition(this.context); } + componentDidMount() { + this.setupCommunityFilter(); + } + static getDerivedStateFromProps(props: any): SearchProps { return { q: Search.getSearchQueryFromProps(props.match.params.q), @@ -152,22 +192,57 @@ export class Search extends Component { listingType: Search.getListingTypeFromProps( props.match.params.listing_type ), + communityId: Search.getCommunityIdFromProps( + props.match.params.community_id + ), page: Search.getPageFromProps(props.match.params.page), }; } + fetchCommunities() { + let listCommunitiesForm: ListCommunities = { + type_: ListingType.All, + sort: SortType.TopAll, + limit: fetchLimit, + auth: authField(false), + }; + WebSocketService.Instance.send( + wsClient.listCommunities(listCommunitiesForm) + ); + } + static fetchInitialData(req: InitialFetchRequest): Promise[] { let pathSplit = req.path.split("/"); let promises: Promise[] = []; + let cId = this.getCommunityIdFromProps(pathSplit[11]); + if (cId !== 0) { + let f: GetCommunity = { + id: cId, + }; + setOptionalAuth(f, req.auth); + promises.push(req.client.getCommunity(f)); + } else { + let listCommunitiesForm: ListCommunities = { + type_: ListingType.All, + sort: SortType.TopAll, + limit: fetchLimit, + }; + setOptionalAuth(listCommunitiesForm, req.auth); + promises.push(req.client.listCommunities(listCommunitiesForm)); + } + let form: SearchForm = { q: this.getSearchQueryFromProps(pathSplit[3]), type_: this.getSearchTypeFromProps(pathSplit[5]), sort: this.getSortTypeFromProps(pathSplit[7]), listing_type: this.getListingTypeFromProps(pathSplit[9]), - page: this.getPageFromProps(pathSplit[11]), + page: this.getPageFromProps(pathSplit[13]), limit: fetchLimit, }; + if (cId !== 0) { + form.community_id = cId; + } setOptionalAuth(form, req.auth); if (form.q != "") { @@ -183,6 +258,7 @@ export class Search extends Component { lastState.type_ !== this.state.type_ || lastState.sort !== this.state.sort || lastState.listingType !== this.state.listingType || + lastState.communityId !== this.state.communityId || lastState.page !== this.state.page ) { this.setState({ loading: true, searchText: this.state.q }); @@ -277,6 +353,7 @@ export class Search extends Component { hideMostComments /> + {this.state.communities.length > 0 && this.communityFilter()} ); } @@ -440,6 +517,32 @@ export class Search extends Component { ); } + communityFilter() { + return ( +
+ +
+ +
+
+ ); + } + paginator() { return (
@@ -492,12 +595,45 @@ export class Search extends Component { limit: fetchLimit, auth: authField(false), }; + if (this.state.communityId !== 0) { + form.community_id = this.state.communityId; + } if (this.state.q != "") { WebSocketService.Instance.send(wsClient.search(form)); } } + setupCommunityFilter() { + if (isBrowser()) { + let selectId: any = document.getElementById("community-filter"); + if (selectId) { + this.choices = new Choices(selectId, choicesConfig); + this.choices.passedElement.element.addEventListener( + "choice", + (e: any) => { + this.handleCommunityFilterChange(Number(e.detail.choice.value)); + }, + false + ); + this.choices.passedElement.element.addEventListener( + "search", + debounce(async (e: any) => { + let communities = (await fetchCommunities(e.detail.value)) + .communities; + this.choices.setChoices( + communities.map(cv => communityToChoice(cv)), + "value", + "label", + true + ); + }, 400), + false + ); + } + } + } + handleSortChange(val: SortType) { this.updateUrl({ sort: val, page: 1 }); } @@ -516,12 +652,20 @@ export class Search extends Component { }); } + handleCommunityFilterChange(communityId: number) { + this.updateUrl({ + communityId, + 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, sort: i.state.sort, page: i.state.page, }); @@ -537,9 +681,11 @@ export class Search extends Component { 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" || this.state.communityId; const page = paramUpdates.page || this.state.page; this.props.history.push( - `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/listing_type/${listingTypeStr}/page/${page}` + `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/listing_type/${listingTypeStr}/community_id/${communityId}/page/${page}` ); } @@ -567,6 +713,11 @@ export class Search extends Component { let data = wsJsonToRes(msg).data; createPostLikeFindRes(data.post_view, this.state.searchResponse.posts); this.setState(this.state); + } else if (op == UserOperation.ListCommunities) { + let data = wsJsonToRes(msg).data; + this.state.communities = data.communities; + this.setState(this.state); + this.setupCommunityFilter(); } } } diff --git a/src/shared/routes.ts b/src/shared/routes.ts index e134419e..a231c6dc 100644 --- a/src/shared/routes.ts +++ b/src/shared/routes.ts @@ -133,7 +133,7 @@ export const routes: IRoutePropsWithFetch[] = [ fetchInitialData: req => AdminSettings.fetchInitialData(req), }, { - path: `/search/q/:q/type/:type/sort/:sort/listing_type/:listing_type/page/:page`, + path: `/search/q/:q/type/:type/sort/:sort/listing_type/:listing_type/community_id/:community_id/page/:page`, component: Search, fetchInitialData: req => Search.fetchInitialData(req), }, diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 7dcf7ff8..a5c88156 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -46,6 +46,7 @@ import { LemmyWebsocket, PersonViewSafe, CommunityView, + LemmyHttp, } from "lemmy-js-client"; import { @@ -70,6 +71,7 @@ import moment from "moment"; import { Subscription } from "rxjs"; import { retryWhen, delay, take } from "rxjs/operators"; import { i18n } from "./i18next"; +import { httpBase } from "./env"; export const wsClient = new LemmyWebsocket(); @@ -1245,3 +1247,49 @@ export function communityToChoice(cv: CommunityView): ChoicesValue { }; return choice; } + +export async function fetchCommunities(q: string) { + let form: Search = { + q, + type_: SearchType.Communities, + sort: SortType.TopAll, + listing_type: ListingType.All, + page: 1, + limit: fetchLimit, + auth: authField(false), + }; + let client = new LemmyHttp(httpBase); + return client.search(form); +} + +export const choicesConfig = { + shouldSort: false, + classNames: { + containerOuter: "choices", + containerInner: "choices__inner bg-light border-0", + input: "form-control", + inputCloned: "choices__input--cloned", + list: "choices__list", + listItems: "choices__list--multiple", + listSingle: "choices__list--single", + listDropdown: "choices__list--dropdown", + item: "choices__item bg-light", + itemSelectable: "choices__item--selectable", + itemDisabled: "choices__item--disabled", + itemChoice: "choices__item--choice", + placeholder: "choices__placeholder", + group: "choices__group", + groupHeading: "choices__heading", + button: "choices__button", + activeState: "is-active", + focusState: "is-focused", + openState: "is-open", + disabledState: "is-disabled", + highlightedState: "text-info", + selectedState: "text-info", + flippedState: "is-flipped", + loadingState: "is-loading", + noResults: "has-no-results", + noChoices: "has-no-choices", + }, +};