import type { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import {
- CommentResponse,
CommentView,
CommunityView,
GetCommunity,
ListCommunitiesResponse,
ListingType,
PersonView,
- PostResponse,
PostView,
ResolveObject,
ResolveObjectResponse,
SearchResponse,
SearchType,
SortType,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
-import { Subscription } from "rxjs";
import { i18n } from "../i18next";
import { CommentViewType, InitialFetchRequest } from "../interfaces";
-import { WebSocketService } from "../services";
+import { FirstLoadService } from "../services/FirstLoadService";
+import { HttpService, RequestState } from "../services/HttpService";
import {
Choice,
QueryParams,
- WithPromiseKeys,
+ RouteDataResponse,
capitalizeFirstLetter,
commentsToFlatNodes,
communityToChoice,
- createCommentLikeRes,
- createPostLikeFindRes,
debounce,
enableDownvotes,
enableNsfw,
saveScrollPosition,
setIsoData,
showLocal,
- toast,
- wsClient,
- wsSubscribe,
} from "../utils";
import { CommentNodes } from "./comment/comment-nodes";
import { HtmlTags } from "./common/html-tags";
page: number;
}
-interface SearchData {
+type SearchData = RouteDataResponse<{
communityResponse?: GetCommunityResponse;
listCommunitiesResponse?: ListCommunitiesResponse;
creatorDetailsResponse?: GetPersonDetailsResponse;
searchResponse?: SearchResponse;
resolveObjectResponse?: ResolveObjectResponse;
-}
+}>;
type FilterType = "creator" | "community";
interface SearchState {
- searchResponse?: SearchResponse;
- communities: CommunityView[];
- creatorDetails?: GetPersonDetailsResponse;
- searchLoading: boolean;
- searchCommunitiesLoading: boolean;
- searchCreatorLoading: boolean;
+ searchRes: RequestState<SearchResponse>;
+ resolveObjectRes: RequestState<ResolveObjectResponse>;
+ creatorDetailsRes: RequestState<GetPersonDetailsResponse>;
+ communitiesRes: RequestState<ListCommunitiesResponse>;
+ communityRes: RequestState<GetCommunityResponse>;
siteRes: GetSiteResponse;
searchText?: string;
- resolveObjectResponse?: ResolveObjectResponse;
communitySearchOptions: Choice[];
creatorSearchOptions: Choice[];
+ searchCreatorLoading: boolean;
+ searchCommunitiesLoading: boolean;
+ isIsomorphic: boolean;
}
interface Combined {
export class Search extends Component<any, SearchState> {
private isoData = setIsoData<SearchData>(this.context);
- private subscription?: Subscription;
+
state: SearchState = {
- searchLoading: false,
+ resolveObjectRes: { state: "empty" },
+ creatorDetailsRes: { state: "empty" },
+ communitiesRes: { state: "empty" },
+ communityRes: { state: "empty" },
siteRes: this.isoData.site_res,
- communities: [],
- searchCommunitiesLoading: false,
- searchCreatorLoading: false,
creatorSearchOptions: [],
communitySearchOptions: [],
+ searchRes: { state: "empty" },
+ searchCreatorLoading: false,
+ searchCommunitiesLoading: false,
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
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 = {
};
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
+ if (FirstLoadService.isFirstLoad) {
const {
- communityResponse,
- creatorDetailsResponse,
- listCommunitiesResponse,
- resolveObjectResponse,
- searchResponse,
+ communityResponse: communityRes,
+ creatorDetailsResponse: creatorDetailsRes,
+ listCommunitiesResponse: communitiesRes,
+ resolveObjectResponse: resolveObjectRes,
+ searchResponse: searchRes,
} = this.isoData.routeData;
- // This can be single or multiple communities given
- if (listCommunitiesResponse) {
+ this.state = {
+ ...this.state,
+ isIsomorphic: true,
+ };
+
+ if (creatorDetailsRes?.state === "success") {
this.state = {
...this.state,
- communities: listCommunitiesResponse.communities,
+ creatorSearchOptions:
+ creatorDetailsRes?.state === "success"
+ ? [personToChoice(creatorDetailsRes.data.person_view)]
+ : [],
+ creatorDetailsRes,
};
}
- if (communityResponse) {
+
+ if (communitiesRes?.state === "success") {
this.state = {
...this.state,
- communities: [communityResponse.community_view],
- communitySearchOptions: [
- communityToChoice(communityResponse.community_view),
- ],
+ communitiesRes,
};
}
- this.state = {
- ...this.state,
- creatorDetails: creatorDetailsResponse,
- creatorSearchOptions: creatorDetailsResponse
- ? [personToChoice(creatorDetailsResponse.person_view)]
- : [],
- };
+ if (communityRes?.state === "success") {
+ this.state = {
+ ...this.state,
+ communityRes,
+ };
+ }
if (q !== "") {
this.state = {
...this.state,
- searchResponse,
- resolveObjectResponse,
- searchLoading: false,
};
- } else {
- this.search();
- }
- } else {
- const listCommunitiesForm: ListCommunities = {
- type_: defaultListingType,
- sort: defaultSortType,
- limit: fetchLimit,
- auth: myAuth(false),
- };
- WebSocketService.Instance.send(
- wsClient.listCommunities(listCommunitiesForm)
- );
+ if (searchRes?.state === "success") {
+ this.state = {
+ ...this.state,
+ searchRes,
+ };
+ }
- if (q) {
- this.search();
+ if (resolveObjectRes?.state === "success") {
+ this.state = {
+ ...this.state,
+ resolveObjectRes,
+ };
+ }
}
}
}
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ const promises = [this.fetchCommunities()];
+ if (this.state.searchText) {
+ promises.push(this.search());
+ }
+
+ await Promise.all(promises);
+ }
+ }
+
+ async fetchCommunities() {
+ this.setState({ communitiesRes: { state: "loading" } });
+ this.setState({
+ communitiesRes: await HttpService.client.listCommunities({
+ type_: defaultListingType,
+ sort: defaultSortType,
+ limit: fetchLimit,
+ auth: myAuth(),
+ }),
+ });
+ }
+
componentWillUnmount() {
- this.subscription?.unsubscribe();
saveScrollPosition(this.context);
}
- static fetchInitialData({
+ static async fetchInitialData({
client,
auth,
query: { communityId, creatorId, q, type, sort, listingType, page },
- }: InitialFetchRequest<
- QueryParams<SearchProps>
- >): WithPromiseKeys<SearchData> {
+ }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<SearchData> {
const community_id = getIdFromString(communityId);
- let communityResponse: Promise<GetCommunityResponse> | undefined =
- undefined;
- let listCommunitiesResponse: Promise<ListCommunitiesResponse> | undefined =
+ let communityResponse: RequestState<GetCommunityResponse> | undefined =
undefined;
+ let listCommunitiesResponse:
+ | RequestState<ListCommunitiesResponse>
+ | undefined = undefined;
if (community_id) {
const getCommunityForm: GetCommunity = {
id: community_id,
auth,
};
- communityResponse = client.getCommunity(getCommunityForm);
+ communityResponse = await client.getCommunity(getCommunityForm);
} else {
const listCommunitiesForm: ListCommunities = {
type_: defaultListingType,
auth,
};
- listCommunitiesResponse = client.listCommunities(listCommunitiesForm);
+ listCommunitiesResponse = await client.listCommunities(
+ listCommunitiesForm
+ );
}
const creator_id = getIdFromString(creatorId);
- let creatorDetailsResponse: Promise<GetPersonDetailsResponse> | undefined =
- undefined;
+ let creatorDetailsResponse:
+ | RequestState<GetPersonDetailsResponse>
+ | undefined = undefined;
if (creator_id) {
const getCreatorForm: GetPersonDetails = {
person_id: creator_id,
auth,
};
- creatorDetailsResponse = client.getPersonDetails(getCreatorForm);
+ creatorDetailsResponse = await client.getPersonDetails(getCreatorForm);
}
const query = getSearchQueryFromQuery(q);
- let searchResponse: Promise<SearchResponse> | undefined = undefined;
- let resolveObjectResponse:
- | Promise<ResolveObjectResponse | undefined>
- | undefined = undefined;
+ let searchResponse: RequestState<SearchResponse> | undefined = undefined;
+ let resolveObjectResponse: RequestState<ResolveObjectResponse> | undefined =
+ undefined;
if (query) {
const form: SearchForm = {
};
if (query !== "") {
- searchResponse = client.search(form);
+ searchResponse = await client.search(form);
if (auth) {
const resolveObjectForm: ResolveObject = {
q: query,
auth,
};
- resolveObjectResponse = client
+ resolveObjectResponse = await client
.resolveObject(resolveObjectForm)
.catch(() => undefined);
}
{this.selects}
{this.searchForm}
{this.displayResults(type)}
- {this.resultsCount === 0 && !this.state.searchLoading && (
- <span>{i18n.t("no_results")}</span>
- )}
+ {this.resultsCount === 0 &&
+ this.state.searchRes.state === "success" && (
+ <span>{i18n.t("no_results")}</span>
+ )}
<Paginator page={page} onChange={this.handlePageChange} />
</div>
);
minLength={1}
/>
<button type="submit" className="btn btn-secondary mr-2 mb-2">
- {this.state.searchLoading ? (
+ {this.state.searchRes.state === "loading" ? (
<Spinner />
) : (
<span>{i18n.t("search")}</span>
creatorSearchOptions,
searchCommunitiesLoading,
searchCreatorLoading,
+ communitiesRes,
} = this.state;
+ const hasCommunities =
+ communitiesRes.state == "success" &&
+ communitiesRes.data.communities.length > 0;
+
return (
<div className="mb-2">
<select
/>
</span>
<div className="form-row">
- {this.state.communities.length > 0 && (
+ {hasCommunities && (
<Filter
filterType="community"
onChange={this.handleCommunityFilterChange}
onSearch={this.handleCommunitySearch}
options={communitySearchOptions}
- loading={searchCommunitiesLoading}
value={communityId}
+ loading={searchCommunitiesLoading}
/>
)}
<Filter
onChange={this.handleCreatorFilterChange}
onSearch={this.handleCreatorSearch}
options={creatorSearchOptions}
- loading={searchCreatorLoading}
value={creatorId}
+ loading={searchCreatorLoading}
/>
</div>
</div>
buildCombined(): Combined[] {
const combined: Combined[] = [];
- const { resolveObjectResponse, searchResponse } = this.state;
+ const {
+ resolveObjectRes: resolveObjectResponse,
+ searchRes: searchResponse,
+ } = this.state;
// Push the possible resolve / federated objects first
- if (resolveObjectResponse) {
- const { comment, post, community, person } = resolveObjectResponse;
+ if (resolveObjectResponse.state == "success") {
+ const { comment, post, community, person } = resolveObjectResponse.data;
if (comment) {
combined.push(commentViewToCombined(comment));
}
// Push the search results
- if (searchResponse) {
- const { comments, posts, communities, users } = searchResponse;
+ if (searchResponse.state === "success") {
+ const { comments, posts, communities, users } = searchResponse.data;
combined.push(
...[
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" && (
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" && (
}
get comments() {
- const { searchResponse, resolveObjectResponse, siteRes } = this.state;
- const comments = searchResponse?.comments ?? [];
-
- if (resolveObjectResponse?.comment) {
- comments.unshift(resolveObjectResponse?.comment);
+ 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 (
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" })}
/>
);
}
get posts() {
- const { searchResponse, resolveObjectResponse, siteRes } = this.state;
- const posts = searchResponse?.posts ?? [];
-
- if (resolveObjectResponse?.post) {
- posts.unshift(resolveObjectResponse.post);
+ const {
+ searchRes: searchResponse,
+ resolveObjectRes: resolveObjectResponse,
+ siteRes,
+ } = this.state;
+ const posts =
+ searchResponse.state === "success" ? searchResponse.data.posts : [];
+
+ if (
+ resolveObjectResponse.state === "success" &&
+ resolveObjectResponse.data.post
+ ) {
+ posts.unshift(resolveObjectResponse.data.post);
}
return (
allLanguages={siteRes.all_languages}
siteLanguages={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={() => {}}
/>
</div>
</div>
}
get communities() {
- const { searchResponse, resolveObjectResponse } = this.state;
- const communities = searchResponse?.communities ?? [];
-
- if (resolveObjectResponse?.community) {
- communities.unshift(resolveObjectResponse.community);
+ const {
+ searchRes: searchResponse,
+ resolveObjectRes: resolveObjectResponse,
+ } = this.state;
+ const communities =
+ searchResponse.state === "success" ? searchResponse.data.communities : [];
+
+ if (
+ resolveObjectResponse.state === "success" &&
+ resolveObjectResponse.data.community
+ ) {
+ communities.unshift(resolveObjectResponse.data.community);
}
return (
}
get users() {
- const { searchResponse, resolveObjectResponse } = this.state;
- const users = searchResponse?.users ?? [];
-
- if (resolveObjectResponse?.person) {
- users.unshift(resolveObjectResponse.person);
+ const {
+ searchRes: searchResponse,
+ resolveObjectRes: resolveObjectResponse,
+ } = this.state;
+ const users =
+ searchResponse.state === "success" ? searchResponse.data.users : [];
+
+ if (
+ resolveObjectResponse.state === "success" &&
+ resolveObjectResponse.data.person
+ ) {
+ users.unshift(resolveObjectResponse.data.person);
}
return (
}
get resultsCount(): number {
- const { searchResponse: r, resolveObjectResponse: resolveRes } = this.state;
-
- const searchCount = r
- ? r.posts.length +
- r.comments.length +
- r.communities.length +
- r.users.length
- : 0;
-
- const resObjCount = resolveRes
- ? resolveRes.post ||
- resolveRes.person ||
- resolveRes.community ||
- resolveRes.comment
- ? 1
- : 0
- : 0;
+ 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;
return resObjCount + searchCount;
}
- search() {
- const auth = myAuth(false);
+ async search() {
+ const auth = myAuth();
const { searchText: q } = this.state;
const { communityId, creatorId, type, sort, listingType, page } =
getSearchQueryParams();
- if (q && q !== "") {
- const form: SearchForm = {
- q,
- community_id: communityId ?? undefined,
- creator_id: creatorId ?? undefined,
- type_: type,
- sort,
- listing_type: listingType,
- page,
- limit: fetchLimit,
- auth,
- };
-
- if (auth) {
- const resolveObjectForm: ResolveObject = {
+ 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,
- };
- WebSocketService.Instance.send(
- wsClient.resolveObject(resolveObjectForm)
- );
- }
-
- this.setState({
- searchResponse: undefined,
- resolveObjectResponse: undefined,
- searchLoading: true,
+ }),
});
+ window.scrollTo(0, 0);
+ restoreScrollPosition(this.context);
- WebSocketService.Instance.send(wsClient.search(form));
+ if (auth) {
+ this.setState({ resolveObjectRes: { state: "loading" } });
+ this.setState({
+ resolveObjectRes: await HttpService.client.resolveObject({
+ q,
+ auth,
+ }),
+ });
+ }
}
}
handleCreatorSearch = debounce(async (text: string) => {
const { creatorId } = getSearchQueryParams();
const { creatorSearchOptions } = this.state;
- this.setState({
- searchCreatorLoading: true,
- });
-
const newOptions: Choice[] = [];
+ this.setState({ searchCreatorLoading: true });
+
const selectedChoice = creatorSearchOptions.find(
choice => getIdFromString(choice.value) === creatorId
);
}
if (text.length > 0) {
- newOptions.push(...(await fetchUsers(text)).users.map(personToChoice));
+ newOptions.push(...(await fetchUsers(text)).map(personToChoice));
}
this.setState({
}
if (text.length > 0) {
- newOptions.push(
- ...(await fetchCommunities(text)).communities.map(communityToChoice)
- );
+ newOptions.push(...(await fetchCommunities(text)).map(communityToChoice));
}
this.setState({
i.setState({ searchText: event.target.value });
}
- updateUrl({
+ async updateUrl({
q,
type,
listingType,
this.props.history.push(`/search${getQueryString(queryParams)}`);
- this.search();
- }
-
- parseMessage(msg: any) {
- console.log(msg);
- const op = wsUserOp(msg);
- if (msg.error) {
- if (msg.error === "couldnt_find_object") {
- this.setState({
- resolveObjectResponse: {},
- });
- this.checkFinishedLoading();
- } else {
- toast(i18n.t(msg.error), "danger");
- }
- } else {
- switch (op) {
- case UserOperation.Search: {
- const searchResponse = wsJsonToRes<SearchResponse>(msg);
- this.setState({ searchResponse });
- window.scrollTo(0, 0);
- this.checkFinishedLoading();
- restoreScrollPosition(this.context);
-
- break;
- }
-
- case UserOperation.CreateCommentLike: {
- const { comment_view } = wsJsonToRes<CommentResponse>(msg);
- createCommentLikeRes(
- comment_view,
- this.state.searchResponse?.comments
- );
-
- break;
- }
-
- case UserOperation.CreatePostLike: {
- const { post_view } = wsJsonToRes<PostResponse>(msg);
- createPostLikeFindRes(post_view, this.state.searchResponse?.posts);
-
- break;
- }
-
- case UserOperation.ListCommunities: {
- const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
- this.setState({ communities });
-
- break;
- }
-
- case UserOperation.ResolveObject: {
- const resolveObjectResponse = wsJsonToRes<ResolveObjectResponse>(msg);
- this.setState({ resolveObjectResponse });
- this.checkFinishedLoading();
-
- break;
- }
- }
- }
- }
-
- checkFinishedLoading() {
- if (this.state.searchResponse || this.state.resolveObjectResponse) {
- this.setState({ searchLoading: false });
- }
+ await this.search();
}
}