-Subproject commit d2b85d582071d84b559f7b9db1ab623f6596c586
+Subproject commit 5c50ce3ebaf058ad5d4e9bcd445653960cbc98b1
"autosize": "^6.0.1",
"babel-loader": "^9.1.2",
"babel-plugin-inferno": "^6.6.0",
+ "bootstrap": "^5.2.3",
"check-password-strength": "^2.0.7",
- "choices.js": "^10.2.0",
"classnames": "^2.3.1",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"markdown-it-sup": "^1.0.0",
"mini-css-extract-plugin": "^2.7.2",
"moment": "^2.29.4",
- "node-fetch": "^2.6.1",
"register-service-worker": "^1.7.2",
"run-node-webpack-plugin": "^1.3.0",
"rxjs": "^7.8.0",
"@types/markdown-it": "^12.2.3",
"@types/markdown-it-container": "^2.0.5",
"@types/node": "^18.14.0",
- "@types/node-fetch": "^2.6.2",
"@types/sanitize-html": "^2.8.0",
"@types/serialize-javascript": "^5.0.1",
"@types/toastify-js": "^1.11.1",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
- "bootstrap": "^5.2.3",
"bootswatch": "^5.2.3",
"eslint": "^8.34.0",
"eslint-plugin-inferno": "^7.32.1",
import { App } from "../shared/components/app/app";
import { initializeSite } from "../shared/utils";
+import "bootstrap/js/dist/dropdown";
+
const site = window.isoData.site_res;
initializeSite(site);
</BrowserRouter>
);
-let root = document.getElementById("root");
+const root = document.getElementById("root");
if (root) {
hydrate(wrapper, root);
}
const context = {} as any;
let auth: string | undefined = IsomorphicCookie.load("jwt", req);
- let getSiteForm: GetSite = { auth };
+ const getSiteForm: GetSite = { auth };
- let promises: Promise<any>[] = [];
+ const promises: Promise<any>[] = [];
- let headers = setForwardedHeaders(req.headers);
-
- let initialFetchReq: InitialFetchRequest = {
- client: new LemmyHttp(httpBaseInternal, headers),
- auth,
- path: req.path,
- };
+ const headers = setForwardedHeaders(req.headers);
+ const client = new LemmyHttp(httpBaseInternal, headers);
// Get site data first
// This bypasses errors, so that the client can hit the error on its own,
// in order to remove the jwt on the browser. Necessary for wrong jwts
- let try_site: any = await initialFetchReq.client.getSite(getSiteForm);
+ let try_site: any = await client.getSite(getSiteForm);
if (try_site.error == "not_logged_in") {
console.error(
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
);
getSiteForm.auth = undefined;
- initialFetchReq.auth = undefined;
- try_site = await initialFetchReq.client.getSite(getSiteForm);
+ auth = undefined;
+ try_site = await client.getSite(getSiteForm);
}
- let site: GetSiteResponse = try_site;
+ const site: GetSiteResponse = try_site;
initializeSite(site);
+ const initialFetchReq: InitialFetchRequest = {
+ client,
+ auth,
+ path: req.path,
+ query: req.query,
+ site,
+ };
+
if (activeRoute?.fetchInitialData) {
promises.push(...activeRoute.fetchInitialData(initialFetchReq));
}
- let routeData = await Promise.all(promises);
+ const routeData = await Promise.all(promises);
// Redirect to the 404 if there's an API error
if (routeData[0] && routeData[0].error) {
- let errCode = routeData[0].error;
- console.error(errCode);
- if (errCode == "instance_is_private") {
+ const error = routeData[0].error;
+ console.error(error);
+ if (error === "instance_is_private") {
return res.redirect(`/signup`);
} else {
- return res.send(`404: ${removeAuthParam(errCode)}`);
+ return res.send(`404: ${removeAuthParam(error)}`);
}
}
- let isoData: IsoData = {
+ const isoData: IsoData = {
path: req.path,
site_res: site,
routeData,
<script>eruda.init();</script>
</>
);
+
const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : "";
const root = renderToString(wrapper);
const helmet = Helmet.renderStatic();
<Navbar siteRes={siteRes} />
<div className="mt-4 p-0 fl-1">
<Switch>
- {routes.map(
- ({ path, exact, component: Component, ...rest }) => (
- <Route
- key={path}
- path={path}
- exact={exact}
- render={props => <Component {...props} {...rest} />}
- />
- )
- )}
- <Route render={props => <NoMatch {...props} />} />
+ {routes.map(({ path, component }) => (
+ <Route key={path} path={path} exact component={component} />
+ ))}
+ <Route component={NoMatch} />
</Switch>
</div>
<Footer site={siteRes} />
unreadInboxCount: number;
unreadReportCount: number;
unreadApplicationCount: number;
- searchParam: string;
showDropdown: boolean;
onSiteBanner?(url: string): any;
}
unreadReportCount: 0,
unreadApplicationCount: 0,
expanded: false,
- searchParam: "",
showDropdown: false,
};
subscription: any;
this.unreadApplicationCountSub.unsubscribe();
}
- updateUrl() {
- const searchParam = this.state.searchParam;
- this.setState({ searchParam: "" });
- this.setState({ showDropdown: false, expanded: false });
- if (searchParam === "") {
- this.context.router.history.push(`/search/`);
- } else {
- const searchParamEncoded = encodeURIComponent(searchParam);
- this.context.router.history.push(
- `/search/q/${searchParamEncoded}/type/All/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
- );
- }
- }
-
render() {
return this.navbar();
}
i.setState({ expanded: false, showDropdown: false });
}
- handleSearchParam(i: Navbar, event: any) {
- i.setState({ searchParam: event.target.value });
- }
-
handleLogoutClick(i: Navbar) {
i.setState({ showDropdown: false, expanded: false });
UserService.Instance.logout();
// Custom css
@import "../../../../node_modules/tributejs/dist/tribute.css";
@import "../../../../node_modules/toastify-js/src/toastify.css";
-@import "../../../../node_modules/choices.js/src/styles/choices.scss";
@import "../../../../node_modules/tippy.js/dist/tippy.css";
+@import "../../../../node_modules/bootstrap/dist/css/bootstrap-utilities.min.css";
@import "../../../assets/css/main.css";
<button className="btn btn-link btn-animate">
<Link
className="text-muted"
- to={`/create_private_message/recipient/${cv.creator.id}`}
+ to={`/create_private_message/${cv.creator.id}`}
title={i18n.t("message").toLowerCase()}
>
<Icon icon="mail" />
}
export class CommentNodes extends Component<CommentNodesProps, any> {
- constructor(props: any, context: any) {
+ constructor(props: CommentNodesProps, context: any) {
super(props, context);
}
--- /dev/null
+import classNames from "classnames";
+import {
+ ChangeEvent,
+ Component,
+ createRef,
+ linkEvent,
+ RefObject,
+} from "inferno";
+import { i18n } from "../../i18next";
+import { Choice } from "../../utils";
+import { Icon, Spinner } from "./icon";
+
+interface SearchableSelectProps {
+ id: string;
+ value?: number | string;
+ options: Choice[];
+ onChange?: (option: Choice) => void;
+ onSearch?: (text: string) => void;
+ loading?: boolean;
+}
+
+interface SearchableSelectState {
+ selectedIndex: number;
+ searchText: string;
+ loadingEllipses: string;
+}
+
+function handleSearch(i: SearchableSelect, e: ChangeEvent<HTMLInputElement>) {
+ const { onSearch } = i.props;
+ const searchText = e.target.value;
+
+ if (onSearch) {
+ onSearch(searchText);
+ }
+
+ i.setState({
+ searchText,
+ });
+}
+
+export class SearchableSelect extends Component<
+ SearchableSelectProps,
+ SearchableSelectState
+> {
+ private searchInputRef: RefObject<HTMLInputElement> = createRef();
+ private toggleButtonRef: RefObject<HTMLButtonElement> = createRef();
+ private loadingEllipsesInterval?: NodeJS.Timer = undefined;
+
+ state: SearchableSelectState = {
+ selectedIndex: 0,
+ searchText: "",
+ loadingEllipses: "...",
+ };
+
+ constructor(props: SearchableSelectProps, context: any) {
+ super(props, context);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.focusSearch = this.focusSearch.bind(this);
+
+ if (props.value) {
+ let selectedIndex = props.options.findIndex(
+ ({ value }) => value === props.value?.toString()
+ );
+
+ if (selectedIndex < 0) {
+ selectedIndex = 0;
+ }
+
+ this.state = {
+ ...this.state,
+ selectedIndex,
+ };
+ }
+ }
+
+ render() {
+ const { id, options, onSearch, loading } = this.props;
+ const { searchText, selectedIndex, loadingEllipses } = this.state;
+
+ return (
+ <div className="dropdown">
+ <button
+ id={id}
+ type="button"
+ className="custom-select text-start"
+ aria-haspopup="listbox"
+ data-bs-toggle="dropdown"
+ onClick={this.focusSearch}
+ >
+ {loading
+ ? `${i18n.t("loading")}${loadingEllipses}`
+ : options[selectedIndex].label}
+ </button>
+ <div
+ role="combobox"
+ aria-activedescendant={options[selectedIndex].label}
+ className="modlog-choices-font-size dropdown-menu w-100 p-2"
+ >
+ <div className="input-group">
+ <span className="input-group-text">
+ {loading ? <Spinner /> : <Icon icon="search" />}
+ </span>
+ <input
+ type="text"
+ className="form-control"
+ ref={this.searchInputRef}
+ onInput={linkEvent(this, handleSearch)}
+ value={searchText}
+ placeholder={`${i18n.t("search")}...`}
+ />
+ </div>
+ {!loading &&
+ // If onSearch is provided, it is assumed that the parent component is doing it's own sorting logic.
+ (onSearch || searchText.length === 0
+ ? options
+ : options.filter(({ label }) =>
+ label.toLowerCase().includes(searchText.toLowerCase())
+ )
+ ).map((option, index) => (
+ <button
+ key={option.value}
+ className={classNames("dropdown-item", {
+ active: selectedIndex === index,
+ })}
+ role="option"
+ aria-disabled={option.disabled}
+ disabled={option.disabled}
+ aria-selected={selectedIndex === index}
+ onClick={() => this.handleChange(option)}
+ type="button"
+ >
+ {option.label}
+ </button>
+ ))}
+ </div>
+ </div>
+ );
+ }
+
+ focusSearch() {
+ if (this.toggleButtonRef.current?.ariaExpanded !== "true") {
+ this.searchInputRef.current?.focus();
+
+ if (this.props.onSearch) {
+ this.props.onSearch("");
+ }
+
+ this.setState({
+ searchText: "",
+ });
+ }
+ }
+
+ static getDerivedStateFromProps({
+ value,
+ options,
+ }: SearchableSelectProps): Partial<SearchableSelectState> {
+ let selectedIndex =
+ value || value === 0
+ ? options.findIndex(option => option.value === value.toString())
+ : 0;
+
+ if (selectedIndex < 0) {
+ selectedIndex = 0;
+ }
+
+ return {
+ selectedIndex,
+ };
+ }
+
+ componentDidUpdate() {
+ const { loading } = this.props;
+ if (loading && !this.loadingEllipsesInterval) {
+ this.loadingEllipsesInterval = setInterval(() => {
+ this.setState(({ loadingEllipses }) => ({
+ loadingEllipses:
+ loadingEllipses.length === 3 ? "" : loadingEllipses + ".",
+ }));
+ }, 750);
+ } else if (!loading && this.loadingEllipsesInterval) {
+ clearInterval(this.loadingEllipsesInterval);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.loadingEllipsesInterval) {
+ clearInterval(this.loadingEllipsesInterval);
+ }
+ }
+
+ handleChange(option: Choice) {
+ const { onChange, value } = this.props;
+
+ if (option.value !== value?.toString()) {
+ if (onChange) {
+ onChange(option);
+ }
+
+ this.setState({ searchText: "" });
+ }
+ }
+}
import { i18n } from "../../i18next";
import { WebSocketService } from "../../services";
import {
- getListingTypeFromPropsNoDefault,
- getPageFromProps,
+ getPageFromString,
+ getQueryParams,
+ getQueryString,
isBrowser,
myAuth,
numToSI,
+ QueryParams,
+ routeListingTypeToEnum,
setIsoData,
showLocal,
toast,
interface CommunitiesState {
listCommunitiesResponse?: ListCommunitiesResponse;
- page: number;
loading: boolean;
siteRes: GetSiteResponse;
searchText: string;
- listingType: ListingType;
}
interface CommunitiesProps {
- listingType?: ListingType;
- page?: number;
+ listingType: ListingType;
+ page: number;
+}
+
+function getCommunitiesQueryParams() {
+ return getQueryParams<CommunitiesProps>({
+ listingType: getListingTypeFromQuery,
+ page: getPageFromString,
+ });
+}
+
+function getListingTypeFromQuery(listingType?: string): ListingType {
+ return routeListingTypeToEnum(listingType ?? "", ListingType.Local);
+}
+
+function toggleSubscribe(community_id: number, follow: boolean) {
+ const auth = myAuth();
+ if (auth) {
+ const form: FollowCommunity = {
+ community_id,
+ follow,
+ auth,
+ };
+
+ WebSocketService.Instance.send(wsClient.followCommunity(form));
+ }
+}
+
+function refetch() {
+ const { listingType, page } = getCommunitiesQueryParams();
+
+ const listCommunitiesForm: ListCommunities = {
+ type_: listingType,
+ sort: SortType.TopMonth,
+ limit: communityLimit,
+ page,
+ auth: myAuth(false),
+ };
+
+ WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
}
export class Communities extends Component<any, CommunitiesState> {
private isoData = setIsoData(this.context);
state: CommunitiesState = {
loading: true,
- page: getPageFromProps(this.props),
- listingType: getListingTypeFromPropsNoDefault(this.props),
siteRes: this.isoData.site_res,
searchText: "",
};
this.subscription = wsSubscribe(this.parseMessage);
// Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
- let listRes = this.isoData.routeData[0] as ListCommunitiesResponse;
+ if (this.isoData.path === this.context.router.route.match.url) {
+ const listRes = this.isoData.routeData[0] as ListCommunitiesResponse;
this.state = {
...this.state,
listCommunitiesResponse: listRes,
loading: false,
};
} else {
- this.refetch();
+ refetch();
}
}
}
}
- static getDerivedStateFromProps(props: any): CommunitiesProps {
- return {
- listingType: getListingTypeFromPropsNoDefault(props),
- page: getPageFromProps(props),
- };
- }
-
- componentDidUpdate(_: any, lastState: CommunitiesState) {
- if (
- lastState.page !== this.state.page ||
- lastState.listingType !== this.state.listingType
- ) {
- this.setState({ loading: true });
- this.refetch();
- }
- }
-
get documentTitle(): string {
return `${i18n.t("communities")} - ${
this.state.siteRes.site_view.site.name
}
render() {
+ const { listingType, page } = getCommunitiesQueryParams();
+
return (
<div className="container-lg">
<HtmlTags
<h4>{i18n.t("list_of_communities")}</h4>
<span className="mb-2">
<ListingTypeSelect
- type_={this.state.listingType}
+ type_={listingType}
showLocal={showLocal(this.isoData)}
showSubscribed
onChange={this.handleListingTypeChange}
{i18n.t("unsubscribe")}
</button>
)}
- {cv.subscribed == SubscribedType.NotSubscribed && (
+ {cv.subscribed === SubscribedType.NotSubscribed && (
<button
className="btn btn-link d-inline-block"
onClick={linkEvent(
{i18n.t("subscribe")}
</button>
)}
- {cv.subscribed == SubscribedType.Pending && (
+ {cv.subscribed === SubscribedType.Pending && (
<div className="text-warning d-inline-block">
{i18n.t("subscribe_pending")}
</div>
</tbody>
</table>
</div>
- <Paginator
- page={this.state.page}
- onChange={this.handlePageChange}
- />
+ <Paginator page={page} onChange={this.handlePageChange} />
</div>
)}
</div>
);
}
- updateUrl(paramUpdates: CommunitiesProps) {
- const page = paramUpdates.page || this.state.page;
- const listingTypeStr = paramUpdates.listingType || this.state.listingType;
- this.props.history.push(
- `/communities/listing_type/${listingTypeStr}/page/${page}`
- );
+ updateUrl({ listingType, page }: Partial<CommunitiesProps>) {
+ const { listingType: urlListingType, page: urlPage } =
+ getCommunitiesQueryParams();
+
+ const queryParams: QueryParams<CommunitiesProps> = {
+ listingType: listingType ?? urlListingType,
+ page: (page ?? urlPage)?.toString(),
+ };
+
+ this.props.history.push(`/communities${getQueryString(queryParams)}`);
+
+ refetch();
}
handlePageChange(page: number) {
}
handleUnsubscribe(communityId: number) {
- let auth = myAuth();
- if (auth) {
- let form: FollowCommunity = {
- community_id: communityId,
- follow: false,
- auth,
- };
- WebSocketService.Instance.send(wsClient.followCommunity(form));
- }
+ toggleSubscribe(communityId, false);
}
handleSubscribe(communityId: number) {
- let auth = myAuth();
- if (auth) {
- let form: FollowCommunity = {
- community_id: communityId,
- follow: true,
- auth,
- };
- WebSocketService.Instance.send(wsClient.followCommunity(form));
- }
+ toggleSubscribe(communityId, true);
}
handleSearchChange(i: Communities, event: any) {
handleSearchSubmit(i: Communities) {
const searchParamEncoded = encodeURIComponent(i.state.searchText);
- i.context.router.history.push(
- `/search/q/${searchParamEncoded}/type/Communities/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
- );
- }
-
- refetch() {
- let listCommunitiesForm: ListCommunities = {
- type_: this.state.listingType,
- sort: SortType.TopMonth,
- limit: communityLimit,
- page: this.state.page,
- auth: myAuth(false),
- };
-
- WebSocketService.Instance.send(
- wsClient.listCommunities(listCommunitiesForm)
- );
+ i.context.router.history.push(`/search?q=${searchParamEncoded}`);
}
- static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
- let pathSplit = req.path.split("/");
- let type_: ListingType = pathSplit[3]
- ? ListingType[pathSplit[3]]
- : ListingType.Local;
- let page = pathSplit[5] ? Number(pathSplit[5]) : 1;
- let listCommunitiesForm: ListCommunities = {
- type_,
+ static fetchInitialData({
+ query: { listingType, page },
+ client,
+ auth,
+ }: InitialFetchRequest<QueryParams<CommunitiesProps>>): Promise<any>[] {
+ const listCommunitiesForm: ListCommunities = {
+ type_: getListingTypeFromQuery(listingType),
sort: SortType.TopMonth,
limit: communityLimit,
- page,
- auth: req.auth,
+ page: getPageFromString(page),
+ auth: auth,
};
- return [req.client.listCommunities(listCommunitiesForm)];
+ return [client.listCommunities(listCommunitiesForm)];
}
parseMessage(msg: any) {
- let op = wsUserOp(msg);
+ const op = wsUserOp(msg);
console.log(msg);
if (msg.error) {
toast(i18n.t(msg.error), "danger");
- return;
- } else if (op == UserOperation.ListCommunities) {
- let data = wsJsonToRes<ListCommunitiesResponse>(msg);
+ } else if (op === UserOperation.ListCommunities) {
+ const data = wsJsonToRes<ListCommunitiesResponse>(msg);
this.setState({ listCommunitiesResponse: data, loading: false });
window.scrollTo(0, 0);
- } else if (op == UserOperation.FollowCommunity) {
- let data = wsJsonToRes<CommunityResponse>(msg);
- let res = this.state.listCommunitiesResponse;
- let found = res?.communities.find(
- c => c.community.id == data.community_view.community.id
+ } else if (op === UserOperation.FollowCommunity) {
+ const {
+ community_view: {
+ community,
+ subscribed,
+ counts: { subscribers },
+ },
+ } = wsJsonToRes<CommunityResponse>(msg);
+ const res = this.state.listCommunitiesResponse;
+ const found = res?.communities.find(
+ ({ community: { id } }) => id == community.id
);
+
if (found) {
- found.subscribed = data.community_view.subscribed;
- found.counts.subscribers = data.community_view.counts.subscribers;
+ found.subscribed = subscribed;
+ found.counts.subscribers = subscribers;
this.setState(this.state);
}
}
import { Component, linkEvent } from "inferno";
+import { RouteComponentProps } from "inferno-router/dist/Route";
import {
AddModToCommunityResponse,
BanFromCommunityResponse,
BlockCommunityResponse,
BlockPersonResponse,
- CommentReportResponse,
CommentResponse,
CommentView,
CommunityResponse,
GetCommunityResponse,
GetPosts,
GetPostsResponse,
- GetSiteResponse,
ListingType,
PostReportResponse,
PostResponse,
enableDownvotes,
enableNsfw,
fetchLimit,
- getDataTypeFromProps,
- getPageFromProps,
- getSortTypeFromProps,
+ getDataTypeString,
+ getPageFromString,
+ getQueryParams,
+ getQueryString,
isPostBlocked,
myAuth,
notifyPost,
nsfwCheck,
postToCommentSortType,
+ QueryParams,
relTags,
restoreScrollPosition,
+ routeDataTypeToEnum,
+ routeSortTypeToEnum,
saveCommentRes,
saveScrollPosition,
setIsoData,
interface State {
communityRes?: GetCommunityResponse;
- siteRes: GetSiteResponse;
- communityName: string;
communityLoading: boolean;
- postsLoading: boolean;
- commentsLoading: boolean;
+ listingsLoading: boolean;
posts: PostView[];
comments: CommentView[];
- dataType: DataType;
- sort: SortType;
- page: number;
showSidebarMobile: boolean;
}
page: number;
}
-interface UrlParams {
- dataType?: string;
- sort?: SortType;
- page?: number;
+function getCommunityQueryParams() {
+ return getQueryParams<CommunityProps>({
+ dataType: getDataTypeFromQuery,
+ page: getPageFromString,
+ sort: getSortTypeFromQuery,
+ });
+}
+
+const getDataTypeFromQuery = (type?: string): DataType =>
+ routeDataTypeToEnum(type ?? "", DataType.Post);
+
+function getSortTypeFromQuery(type?: string): SortType {
+ const mySortType =
+ UserService.Instance.myUserInfo?.local_user_view.local_user
+ .default_sort_type;
+
+ return routeSortTypeToEnum(
+ type ?? "",
+ mySortType ? Object.values(SortType)[mySortType] : SortType.Active
+ );
}
-export class Community extends Component<any, State> {
+export class Community extends Component<
+ RouteComponentProps<{ name: string }>,
+ State
+> {
private isoData = setIsoData(this.context);
private subscription?: Subscription;
state: State = {
- communityName: this.props.match.params.name,
communityLoading: true,
- postsLoading: true,
- commentsLoading: true,
+ listingsLoading: true,
posts: [],
comments: [],
- dataType: getDataTypeFromProps(this.props),
- sort: getSortTypeFromProps(this.props),
- page: getPageFromProps(this.props),
- siteRes: this.isoData.site_res,
showSidebarMobile: false,
};
- constructor(props: any, context: any) {
+ constructor(props: RouteComponentProps<{ name: string }>, context: any) {
super(props, context);
this.handleSortChange = this.handleSortChange.bind(this);
...this.state,
communityRes: this.isoData.routeData[0] as GetCommunityResponse,
};
- let postsRes = this.isoData.routeData[1] as GetPostsResponse | undefined;
- let commentsRes = this.isoData.routeData[2] as
+ const postsRes = this.isoData.routeData[1] as
+ | GetPostsResponse
+ | undefined;
+ const commentsRes = this.isoData.routeData[2] as
| GetCommentsResponse
| undefined;
this.state = {
...this.state,
communityLoading: false,
- postsLoading: false,
- commentsLoading: false,
+ listingsLoading: false,
};
} else {
this.fetchCommunity();
}
fetchCommunity() {
- let form: GetCommunity = {
- name: this.state.communityName,
+ const form: GetCommunity = {
+ name: this.props.match.params.name,
auth: myAuth(false),
};
WebSocketService.Instance.send(wsClient.getCommunity(form));
this.subscription?.unsubscribe();
}
- static getDerivedStateFromProps(props: any): CommunityProps {
- return {
- dataType: getDataTypeFromProps(props),
- sort: getSortTypeFromProps(props),
- page: getPageFromProps(props),
- };
- }
-
- static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
- let pathSplit = req.path.split("/");
- let promises: Promise<any>[] = [];
-
- let communityName = pathSplit[2];
- let communityForm: GetCommunity = {
+ static fetchInitialData({
+ client,
+ path,
+ query: { dataType: urlDataType, page: urlPage, sort: urlSort },
+ auth,
+ }: InitialFetchRequest<QueryParams<CommunityProps>>): Promise<any>[] {
+ const pathSplit = path.split("/");
+ const promises: Promise<any>[] = [];
+
+ const communityName = pathSplit[2];
+ const communityForm: GetCommunity = {
name: communityName,
- auth: req.auth,
+ auth,
};
- promises.push(req.client.getCommunity(communityForm));
+ promises.push(client.getCommunity(communityForm));
- let dataType: DataType = pathSplit[4]
- ? DataType[pathSplit[4]]
- : DataType.Post;
+ const dataType = getDataTypeFromQuery(urlDataType);
- let mui = UserService.Instance.myUserInfo;
+ const sort = getSortTypeFromQuery(urlSort);
- let sort: SortType = pathSplit[6]
- ? SortType[pathSplit[6]]
- : mui
- ? Object.values(SortType)[
- mui.local_user_view.local_user.default_sort_type
- ]
- : SortType.Active;
+ const page = getPageFromString(urlPage);
- let page = pathSplit[8] ? Number(pathSplit[8]) : 1;
-
- if (dataType == DataType.Post) {
- let getPostsForm: GetPosts = {
+ if (dataType === DataType.Post) {
+ const getPostsForm: GetPosts = {
community_name: communityName,
page,
limit: fetchLimit,
sort,
type_: ListingType.All,
saved_only: false,
- auth: req.auth,
+ auth,
};
- promises.push(req.client.getPosts(getPostsForm));
+ promises.push(client.getPosts(getPostsForm));
promises.push(Promise.resolve());
} else {
- let getCommentsForm: GetComments = {
+ const getCommentsForm: GetComments = {
community_name: communityName,
page,
limit: fetchLimit,
sort: postToCommentSortType(sort),
type_: ListingType.All,
saved_only: false,
- auth: req.auth,
+ auth,
};
promises.push(Promise.resolve());
- promises.push(req.client.getComments(getCommentsForm));
+ promises.push(client.getComments(getCommentsForm));
}
return promises;
}
- componentDidUpdate(_: any, lastState: State) {
- if (
- lastState.dataType !== this.state.dataType ||
- lastState.sort !== this.state.sort ||
- lastState.page !== this.state.page
- ) {
- this.setState({ postsLoading: true, commentsLoading: true });
- this.fetchData();
- }
- }
-
get documentTitle(): string {
- let cRes = this.state.communityRes;
+ const cRes = this.state.communityRes;
return cRes
- ? `${cRes.community_view.community.title} - ${this.state.siteRes.site_view.site.name}`
+ ? `${cRes.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}`
: "";
}
render() {
- // For some reason, this returns an empty vec if it matches the site langs
- let res = this.state.communityRes;
- let communityLangs =
- res?.discussion_languages.length == 0
- ? this.state.siteRes.all_languages.map(l => l.id)
- : res?.discussion_languages;
+ const res = this.state.communityRes;
+ const { page } = getCommunityQueryParams();
return (
<div className="container-lg">
<div className="row">
<div className="col-12 col-md-8">
- {this.communityInfo()}
+ {this.communityInfo}
<div className="d-block d-md-none">
<button
className="btn btn-secondary d-inline-block mb-2 mr-3"
classes="icon-inline"
/>
</button>
- {this.state.showSidebarMobile && (
- <>
- <Sidebar
- community_view={res.community_view}
- moderators={res.moderators}
- admins={this.state.siteRes.admins}
- online={res.online}
- enableNsfw={enableNsfw(this.state.siteRes)}
- editable
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={
- this.state.siteRes.discussion_languages
- }
- communityLanguages={communityLangs}
- />
- {!res.community_view.community.local && res.site && (
- <SiteSidebar
- site={res.site}
- showLocal={showLocal(this.isoData)}
- />
- )}
- </>
- )}
+ {this.state.showSidebarMobile && this.sidebar(res)}
</div>
- {this.selects()}
- {this.listings()}
- <Paginator
- page={this.state.page}
- onChange={this.handlePageChange}
- />
+ {this.selects}
+ {this.listings}
+ <Paginator page={page} onChange={this.handlePageChange} />
</div>
<div className="d-none d-md-block col-md-4">
- <Sidebar
- community_view={res.community_view}
- moderators={res.moderators}
- admins={this.state.siteRes.admins}
- online={res.online}
- enableNsfw={enableNsfw(this.state.siteRes)}
- editable
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
- communityLanguages={communityLangs}
- />
- {!res.community_view.community.local && res.site && (
- <SiteSidebar
- site={res.site}
- showLocal={showLocal(this.isoData)}
- />
- )}
+ {this.sidebar(res)}
</div>
</div>
</>
);
}
- listings() {
- return this.state.dataType == DataType.Post ? (
- this.state.postsLoading ? (
+ sidebar({
+ community_view,
+ moderators,
+ online,
+ discussion_languages,
+ site,
+ }: GetCommunityResponse) {
+ const { site_res } = this.isoData;
+ // For some reason, this returns an empty vec if it matches the site langs
+ const communityLangs =
+ discussion_languages.length === 0
+ ? site_res.all_languages.map(({ id }) => id)
+ : discussion_languages;
+
+ return (
+ <>
+ <Sidebar
+ community_view={community_view}
+ moderators={moderators}
+ admins={site_res.admins}
+ online={online}
+ enableNsfw={enableNsfw(site_res)}
+ editable
+ allLanguages={site_res.all_languages}
+ siteLanguages={site_res.discussion_languages}
+ communityLanguages={communityLangs}
+ />
+ {!community_view.community.local && site && (
+ <SiteSidebar site={site} showLocal={showLocal(this.isoData)} />
+ )}
+ </>
+ );
+ }
+
+ get listings() {
+ const { dataType } = getCommunityQueryParams();
+ const { site_res } = this.isoData;
+ const { listingsLoading, posts, comments, communityRes } = this.state;
+
+ if (listingsLoading) {
+ return (
<h5>
<Spinner large />
</h5>
- ) : (
+ );
+ } else if (dataType === DataType.Post) {
+ return (
<PostListings
- posts={this.state.posts}
+ posts={posts}
removeDuplicates
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- enableNsfw={enableNsfw(this.state.siteRes)}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
+ enableDownvotes={enableDownvotes(site_res)}
+ enableNsfw={enableNsfw(site_res)}
+ allLanguages={site_res.all_languages}
+ siteLanguages={site_res.discussion_languages}
/>
- )
- ) : this.state.commentsLoading ? (
- <h5>
- <Spinner large />
- </h5>
- ) : (
- <CommentNodes
- nodes={commentsToFlatNodes(this.state.comments)}
- viewType={CommentViewType.Flat}
- noIndent
- showContext
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- moderators={this.state.communityRes?.moderators}
- admins={this.state.siteRes.admins}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
- />
- );
+ );
+ } else {
+ return (
+ <CommentNodes
+ nodes={commentsToFlatNodes(comments)}
+ viewType={CommentViewType.Flat}
+ noIndent
+ showContext
+ enableDownvotes={enableDownvotes(site_res)}
+ moderators={communityRes?.moderators}
+ admins={site_res.admins}
+ allLanguages={site_res.all_languages}
+ siteLanguages={site_res.discussion_languages}
+ />
+ );
+ }
}
- communityInfo() {
- let community = this.state.communityRes?.community_view.community;
+ get communityInfo() {
+ const community = this.state.communityRes?.community_view.community;
+
return (
community && (
<div className="mb-2">
);
}
- selects() {
+ get selects() {
// let communityRss = this.state.communityRes.map(r =>
// communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
// );
- let res = this.state.communityRes;
- let communityRss = res
- ? communityRSSUrl(res.community_view.community.actor_id, this.state.sort)
+ const { dataType, sort } = getCommunityQueryParams();
+ const res = this.state.communityRes;
+ const communityRss = res
+ ? communityRSSUrl(res.community_view.community.actor_id, sort)
: undefined;
return (
<div className="mb-3">
<span className="mr-3">
<DataTypeSelect
- type_={this.state.dataType}
+ type_={dataType}
onChange={this.handleDataTypeChange}
/>
</span>
<span className="mr-2">
- <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
+ <SortSelect sort={sort} onChange={this.handleSortChange} />
</span>
{communityRss && (
<>
window.scrollTo(0, 0);
}
- handleSortChange(val: SortType) {
- this.updateUrl({ sort: val, page: 1 });
+ handleSortChange(sort: SortType) {
+ this.updateUrl({ sort, page: 1 });
window.scrollTo(0, 0);
}
- handleDataTypeChange(val: DataType) {
- this.updateUrl({ dataType: DataType[val], page: 1 });
+ handleDataTypeChange(dataType: DataType) {
+ this.updateUrl({ dataType, page: 1 });
window.scrollTo(0, 0);
}
handleShowSidebarMobile(i: Community) {
- i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
+ i.setState(({ showSidebarMobile }) => ({
+ showSidebarMobile: !showSidebarMobile,
+ }));
}
- updateUrl(paramUpdates: UrlParams) {
- const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
- const sortStr = paramUpdates.sort || this.state.sort;
- const page = paramUpdates.page || this.state.page;
-
- let typeView = `/c/${this.state.communityName}`;
+ updateUrl({ dataType, page, sort }: Partial<CommunityProps>) {
+ const {
+ dataType: urlDataType,
+ page: urlPage,
+ sort: urlSort,
+ } = getCommunityQueryParams();
+
+ const queryParams: QueryParams<CommunityProps> = {
+ dataType: getDataTypeString(dataType ?? urlDataType),
+ page: (page ?? urlPage).toString(),
+ sort: sort ?? urlSort,
+ };
this.props.history.push(
- `${typeView}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
+ `/c/${this.props.match.params.name}${getQueryString(queryParams)}`
);
+
+ this.setState({
+ comments: [],
+ posts: [],
+ listingsLoading: true,
+ });
+
+ this.fetchData();
}
fetchData() {
- if (this.state.dataType == DataType.Post) {
- let form: GetPosts = {
- page: this.state.page,
+ const { dataType, page, sort } = getCommunityQueryParams();
+ const { name } = this.props.match.params;
+
+ let req: string;
+ if (dataType === DataType.Post) {
+ const form: GetPosts = {
+ page,
limit: fetchLimit,
- sort: this.state.sort,
+ sort,
type_: ListingType.All,
- community_name: this.state.communityName,
+ community_name: name,
saved_only: false,
auth: myAuth(false),
};
- WebSocketService.Instance.send(wsClient.getPosts(form));
+ req = wsClient.getPosts(form);
} else {
- let form: GetComments = {
- page: this.state.page,
+ const form: GetComments = {
+ page,
limit: fetchLimit,
- sort: postToCommentSortType(this.state.sort),
+ sort: postToCommentSortType(sort),
type_: ListingType.All,
- community_name: this.state.communityName,
+ community_name: name,
saved_only: false,
auth: myAuth(false),
};
- WebSocketService.Instance.send(wsClient.getComments(form));
+
+ req = wsClient.getComments(form);
}
+
+ WebSocketService.Instance.send(req);
}
parseMessage(msg: any) {
- let op = wsUserOp(msg);
+ const { page } = getCommunityQueryParams();
+ const op = wsUserOp(msg);
console.log(msg);
- let res = this.state.communityRes;
+ const res = this.state.communityRes;
+
if (msg.error) {
toast(i18n.t(msg.error), "danger");
this.context.router.history.push("/");
- return;
} else if (msg.reconnect) {
if (res) {
WebSocketService.Instance.send(
})
);
}
+
this.fetchData();
- } else if (op == UserOperation.GetCommunity) {
- let data = wsJsonToRes<GetCommunityResponse>(msg);
- this.setState({ communityRes: data, communityLoading: false });
- // TODO why is there no auth in this form?
- WebSocketService.Instance.send(
- wsClient.communityJoin({
- community_id: data.community_view.community.id,
- })
- );
- } else if (
- op == UserOperation.EditCommunity ||
- op == UserOperation.DeleteCommunity ||
- op == UserOperation.RemoveCommunity
- ) {
- let data = wsJsonToRes<CommunityResponse>(msg);
- if (res) {
- res.community_view = data.community_view;
- res.discussion_languages = data.discussion_languages;
- }
- this.setState(this.state);
- } else if (op == UserOperation.FollowCommunity) {
- let data = wsJsonToRes<CommunityResponse>(msg);
- if (res) {
- res.community_view.subscribed = data.community_view.subscribed;
- res.community_view.counts.subscribers =
- data.community_view.counts.subscribers;
- }
- this.setState(this.state);
- } else if (op == UserOperation.GetPosts) {
- let data = wsJsonToRes<GetPostsResponse>(msg);
- this.setState({ posts: data.posts, postsLoading: false });
- restoreScrollPosition(this.context);
- setupTippy();
- } else if (
- op == UserOperation.EditPost ||
- op == UserOperation.DeletePost ||
- op == UserOperation.RemovePost ||
- op == UserOperation.LockPost ||
- op == UserOperation.FeaturePost ||
- op == UserOperation.SavePost
- ) {
- let data = wsJsonToRes<PostResponse>(msg);
- editPostFindRes(data.post_view, this.state.posts);
- this.setState(this.state);
- } else if (op == UserOperation.CreatePost) {
- let data = wsJsonToRes<PostResponse>(msg);
-
- let showPostNotifs =
- UserService.Instance.myUserInfo?.local_user_view.local_user
- .show_new_post_notifs;
-
- // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
- //
- if (
- this.state.page == 1 &&
- nsfwCheck(data.post_view) &&
- !isPostBlocked(data.post_view)
- ) {
- this.state.posts.unshift(data.post_view);
- if (showPostNotifs) {
- notifyPost(data.post_view, this.context.router);
+ } else {
+ switch (op) {
+ case UserOperation.GetCommunity: {
+ const data = wsJsonToRes<GetCommunityResponse>(msg);
+
+ this.setState({ communityRes: data, communityLoading: false });
+ // TODO why is there no auth in this form?
+ WebSocketService.Instance.send(
+ wsClient.communityJoin({
+ community_id: data.community_view.community.id,
+ })
+ );
+
+ break;
+ }
+
+ case UserOperation.EditCommunity:
+ case UserOperation.DeleteCommunity:
+ case UserOperation.RemoveCommunity: {
+ const { community_view, discussion_languages } =
+ wsJsonToRes<CommunityResponse>(msg);
+
+ if (res) {
+ res.community_view = community_view;
+ res.discussion_languages = discussion_languages;
+ this.setState(this.state);
+ }
+
+ break;
+ }
+
+ case UserOperation.FollowCommunity: {
+ const {
+ community_view: {
+ subscribed,
+ counts: { subscribers },
+ },
+ } = wsJsonToRes<CommunityResponse>(msg);
+
+ if (res) {
+ res.community_view.subscribed = subscribed;
+ res.community_view.counts.subscribers = subscribers;
+ this.setState(this.state);
+ }
+
+ break;
+ }
+
+ case UserOperation.GetPosts: {
+ const { posts } = wsJsonToRes<GetPostsResponse>(msg);
+
+ this.setState({ posts, listingsLoading: false });
+ restoreScrollPosition(this.context);
+ setupTippy();
+
+ break;
+ }
+
+ case UserOperation.EditPost:
+ case UserOperation.DeletePost:
+ case UserOperation.RemovePost:
+ case UserOperation.LockPost:
+ case UserOperation.FeaturePost:
+ case UserOperation.SavePost: {
+ const { post_view } = wsJsonToRes<PostResponse>(msg);
+
+ editPostFindRes(post_view, this.state.posts);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.CreatePost: {
+ const { post_view } = wsJsonToRes<PostResponse>(msg);
+
+ const showPostNotifs =
+ UserService.Instance.myUserInfo?.local_user_view.local_user
+ .show_new_post_notifs;
+
+ // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
+ if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
+ this.state.posts.unshift(post_view);
+ if (showPostNotifs) {
+ notifyPost(post_view, this.context.router);
+ }
+ this.setState(this.state);
+ }
+
+ break;
+ }
+
+ case UserOperation.CreatePostLike: {
+ const { post_view } = wsJsonToRes<PostResponse>(msg);
+
+ createPostLikeFindRes(post_view, this.state.posts);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.AddModToCommunity: {
+ const { moderators } = wsJsonToRes<AddModToCommunityResponse>(msg);
+
+ if (res) {
+ res.moderators = moderators;
+ this.setState(this.state);
+ }
+
+ break;
+ }
+
+ case UserOperation.BanFromCommunity: {
+ const {
+ person_view: {
+ person: { id: personId },
+ },
+ banned,
+ } = wsJsonToRes<BanFromCommunityResponse>(msg);
+
+ // TODO this might be incorrect
+ this.state.posts
+ .filter(p => p.creator.id === personId)
+ .forEach(p => (p.creator_banned_from_community = banned));
+
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.GetComments: {
+ const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
+ this.setState({ comments, listingsLoading: false });
+
+ break;
+ }
+
+ case UserOperation.EditComment:
+ case UserOperation.DeleteComment:
+ case UserOperation.RemoveComment: {
+ const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+ editCommentRes(comment_view, this.state.comments);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.CreateComment: {
+ const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
+
+ // Necessary since it might be a user reply
+ if (form_id) {
+ this.setState(({ comments }) => ({
+ comments: [comment_view].concat(comments),
+ }));
+ }
+
+ break;
+ }
+
+ case UserOperation.SaveComment: {
+ const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+
+ saveCommentRes(comment_view, this.state.comments);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.CreateCommentLike: {
+ const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+
+ createCommentLikeRes(comment_view, this.state.comments);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.BlockPerson: {
+ const data = wsJsonToRes<BlockPersonResponse>(msg);
+ updatePersonBlock(data);
+
+ break;
+ }
+
+ case UserOperation.CreatePostReport:
+ case UserOperation.CreateCommentReport: {
+ const data = wsJsonToRes<PostReportResponse>(msg);
+
+ if (data) {
+ toast(i18n.t("report_created"));
+ }
+
+ break;
+ }
+
+ case UserOperation.PurgeCommunity: {
+ const { success } = wsJsonToRes<PurgeItemResponse>(msg);
+
+ if (success) {
+ toast(i18n.t("purge_success"));
+ this.context.router.history.push(`/`);
+ }
+
+ break;
+ }
+
+ case UserOperation.BlockCommunity: {
+ const data = wsJsonToRes<BlockCommunityResponse>(msg);
+ if (res) {
+ res.community_view.blocked = data.blocked;
+ this.setState(this.state);
+ }
+ updateCommunityBlock(data);
+
+ break;
}
- this.setState(this.state);
- }
- } else if (op == UserOperation.CreatePostLike) {
- let data = wsJsonToRes<PostResponse>(msg);
- createPostLikeFindRes(data.post_view, this.state.posts);
- this.setState(this.state);
- } else if (op == UserOperation.AddModToCommunity) {
- let data = wsJsonToRes<AddModToCommunityResponse>(msg);
- if (res) {
- res.moderators = data.moderators;
- }
- this.setState(this.state);
- } else if (op == UserOperation.BanFromCommunity) {
- let data = wsJsonToRes<BanFromCommunityResponse>(msg);
-
- // TODO this might be incorrect
- this.state.posts
- .filter(p => p.creator.id == data.person_view.person.id)
- .forEach(p => (p.creator_banned_from_community = data.banned));
-
- this.setState(this.state);
- } else if (op == UserOperation.GetComments) {
- let data = wsJsonToRes<GetCommentsResponse>(msg);
- this.setState({ comments: data.comments, commentsLoading: false });
- } else if (
- op == UserOperation.EditComment ||
- op == UserOperation.DeleteComment ||
- op == UserOperation.RemoveComment
- ) {
- let data = wsJsonToRes<CommentResponse>(msg);
- editCommentRes(data.comment_view, this.state.comments);
- this.setState(this.state);
- } else if (op == UserOperation.CreateComment) {
- let data = wsJsonToRes<CommentResponse>(msg);
-
- // Necessary since it might be a user reply
- if (data.form_id) {
- this.state.comments.unshift(data.comment_view);
- this.setState(this.state);
- }
- } else if (op == UserOperation.SaveComment) {
- let data = wsJsonToRes<CommentResponse>(msg);
- saveCommentRes(data.comment_view, this.state.comments);
- this.setState(this.state);
- } else if (op == UserOperation.CreateCommentLike) {
- let data = wsJsonToRes<CommentResponse>(msg);
- createCommentLikeRes(data.comment_view, this.state.comments);
- this.setState(this.state);
- } else if (op == UserOperation.BlockPerson) {
- let data = wsJsonToRes<BlockPersonResponse>(msg);
- updatePersonBlock(data);
- } else if (op == UserOperation.CreatePostReport) {
- let data = wsJsonToRes<PostReportResponse>(msg);
- if (data) {
- toast(i18n.t("report_created"));
- }
- } else if (op == UserOperation.CreateCommentReport) {
- let data = wsJsonToRes<CommentReportResponse>(msg);
- if (data) {
- toast(i18n.t("report_created"));
- }
- } else if (op == UserOperation.PurgeCommunity) {
- let data = wsJsonToRes<PurgeItemResponse>(msg);
- if (data.success) {
- toast(i18n.t("purge_success"));
- this.context.router.history.push(`/`);
- }
- } else if (op == UserOperation.BlockCommunity) {
- let data = wsJsonToRes<BlockCommunityResponse>(msg);
- if (res) {
- res.community_view.blocked = data.blocked;
}
- updateCommunityBlock(data);
- this.setState(this.state);
}
}
}
className={`btn btn-secondary btn-block mb-2 ${
cv.community.deleted || cv.community.removed ? "no-click" : ""
}`}
- to={`/create_post?community_id=${cv.community.id}`}
+ to={`/create_post?communityId=${cv.community.id}`}
>
{i18n.t("create_a_post")}
</Link>
-import { Component, linkEvent } from "inferno";
+import { NoOptionI18nKeys } from "i18next";
+import { Component, linkEvent, MouseEventHandler } from "inferno";
import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router";
import {
enableDownvotes,
enableNsfw,
fetchLimit,
- getDataTypeFromProps,
- getListingTypeFromProps,
- getPageFromProps,
+ getDataTypeString,
+ getPageFromString,
+ getQueryParams,
+ getQueryString,
getRandomFromList,
- getSortTypeFromProps,
isBrowser,
isPostBlocked,
mdToHtml,
notifyPost,
nsfwCheck,
postToCommentSortType,
+ QueryParams,
relTags,
restoreScrollPosition,
+ routeDataTypeToEnum,
+ routeListingTypeToEnum,
+ routeSortTypeToEnum,
saveCommentRes,
saveScrollPosition,
setIsoData,
siteRes: GetSiteResponse;
posts: PostView[];
comments: CommentView[];
- listingType: ListingType;
- dataType: DataType;
- sort: SortType;
- page: number;
showSubscribedMobile: boolean;
showTrendingMobile: boolean;
showSidebarMobile: boolean;
page: number;
}
-interface UrlParams {
- listingType?: ListingType;
- dataType?: string;
- sort?: SortType;
- page?: number;
+const getDataTypeFromQuery = (type?: string) =>
+ routeDataTypeToEnum(type ?? "", DataType.Post);
+
+function getListingTypeFromQuery(type?: string) {
+ const mui = UserService.Instance.myUserInfo;
+
+ return routeListingTypeToEnum(
+ type ?? "",
+ mui
+ ? Object.values(ListingType)[
+ mui.local_user_view.local_user.default_listing_type
+ ]
+ : ListingType.Local
+ );
+}
+
+function getSortTypeFromQuery(type?: string) {
+ const mui = UserService.Instance.myUserInfo;
+
+ return routeSortTypeToEnum(
+ type ?? "",
+ mui
+ ? Object.values(SortType)[
+ mui.local_user_view.local_user.default_listing_type
+ ]
+ : SortType.Active
+ );
+}
+
+const getHomeQueryParams = () =>
+ getQueryParams<HomeProps>({
+ sort: getSortTypeFromQuery,
+ listingType: getListingTypeFromQuery,
+ page: getPageFromString,
+ dataType: getDataTypeFromQuery,
+ });
+
+function fetchTrendingCommunities() {
+ const listCommunitiesForm: ListCommunities = {
+ type_: ListingType.Local,
+ sort: SortType.Hot,
+ limit: trendingFetchLimit,
+ auth: myAuth(false),
+ };
+ WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
+}
+
+function fetchData() {
+ const auth = myAuth(false);
+ const { dataType, page, listingType, sort } = getHomeQueryParams();
+ let req: string;
+
+ if (dataType === DataType.Post) {
+ const getPostsForm: GetPosts = {
+ page,
+ limit: fetchLimit,
+ sort,
+ saved_only: false,
+ type_: listingType,
+ auth,
+ };
+
+ req = wsClient.getPosts(getPostsForm);
+ } else {
+ const getCommentsForm: GetComments = {
+ page,
+ limit: fetchLimit,
+ sort: postToCommentSortType(sort),
+ saved_only: false,
+ type_: listingType,
+ auth,
+ };
+
+ req = wsClient.getComments(getCommentsForm);
+ }
+
+ WebSocketService.Instance.send(req);
+}
+
+const MobileButton = ({
+ textKey,
+ show,
+ onClick,
+}: {
+ textKey: NoOptionI18nKeys;
+ show: boolean;
+ onClick: MouseEventHandler<HTMLButtonElement>;
+}) => (
+ <button
+ className="btn btn-secondary d-inline-block mb-2 mr-3"
+ onClick={onClick}
+ >
+ {i18n.t(textKey)}{" "}
+ <Icon icon={show ? `minus-square` : `plus-square`} classes="icon-inline" />
+ </button>
+);
+
+const LinkButton = ({
+ path,
+ translationKey,
+}: {
+ path: string;
+ translationKey: NoOptionI18nKeys;
+}) => (
+ <Link className="btn btn-secondary btn-block" to={path}>
+ {i18n.t(translationKey)}
+ </Link>
+);
+
+function getRss(listingType: ListingType) {
+ const { sort } = getHomeQueryParams();
+ const auth = myAuth(false);
+
+ let rss: string | undefined = undefined;
+
+ switch (listingType) {
+ case ListingType.All: {
+ rss = `/feeds/all.xml?sort=${sort}`;
+ break;
+ }
+ case ListingType.Local: {
+ rss = `/feeds/local.xml?sort=${sort}`;
+ break;
+ }
+ case ListingType.Subscribed: {
+ rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
+ break;
+ }
+ }
+
+ return (
+ rss && (
+ <>
+ <a href={rss} rel={relTags} title="RSS">
+ <Icon icon="rss" classes="text-muted small" />
+ </a>
+ <link rel="alternate" type="application/atom+xml" href={rss} />
+ </>
+ )
+ );
}
export class Home extends Component<any, HomeState> {
loading: true,
posts: [],
comments: [],
- listingType: getListingTypeFromProps(
- this.props,
- ListingType[
- this.isoData.site_res.site_view.local_site.default_post_listing_type
- ]
- ),
- dataType: getDataTypeFromProps(this.props),
- sort: getSortTypeFromProps(this.props),
- page: getPageFromProps(this.props),
};
constructor(props: any, context: any) {
this.subscription = wsSubscribe(this.parseMessage);
// Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
- let postsRes = this.isoData.routeData[0] as GetPostsResponse | undefined;
- let commentsRes = this.isoData.routeData[1] as
+ if (this.isoData.path === this.context.router.route.match.url) {
+ const postsRes = this.isoData.routeData[0] as
+ | GetPostsResponse
+ | undefined;
+ const commentsRes = this.isoData.routeData[1] as
| GetCommentsResponse
| undefined;
- let trendingRes = this.isoData.routeData[2] as
+ const trendingRes = this.isoData.routeData[2] as
| ListCommunitiesResponse
| undefined;
tagline: getRandomFromList(taglines)?.content,
};
} else {
- this.fetchTrendingCommunities();
- this.fetchData();
+ fetchTrendingCommunities();
+ fetchData();
}
}
- fetchTrendingCommunities() {
- let listCommunitiesForm: ListCommunities = {
- type_: ListingType.Local,
- sort: SortType.Hot,
- limit: trendingFetchLimit,
- auth: myAuth(false),
- };
- WebSocketService.Instance.send(
- wsClient.listCommunities(listCommunitiesForm)
- );
- }
-
componentDidMount() {
// This means it hasn't been set up yet
if (!this.state.siteRes.site_view.local_site.site_setup) {
this.subscription?.unsubscribe();
}
- static getDerivedStateFromProps(
- props: HomeProps,
- state: HomeState
- ): HomeProps {
- return {
- listingType: getListingTypeFromProps(props, state.listingType),
- dataType: getDataTypeFromProps(props),
- sort: getSortTypeFromProps(props),
- page: getPageFromProps(props),
- };
- }
-
- static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
- let pathSplit = req.path.split("/");
- let dataType: DataType = pathSplit[3]
- ? DataType[pathSplit[3]]
- : DataType.Post;
- let mui = UserService.Instance.myUserInfo;
- let auth = req.auth;
+ static fetchInitialData({
+ client,
+ auth,
+ query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
+ }: InitialFetchRequest<QueryParams<HomeProps>>): Promise<any>[] {
+ const dataType = getDataTypeFromQuery(urlDataType);
// TODO figure out auth default_listingType, default_sort_type
- let type_: ListingType = pathSplit[5]
- ? ListingType[pathSplit[5]]
- : mui
- ? Object.values(ListingType)[
- mui.local_user_view.local_user.default_listing_type
- ]
- : ListingType.Local;
- let sort: SortType = pathSplit[7]
- ? SortType[pathSplit[7]]
- : mui
- ? (Object.values(SortType)[
- mui.local_user_view.local_user.default_sort_type
- ] as SortType)
- : SortType.Active;
+ const type_ = getListingTypeFromQuery(listingType);
+ const sort = getSortTypeFromQuery(urlSort);
- let page = pathSplit[9] ? Number(pathSplit[9]) : 1;
+ const page = urlPage ? Number(urlPage) : 1;
- let promises: Promise<any>[] = [];
+ const promises: Promise<any>[] = [];
- if (dataType == DataType.Post) {
- let getPostsForm: GetPosts = {
+ if (dataType === DataType.Post) {
+ const getPostsForm: GetPosts = {
type_,
page,
limit: fetchLimit,
auth,
};
- promises.push(req.client.getPosts(getPostsForm));
+ promises.push(client.getPosts(getPostsForm));
promises.push(Promise.resolve());
} else {
- let getCommentsForm: GetComments = {
+ const getCommentsForm: GetComments = {
page,
limit: fetchLimit,
sort: postToCommentSortType(sort),
auth,
};
promises.push(Promise.resolve());
- promises.push(req.client.getComments(getCommentsForm));
+ promises.push(client.getComments(getCommentsForm));
}
- let trendingCommunitiesForm: ListCommunities = {
+ const trendingCommunitiesForm: ListCommunities = {
type_: ListingType.Local,
sort: SortType.Hot,
limit: trendingFetchLimit,
auth,
};
- promises.push(req.client.listCommunities(trendingCommunitiesForm));
+ promises.push(client.listCommunities(trendingCommunitiesForm));
return promises;
}
- componentDidUpdate(_: any, lastState: HomeState) {
- if (
- lastState.listingType !== this.state.listingType ||
- lastState.dataType !== this.state.dataType ||
- lastState.sort !== this.state.sort ||
- lastState.page !== this.state.page
- ) {
- this.setState({ loading: true });
- this.fetchData();
- }
- }
-
get documentTitle(): string {
- let siteView = this.state.siteRes.site_view;
- let desc = this.state.siteRes.site_view.site.description;
- return desc ? `${siteView.site.name} - ${desc}` : siteView.site.name;
+ const { name, description } = this.state.siteRes.site_view.site;
+
+ return description ? `${name} - ${description}` : name;
}
render() {
- let tagline = this.state.tagline;
+ const {
+ tagline,
+ siteRes: {
+ site_view: {
+ local_site: { site_setup },
+ },
+ },
+ } = this.state;
return (
<div className="container-lg">
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
- {this.state.siteRes.site_view.local_site.site_setup && (
+ {site_setup && (
<div className="row">
<main role="main" className="col-12 col-md-8">
{tagline && (
dangerouslySetInnerHTML={mdToHtml(tagline)}
></div>
)}
- <div className="d-block d-md-none">{this.mobileView()}</div>
+ <div className="d-block d-md-none">{this.mobileView}</div>
{this.posts()}
</main>
<aside className="d-none d-md-block col-md-4">
- {this.mySidebar()}
+ {this.mySidebar}
</aside>
</div>
)}
}
get hasFollows(): boolean {
- let mui = UserService.Instance.myUserInfo;
+ const mui = UserService.Instance.myUserInfo;
return !!mui && mui.follows.length > 0;
}
- mobileView() {
- let siteRes = this.state.siteRes;
- let siteView = siteRes.site_view;
+ get mobileView() {
+ const {
+ siteRes: {
+ site_view: { counts, site },
+ admins,
+ online,
+ },
+ showSubscribedMobile,
+ showTrendingMobile,
+ showSidebarMobile,
+ } = this.state;
+
return (
<div className="row">
<div className="col-12">
{this.hasFollows && (
- <button
- className="btn btn-secondary d-inline-block mb-2 mr-3"
+ <MobileButton
+ textKey="subscribed"
+ show={showSubscribedMobile}
onClick={linkEvent(this, this.handleShowSubscribedMobile)}
- >
- {i18n.t("subscribed")}{" "}
- <Icon
- icon={
- this.state.showSubscribedMobile
- ? `minus-square`
- : `plus-square`
- }
- classes="icon-inline"
- />
- </button>
+ />
)}
- <button
- className="btn btn-secondary d-inline-block mb-2 mr-3"
+ <MobileButton
+ textKey="trending"
+ show={showTrendingMobile}
onClick={linkEvent(this, this.handleShowTrendingMobile)}
- >
- {i18n.t("trending")}{" "}
- <Icon
- icon={
- this.state.showTrendingMobile ? `minus-square` : `plus-square`
- }
- classes="icon-inline"
- />
- </button>
- <button
- className="btn btn-secondary d-inline-block mb-2 mr-3"
+ />
+ <MobileButton
+ textKey="sidebar"
+ show={showSidebarMobile}
onClick={linkEvent(this, this.handleShowSidebarMobile)}
- >
- {i18n.t("sidebar")}{" "}
- <Icon
- icon={
- this.state.showSidebarMobile ? `minus-square` : `plus-square`
- }
- classes="icon-inline"
- />
- </button>
- {this.state.showSidebarMobile && (
+ />
+ {showSidebarMobile && (
<SiteSidebar
- site={siteView.site}
- admins={siteRes.admins}
- counts={siteView.counts}
- online={siteRes.online}
+ site={site}
+ admins={admins}
+ counts={counts}
+ online={online}
showLocal={showLocal(this.isoData)}
/>
)}
- {this.state.showTrendingMobile && (
+ {showTrendingMobile && (
<div className="col-12 card border-secondary mb-3">
- <div className="card-body">{this.trendingCommunities()}</div>
+ <div className="card-body">{this.trendingCommunities(true)}</div>
</div>
)}
- {this.state.showSubscribedMobile && (
+ {showSubscribedMobile && (
<div className="col-12 card border-secondary mb-3">
- <div className="card-body">{this.subscribedCommunities()}</div>
+ <div className="card-body">{this.subscribedCommunities}</div>
</div>
)}
</div>
);
}
- mySidebar() {
- let siteRes = this.state.siteRes;
- let siteView = siteRes.site_view;
+ get mySidebar() {
+ const {
+ siteRes: {
+ site_view: { counts, site },
+ admins,
+ online,
+ },
+ loading,
+ } = this.state;
+
return (
<div>
- {!this.state.loading && (
+ {!loading && (
<div>
<div className="card border-secondary mb-3">
<div className="card-body">
{this.trendingCommunities()}
- {canCreateCommunity(this.state.siteRes) &&
- this.createCommunityButton()}
- {this.exploreCommunitiesButton()}
+ {canCreateCommunity(this.state.siteRes) && (
+ <LinkButton
+ path="/create_community"
+ translationKey="create_a_community"
+ />
+ )}
+ <LinkButton
+ path="/communities"
+ translationKey="explore_communities"
+ />
</div>
</div>
<SiteSidebar
- site={siteView.site}
- admins={siteRes.admins}
- counts={siteView.counts}
- online={siteRes.online}
+ site={site}
+ admins={admins}
+ counts={counts}
+ online={online}
showLocal={showLocal(this.isoData)}
/>
{this.hasFollows && (
<div className="card border-secondary mb-3">
- <div className="card-body">{this.subscribedCommunities()}</div>
+ <div className="card-body">{this.subscribedCommunities}</div>
</div>
)}
</div>
);
}
- createCommunityButton() {
+ trendingCommunities(isMobile = false) {
return (
- <Link className="mt-2 btn btn-secondary btn-block" to="/create_community">
- {i18n.t("create_a_community")}
- </Link>
- );
- }
-
- exploreCommunitiesButton() {
- return (
- <Link className="btn btn-secondary btn-block" to="/communities">
- {i18n.t("explore_communities")}
- </Link>
- );
- }
-
- trendingCommunities() {
- return (
- <div>
+ <div className={!isMobile ? "mb-2" : ""}>
<h5>
<T i18nKey="trending_communities">
#
);
}
- subscribedCommunities() {
+ get subscribedCommunities() {
+ const { subscribedCollapsed } = this.state;
+
return (
<div>
<h5>
aria-label={i18n.t("collapse")}
data-tippy-content={i18n.t("collapse")}
>
- {this.state.subscribedCollapsed ? (
- <Icon icon="plus-square" classes="icon-inline" />
- ) : (
- <Icon icon="minus-square" classes="icon-inline" />
- )}
+ <Icon
+ icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
+ classes="icon-inline"
+ />
</button>
</h5>
- {!this.state.subscribedCollapsed && (
+ {!subscribedCollapsed && (
<ul className="list-inline mb-0">
{UserService.Instance.myUserInfo?.follows.map(cfv => (
<li
);
}
- updateUrl(paramUpdates: UrlParams) {
- const listingTypeStr = paramUpdates.listingType || this.state.listingType;
- const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
- const sortStr = paramUpdates.sort || this.state.sort;
- const page = paramUpdates.page || this.state.page;
- this.props.history.push(
- `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
- );
+ updateUrl({ dataType, listingType, page, sort }: Partial<HomeProps>) {
+ const {
+ dataType: urlDataType,
+ listingType: urlListingType,
+ page: urlPage,
+ sort: urlSort,
+ } = getHomeQueryParams();
+
+ const queryParams: QueryParams<HomeProps> = {
+ dataType: getDataTypeString(dataType ?? urlDataType),
+ listingType: listingType ?? urlListingType,
+ page: (page ?? urlPage).toString(),
+ sort: sort ?? urlSort,
+ };
+
+ this.props.history.push({
+ pathname: "/",
+ search: getQueryString(queryParams),
+ });
+
+ this.setState({
+ loading: true,
+ posts: [],
+ comments: [],
+ });
+
+ fetchData();
}
posts() {
+ const { page } = getHomeQueryParams();
+
return (
<div className="main-content-wrapper">
{this.state.loading ? (
) : (
<div>
{this.selects()}
- {this.listings()}
- <Paginator
- page={this.state.page}
- onChange={this.handlePageChange}
- />
+ {this.listings}
+ <Paginator page={page} onChange={this.handlePageChange} />
</div>
)}
</div>
);
}
- listings() {
- return this.state.dataType == DataType.Post ? (
+ get listings() {
+ const { dataType } = getHomeQueryParams();
+ const { siteRes, posts, comments } = this.state;
+
+ return dataType === DataType.Post ? (
<PostListings
- posts={this.state.posts}
+ posts={posts}
showCommunity
removeDuplicates
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- enableNsfw={enableNsfw(this.state.siteRes)}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
+ enableDownvotes={enableDownvotes(siteRes)}
+ enableNsfw={enableNsfw(siteRes)}
+ allLanguages={siteRes.all_languages}
+ siteLanguages={siteRes.discussion_languages}
/>
) : (
<CommentNodes
- nodes={commentsToFlatNodes(this.state.comments)}
+ nodes={commentsToFlatNodes(comments)}
viewType={CommentViewType.Flat}
noIndent
showCommunity
showContext
- 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}
/>
);
}
selects() {
- let allRss = `/feeds/all.xml?sort=${this.state.sort}`;
- let localRss = `/feeds/local.xml?sort=${this.state.sort}`;
- let auth = myAuth(false);
- let frontRss = auth
- ? `/feeds/front/${auth}.xml?sort=${this.state.sort}`
- : undefined;
+ const { listingType, dataType, sort } = getHomeQueryParams();
return (
<div className="mb-3">
<span className="mr-3">
<DataTypeSelect
- type_={this.state.dataType}
+ type_={dataType}
onChange={this.handleDataTypeChange}
/>
</span>
<span className="mr-3">
<ListingTypeSelect
- type_={this.state.listingType}
+ type_={listingType}
showLocal={showLocal(this.isoData)}
showSubscribed
onChange={this.handleListingTypeChange}
/>
</span>
<span className="mr-2">
- <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
+ <SortSelect sort={sort} onChange={this.handleSortChange} />
</span>
- {this.state.listingType == ListingType.All && (
- <>
- <a href={allRss} rel={relTags} title="RSS">
- <Icon icon="rss" classes="text-muted small" />
- </a>
- <link rel="alternate" type="application/atom+xml" href={allRss} />
- </>
- )}
- {this.state.listingType == ListingType.Local && (
- <>
- <a href={localRss} rel={relTags} title="RSS">
- <Icon icon="rss" classes="text-muted small" />
- </a>
- <link rel="alternate" type="application/atom+xml" href={localRss} />
- </>
- )}
- {this.state.listingType == ListingType.Subscribed && frontRss && (
- <>
- <a href={frontRss} title="RSS" rel={relTags}>
- <Icon icon="rss" classes="text-muted small" />
- </a>
- <link rel="alternate" type="application/atom+xml" href={frontRss} />
- </>
- )}
+ {getRss(listingType)}
</div>
);
}
}
handleDataTypeChange(val: DataType) {
- this.updateUrl({ dataType: DataType[val], page: 1 });
+ this.updateUrl({ dataType: val, page: 1 });
window.scrollTo(0, 0);
}
- fetchData() {
- let auth = myAuth(false);
- if (this.state.dataType == DataType.Post) {
- let getPostsForm: GetPosts = {
- page: this.state.page,
- limit: fetchLimit,
- sort: this.state.sort,
- saved_only: false,
- type_: this.state.listingType,
- auth,
- };
-
- WebSocketService.Instance.send(wsClient.getPosts(getPostsForm));
- } else {
- let getCommentsForm: GetComments = {
- page: this.state.page,
- limit: fetchLimit,
- sort: postToCommentSortType(this.state.sort),
- saved_only: false,
- type_: this.state.listingType,
- auth,
- };
- WebSocketService.Instance.send(wsClient.getComments(getCommentsForm));
- }
- }
-
parseMessage(msg: any) {
- let op = wsUserOp(msg);
+ const op = wsUserOp(msg);
console.log(msg);
+
if (msg.error) {
toast(i18n.t(msg.error), "danger");
- return;
} else if (msg.reconnect) {
WebSocketService.Instance.send(
wsClient.communityJoin({ community_id: 0 })
);
- this.fetchData();
- } else if (op == UserOperation.ListCommunities) {
- let data = wsJsonToRes<ListCommunitiesResponse>(msg);
- this.setState({ trendingCommunities: data.communities });
- } else if (op == UserOperation.EditSite) {
- let data = wsJsonToRes<SiteResponse>(msg);
- this.setState(s => ((s.siteRes.site_view = data.site_view), s));
- toast(i18n.t("site_saved"));
- } else if (op == UserOperation.GetPosts) {
- let data = wsJsonToRes<GetPostsResponse>(msg);
- this.setState({ posts: data.posts, loading: false });
- WebSocketService.Instance.send(
- wsClient.communityJoin({ community_id: 0 })
- );
- restoreScrollPosition(this.context);
- setupTippy();
- } else if (op == UserOperation.CreatePost) {
- let data = wsJsonToRes<PostResponse>(msg);
- let mui = UserService.Instance.myUserInfo;
-
- let showPostNotifs = mui?.local_user_view.local_user.show_new_post_notifs;
-
- // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
- if (
- this.state.page == 1 &&
- nsfwCheck(data.post_view) &&
- !isPostBlocked(data.post_view)
- ) {
- // If you're on subscribed, only push it if you're subscribed.
- if (this.state.listingType == ListingType.Subscribed) {
- if (
- mui?.follows
- .map(c => c.community.id)
- .includes(data.post_view.community.id)
- ) {
- this.state.posts.unshift(data.post_view);
- if (showPostNotifs) {
- notifyPost(data.post_view, this.context.router);
+ fetchData();
+ } else {
+ switch (op) {
+ case UserOperation.ListCommunities: {
+ const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
+ this.setState({ trendingCommunities: communities });
+
+ break;
+ }
+
+ case UserOperation.EditSite: {
+ const { site_view } = wsJsonToRes<SiteResponse>(msg);
+ this.setState(s => ((s.siteRes.site_view = site_view), s));
+ toast(i18n.t("site_saved"));
+
+ break;
+ }
+
+ case UserOperation.GetPosts: {
+ const { posts } = wsJsonToRes<GetPostsResponse>(msg);
+ this.setState({ posts, loading: false });
+ WebSocketService.Instance.send(
+ wsClient.communityJoin({ community_id: 0 })
+ );
+ restoreScrollPosition(this.context);
+ setupTippy();
+
+ break;
+ }
+
+ case UserOperation.CreatePost: {
+ const { page, listingType } = getHomeQueryParams();
+ const { post_view } = wsJsonToRes<PostResponse>(msg);
+
+ // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
+ if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
+ const mui = UserService.Instance.myUserInfo;
+ const showPostNotifs =
+ mui?.local_user_view.local_user.show_new_post_notifs;
+ let shouldAddPost: boolean;
+
+ switch (listingType) {
+ case ListingType.Subscribed: {
+ // If you're on subscribed, only push it if you're subscribed.
+ shouldAddPost = !!mui?.follows.some(
+ ({ community: { id } }) => id === post_view.community.id
+ );
+ break;
+ }
+ case ListingType.Local: {
+ // If you're on the local view, only push it if its local
+ shouldAddPost = post_view.post.local;
+ break;
+ }
+ default: {
+ shouldAddPost = true;
+ break;
+ }
+ }
+
+ if (shouldAddPost) {
+ this.setState(({ posts }) => ({
+ posts: [post_view].concat(posts),
+ }));
+ if (showPostNotifs) {
+ notifyPost(post_view, this.context.router);
+ }
}
}
- } else if (this.state.listingType == ListingType.Local) {
- // If you're on the local view, only push it if its local
- if (data.post_view.post.local) {
- this.state.posts.unshift(data.post_view);
- if (showPostNotifs) {
- notifyPost(data.post_view, this.context.router);
+
+ break;
+ }
+
+ case UserOperation.EditPost:
+ case UserOperation.DeletePost:
+ case UserOperation.RemovePost:
+ case UserOperation.LockPost:
+ case UserOperation.FeaturePost:
+ case UserOperation.SavePost: {
+ const { post_view } = wsJsonToRes<PostResponse>(msg);
+ editPostFindRes(post_view, this.state.posts);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.CreatePostLike: {
+ const { post_view } = wsJsonToRes<PostResponse>(msg);
+ createPostLikeFindRes(post_view, this.state.posts);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.AddAdmin: {
+ const { admins } = wsJsonToRes<AddAdminResponse>(msg);
+ this.setState(s => ((s.siteRes.admins = admins), s));
+
+ break;
+ }
+
+ case UserOperation.BanPerson: {
+ const {
+ banned,
+ person_view: {
+ person: { id },
+ },
+ } = wsJsonToRes<BanPersonResponse>(msg);
+
+ this.state.posts
+ .filter(p => p.creator.id == id)
+ .forEach(p => (p.creator.banned = banned));
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.GetComments: {
+ const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
+ this.setState({ comments, loading: false });
+
+ break;
+ }
+
+ case UserOperation.EditComment:
+ case UserOperation.DeleteComment:
+ case UserOperation.RemoveComment: {
+ const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+ editCommentRes(comment_view, this.state.comments);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.CreateComment: {
+ const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
+
+ // Necessary since it might be a user reply
+ if (form_id) {
+ const { listingType } = getHomeQueryParams();
+
+ // If you're on subscribed, only push it if you're subscribed.
+ const shouldAddComment =
+ listingType === ListingType.Subscribed
+ ? UserService.Instance.myUserInfo?.follows.some(
+ ({ community: { id } }) => id === comment_view.community.id
+ )
+ : true;
+
+ if (shouldAddComment) {
+ this.setState(({ comments }) => ({
+ comments: [comment_view].concat(comments),
+ }));
}
}
- } else {
- this.state.posts.unshift(data.post_view);
- if (showPostNotifs) {
- notifyPost(data.post_view, this.context.router);
+
+ break;
+ }
+
+ case UserOperation.SaveComment: {
+ const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+ saveCommentRes(comment_view, this.state.comments);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.CreateCommentLike: {
+ const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+ createCommentLikeRes(comment_view, this.state.comments);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.BlockPerson: {
+ const data = wsJsonToRes<BlockPersonResponse>(msg);
+ updatePersonBlock(data);
+
+ break;
+ }
+
+ case UserOperation.CreatePostReport: {
+ const data = wsJsonToRes<PostReportResponse>(msg);
+ if (data) {
+ toast(i18n.t("report_created"));
}
+
+ break;
}
- this.setState(this.state);
- }
- } else if (
- op == UserOperation.EditPost ||
- op == UserOperation.DeletePost ||
- op == UserOperation.RemovePost ||
- op == UserOperation.LockPost ||
- op == UserOperation.FeaturePost ||
- op == UserOperation.SavePost
- ) {
- let data = wsJsonToRes<PostResponse>(msg);
- editPostFindRes(data.post_view, this.state.posts);
- this.setState(this.state);
- } else if (op == UserOperation.CreatePostLike) {
- let data = wsJsonToRes<PostResponse>(msg);
- createPostLikeFindRes(data.post_view, this.state.posts);
- this.setState(this.state);
- } else if (op == UserOperation.AddAdmin) {
- let data = wsJsonToRes<AddAdminResponse>(msg);
- this.setState(s => ((s.siteRes.admins = data.admins), s));
- } else if (op == UserOperation.BanPerson) {
- let data = wsJsonToRes<BanPersonResponse>(msg);
- this.state.posts
- .filter(p => p.creator.id == data.person_view.person.id)
- .forEach(p => (p.creator.banned = data.banned));
-
- this.setState(this.state);
- } else if (op == UserOperation.GetComments) {
- let data = wsJsonToRes<GetCommentsResponse>(msg);
- this.setState({ comments: data.comments, loading: false });
- } else if (
- op == UserOperation.EditComment ||
- op == UserOperation.DeleteComment ||
- op == UserOperation.RemoveComment
- ) {
- let data = wsJsonToRes<CommentResponse>(msg);
- editCommentRes(data.comment_view, this.state.comments);
- this.setState(this.state);
- } else if (op == UserOperation.CreateComment) {
- let data = wsJsonToRes<CommentResponse>(msg);
-
- // Necessary since it might be a user reply
- if (data.form_id) {
- // If you're on subscribed, only push it if you're subscribed.
- if (this.state.listingType == ListingType.Subscribed) {
- if (
- UserService.Instance.myUserInfo?.follows
- .map(c => c.community.id)
- .includes(data.comment_view.community.id)
- ) {
- this.state.comments.unshift(data.comment_view);
+
+ case UserOperation.CreateCommentReport: {
+ const data = wsJsonToRes<CommentReportResponse>(msg);
+ if (data) {
+ toast(i18n.t("report_created"));
}
- } else {
- this.state.comments.unshift(data.comment_view);
+
+ break;
+ }
+
+ case UserOperation.PurgePerson:
+ case UserOperation.PurgePost:
+ case UserOperation.PurgeComment:
+ case UserOperation.PurgeCommunity: {
+ const data = wsJsonToRes<PurgeItemResponse>(msg);
+ if (data.success) {
+ toast(i18n.t("purge_success"));
+ this.context.router.history.push(`/`);
+ }
+
+ break;
}
- this.setState(this.state);
- }
- } else if (op == UserOperation.SaveComment) {
- let data = wsJsonToRes<CommentResponse>(msg);
- saveCommentRes(data.comment_view, this.state.comments);
- this.setState(this.state);
- } else if (op == UserOperation.CreateCommentLike) {
- let data = wsJsonToRes<CommentResponse>(msg);
- createCommentLikeRes(data.comment_view, this.state.comments);
- this.setState(this.state);
- } else if (op == UserOperation.BlockPerson) {
- let data = wsJsonToRes<BlockPersonResponse>(msg);
- updatePersonBlock(data);
- } else if (op == UserOperation.CreatePostReport) {
- let data = wsJsonToRes<PostReportResponse>(msg);
- if (data) {
- toast(i18n.t("report_created"));
- }
- } else if (op == UserOperation.CreateCommentReport) {
- let data = wsJsonToRes<CommentReportResponse>(msg);
- if (data) {
- toast(i18n.t("report_created"));
- }
- } else if (
- op == UserOperation.PurgePerson ||
- op == UserOperation.PurgePost ||
- op == UserOperation.PurgeComment ||
- op == UserOperation.PurgeCommunity
- ) {
- let data = wsJsonToRes<PurgeItemResponse>(msg);
- if (data.success) {
- toast(i18n.t("purge_success"));
- this.context.router.history.push(`/`);
}
}
}
+import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router";
+import { RouteComponentProps } from "inferno-router/dist/Route";
import {
AdminPurgeCommentView,
AdminPurgeCommunityView,
GetCommunityResponse,
GetModlog,
GetModlogResponse,
- GetSiteResponse,
+ GetPersonDetails,
+ GetPersonDetailsResponse,
ModAddCommunityView,
ModAddView,
ModBanFromCommunityView,
import {
amAdmin,
amMod,
- choicesConfig,
+ Choice,
debounce,
fetchLimit,
fetchUsers,
+ getIdFromString,
+ getPageFromString,
+ getQueryParams,
+ getQueryString,
+ getUpdatedSearchId,
isBrowser,
myAuth,
+ personToChoice,
+ QueryParams,
setIsoData,
toast,
wsClient,
import { Spinner } from "./common/icon";
import { MomentTime } from "./common/moment-time";
import { Paginator } from "./common/paginator";
+import { SearchableSelect } from "./common/searchable-select";
import { CommunityLink } from "./community/community-link";
import { PersonListing } from "./person/person-listing";
-type ModlogType = {
+
+type FilterType = "mod" | "user";
+
+type View =
+ | ModRemovePostView
+ | ModLockPostView
+ | ModFeaturePostView
+ | ModRemoveCommentView
+ | ModRemoveCommunityView
+ | ModBanFromCommunityView
+ | ModBanView
+ | ModAddCommunityView
+ | ModTransferCommunityView
+ | ModAddView
+ | AdminPurgePersonView
+ | AdminPurgeCommunityView
+ | AdminPurgePostView
+ | AdminPurgeCommentView;
+
+interface ModlogType {
id: number;
type_: ModlogActionType;
moderator?: PersonSafe;
- view:
- | ModRemovePostView
- | ModLockPostView
- | ModFeaturePostView
- | ModRemoveCommentView
- | ModRemoveCommunityView
- | ModBanFromCommunityView
- | ModBanView
- | ModAddCommunityView
- | ModTransferCommunityView
- | ModAddView
- | AdminPurgePersonView
- | AdminPurgeCommunityView
- | AdminPurgePostView
- | AdminPurgeCommentView;
+ view: View;
when_: string;
-};
-var Choices: any;
-if (isBrowser()) {
- Choices = require("choices.js");
}
+const getModlogQueryParams = () =>
+ getQueryParams<ModlogProps>({
+ actionType: getActionFromString,
+ modId: getIdFromString,
+ userId: getIdFromString,
+ page: getPageFromString,
+ });
+
interface ModlogState {
res?: GetModlogResponse;
- communityId?: number;
communityMods?: CommunityModeratorView[];
communityName?: string;
- page: number;
- siteRes: GetSiteResponse;
- loading: boolean;
- filter_action: ModlogActionType;
- filter_user?: number;
- filter_mod?: number;
+ loadingModlog: boolean;
+ loadingModSearch: boolean;
+ loadingUserSearch: boolean;
+ modSearchOptions: Choice[];
+ userSearchOptions: Choice[];
}
-export class Modlog extends Component<any, ModlogState> {
- private isoData = setIsoData(this.context);
- private subscription?: Subscription;
- private userChoices: any;
- private modChoices: any;
- state: ModlogState = {
- page: 1,
- loading: true,
- siteRes: this.isoData.site_res,
- filter_action: ModlogActionType.All,
- };
+interface ModlogProps {
+ page: number;
+ userId?: number | null;
+ modId?: number | null;
+ actionType: ModlogActionType;
+}
- constructor(props: any, context: any) {
- super(props, context);
- this.handlePageChange = this.handlePageChange.bind(this);
+const getActionFromString = (action?: string) =>
+ action
+ ? ModlogActionType[action] ?? ModlogActionType.All
+ : ModlogActionType.All;
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+const getModlogActionMapper =
+ (
+ actionType: ModlogActionType,
+ getAction: (view: View) => { id: number; when_: string }
+ ) =>
+ (view: View & { moderator?: PersonSafe; admin?: PersonSafe }): ModlogType => {
+ const { id, when_ } = getAction(view);
- this.state = {
- ...this.state,
- communityId: this.props.match.params.community_id
- ? Number(this.props.match.params.community_id)
- : undefined,
+ return {
+ id,
+ type_: actionType,
+ view,
+ when_,
+ moderator: view.moderator ?? view.admin,
};
+ };
- // Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
- this.state = {
- ...this.state,
- res: this.isoData.routeData[0] as GetModlogResponse,
- };
+function buildCombined({
+ removed_comments,
+ locked_posts,
+ featured_posts,
+ removed_communities,
+ removed_posts,
+ added,
+ added_to_community,
+ admin_purged_comments,
+ admin_purged_communities,
+ admin_purged_persons,
+ admin_purged_posts,
+ banned,
+ banned_from_community,
+ transferred_to_community,
+}: GetModlogResponse): ModlogType[] {
+ const combined = removed_posts
+ .map(
+ getModlogActionMapper(
+ ModlogActionType.ModRemovePost,
+ ({ mod_remove_post }: ModRemovePostView) => mod_remove_post
+ )
+ )
+ .concat(
+ locked_posts.map(
+ getModlogActionMapper(
+ ModlogActionType.ModLockPost,
+ ({ mod_lock_post }: ModLockPostView) => mod_lock_post
+ )
+ )
+ )
+ .concat(
+ featured_posts.map(
+ getModlogActionMapper(
+ ModlogActionType.ModFeaturePost,
+ ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post
+ )
+ )
+ )
+ .concat(
+ removed_comments.map(
+ getModlogActionMapper(
+ ModlogActionType.ModRemoveComment,
+ ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment
+ )
+ )
+ )
+ .concat(
+ removed_communities.map(
+ getModlogActionMapper(
+ ModlogActionType.ModRemoveCommunity,
+ ({ mod_remove_community }: ModRemoveCommunityView) =>
+ mod_remove_community
+ )
+ )
+ )
+ .concat(
+ banned_from_community.map(
+ getModlogActionMapper(
+ ModlogActionType.ModBanFromCommunity,
+ ({ mod_ban_from_community }: ModBanFromCommunityView) =>
+ mod_ban_from_community
+ )
+ )
+ )
+ .concat(
+ added_to_community.map(
+ getModlogActionMapper(
+ ModlogActionType.ModAddCommunity,
+ ({ mod_add_community }: ModAddCommunityView) => mod_add_community
+ )
+ )
+ )
+ .concat(
+ transferred_to_community.map(
+ getModlogActionMapper(
+ ModlogActionType.ModTransferCommunity,
+ ({ mod_transfer_community }: ModTransferCommunityView) =>
+ mod_transfer_community
+ )
+ )
+ )
+ .concat(
+ added.map(
+ getModlogActionMapper(
+ ModlogActionType.ModAdd,
+ ({ mod_add }: ModAddView) => mod_add
+ )
+ )
+ )
+ .concat(
+ banned.map(
+ getModlogActionMapper(
+ ModlogActionType.ModBan,
+ ({ mod_ban }: ModBanView) => mod_ban
+ )
+ )
+ )
+ .concat(
+ admin_purged_persons.map(
+ getModlogActionMapper(
+ ModlogActionType.AdminPurgePerson,
+ ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person
+ )
+ )
+ )
+ .concat(
+ admin_purged_communities.map(
+ getModlogActionMapper(
+ ModlogActionType.AdminPurgeCommunity,
+ ({ admin_purge_community }: AdminPurgeCommunityView) =>
+ admin_purge_community
+ )
+ )
+ )
+ .concat(
+ admin_purged_posts.map(
+ getModlogActionMapper(
+ ModlogActionType.AdminPurgePost,
+ ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post
+ )
+ )
+ )
+ .concat(
+ admin_purged_comments.map(
+ getModlogActionMapper(
+ ModlogActionType.AdminPurgeComment,
+ ({ admin_purge_comment }: AdminPurgeCommentView) =>
+ admin_purge_comment
+ )
+ )
+ );
- let communityRes: GetCommunityResponse | undefined =
- this.isoData.routeData[1];
+ // Sort them by time
+ combined.sort((a, b) => b.when_.localeCompare(a.when_));
- // Getting the moderators
- this.state = {
- ...this.state,
- communityMods: communityRes?.moderators,
- };
+ return combined;
+}
- this.state = { ...this.state, loading: false };
- } else {
- this.refetch();
+function renderModlogType({ type_, view }: ModlogType) {
+ switch (type_) {
+ case ModlogActionType.ModRemovePost: {
+ const mrpv = view as ModRemovePostView;
+ const {
+ mod_remove_post: { reason, removed },
+ post: { name, id },
+ } = mrpv;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Restored "}</span>
+ <span>
+ Post <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
}
- }
- componentDidMount() {
- this.setupUserFilter();
- this.setupModFilter();
- }
+ case ModlogActionType.ModLockPost: {
+ const {
+ mod_lock_post: { locked },
+ post: { id, name },
+ } = view as ModLockPostView;
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
+ return (
+ <>
+ <span>{locked ? "Locked " : "Unlocked "}</span>
+ <span>
+ Post <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ </>
+ );
}
- }
-
- buildCombined(res: GetModlogResponse): ModlogType[] {
- let removed_posts: ModlogType[] = res.removed_posts.map(r => ({
- id: r.mod_remove_post.id,
- type_: ModlogActionType.ModRemovePost,
- view: r,
- moderator: r.moderator,
- when_: r.mod_remove_post.when_,
- }));
-
- let locked_posts: ModlogType[] = res.locked_posts.map(r => ({
- id: r.mod_lock_post.id,
- type_: ModlogActionType.ModLockPost,
- view: r,
- moderator: r.moderator,
- when_: r.mod_lock_post.when_,
- }));
-
- let featured_posts: ModlogType[] = res.featured_posts.map(r => ({
- id: r.mod_feature_post.id,
- type_: ModlogActionType.ModFeaturePost,
- view: r,
- moderator: r.moderator,
- when_: r.mod_feature_post.when_,
- }));
-
- let removed_comments: ModlogType[] = res.removed_comments.map(r => ({
- id: r.mod_remove_comment.id,
- type_: ModlogActionType.ModRemoveComment,
- view: r,
- moderator: r.moderator,
- when_: r.mod_remove_comment.when_,
- }));
-
- let removed_communities: ModlogType[] = res.removed_communities.map(r => ({
- id: r.mod_remove_community.id,
- type_: ModlogActionType.ModRemoveCommunity,
- view: r,
- moderator: r.moderator,
- when_: r.mod_remove_community.when_,
- }));
-
- let banned_from_community: ModlogType[] = res.banned_from_community.map(
- r => ({
- id: r.mod_ban_from_community.id,
- type_: ModlogActionType.ModBanFromCommunity,
- view: r,
- moderator: r.moderator,
- when_: r.mod_ban_from_community.when_,
- })
- );
- let added_to_community: ModlogType[] = res.added_to_community.map(r => ({
- id: r.mod_add_community.id,
- type_: ModlogActionType.ModAddCommunity,
- view: r,
- moderator: r.moderator,
- when_: r.mod_add_community.when_,
- }));
-
- let transferred_to_community: ModlogType[] =
- res.transferred_to_community.map(r => ({
- id: r.mod_transfer_community.id,
- type_: ModlogActionType.ModTransferCommunity,
- view: r,
- moderator: r.moderator,
- when_: r.mod_transfer_community.when_,
- }));
-
- let added: ModlogType[] = res.added.map(r => ({
- id: r.mod_add.id,
- type_: ModlogActionType.ModAdd,
- view: r,
- moderator: r.moderator,
- when_: r.mod_add.when_,
- }));
-
- let banned: ModlogType[] = res.banned.map(r => ({
- id: r.mod_ban.id,
- type_: ModlogActionType.ModBan,
- view: r,
- moderator: r.moderator,
- when_: r.mod_ban.when_,
- }));
-
- let purged_persons: ModlogType[] = res.admin_purged_persons.map(r => ({
- id: r.admin_purge_person.id,
- type_: ModlogActionType.AdminPurgePerson,
- view: r,
- moderator: r.admin,
- when_: r.admin_purge_person.when_,
- }));
-
- let purged_communities: ModlogType[] = res.admin_purged_communities.map(
- r => ({
- id: r.admin_purge_community.id,
- type_: ModlogActionType.AdminPurgeCommunity,
- view: r,
- moderator: r.admin,
- when_: r.admin_purge_community.when_,
- })
- );
+ case ModlogActionType.ModFeaturePost: {
+ const {
+ mod_feature_post: { featured, is_featured_community },
+ post: { id, name },
+ } = view as ModFeaturePostView;
- let purged_posts: ModlogType[] = res.admin_purged_posts.map(r => ({
- id: r.admin_purge_post.id,
- type_: ModlogActionType.AdminPurgePost,
- view: r,
- moderator: r.admin,
- when_: r.admin_purge_post.when_,
- }));
-
- let purged_comments: ModlogType[] = res.admin_purged_comments.map(r => ({
- id: r.admin_purge_comment.id,
- type_: ModlogActionType.AdminPurgeComment,
- view: r,
- moderator: r.admin,
- when_: r.admin_purge_comment.when_,
- }));
-
- let combined: ModlogType[] = [];
-
- combined.push(...removed_posts);
- combined.push(...locked_posts);
- combined.push(...featured_posts);
- combined.push(...removed_comments);
- combined.push(...removed_communities);
- combined.push(...banned_from_community);
- combined.push(...added_to_community);
- combined.push(...transferred_to_community);
- combined.push(...added);
- combined.push(...banned);
- combined.push(...purged_persons);
- combined.push(...purged_communities);
- combined.push(...purged_posts);
- combined.push(...purged_comments);
-
- // Sort them by time
- combined.sort((a, b) => b.when_.localeCompare(a.when_));
-
- return combined;
- }
+ return (
+ <>
+ <span>{featured ? "Featured " : "Unfeatured "}</span>
+ <span>
+ Post <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ <span>{is_featured_community ? " In Community" : " In Local"}</span>
+ </>
+ );
+ }
+ case ModlogActionType.ModRemoveComment: {
+ const mrc = view as ModRemoveCommentView;
+ const {
+ mod_remove_comment: { reason, removed },
+ comment: { id, content },
+ commenter,
+ } = mrc;
- renderModlogType(i: ModlogType) {
- switch (i.type_) {
- case ModlogActionType.ModRemovePost: {
- let mrpv = i.view as ModRemovePostView;
- let reason = mrpv.mod_remove_post.reason;
- return (
- <>
- <span>
- {mrpv.mod_remove_post.removed ? "Removed " : "Restored "}
- </span>
- <span>
- Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link>
- </span>
- {reason && (
- <span>
- <div>reason: {reason}</div>
- </span>
- )}
- </>
- );
- }
- case ModlogActionType.ModLockPost: {
- let mlpv = i.view as ModLockPostView;
- return (
- <>
- <span>{mlpv.mod_lock_post.locked ? "Locked " : "Unlocked "}</span>
- <span>
- Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link>
- </span>
- </>
- );
- }
- case ModlogActionType.ModFeaturePost: {
- let mspv = i.view as ModFeaturePostView;
- return (
- <>
- <span>
- {mspv.mod_feature_post.featured ? "Featured " : "Unfeatured "}
- </span>
- <span>
- Post <Link to={`/post/${mspv.post.id}`}>{mspv.post.name}</Link>
- </span>
- <span>
- {mspv.mod_feature_post.is_featured_community
- ? " In Community"
- : " In Local"}
- </span>
- </>
- );
- }
- case ModlogActionType.ModRemoveComment: {
- let mrc = i.view as ModRemoveCommentView;
- let reason = mrc.mod_remove_comment.reason;
- return (
- <>
+ return (
+ <>
+ <span>{removed ? "Removed " : "Restored "}</span>
+ <span>
+ Comment <Link to={`/comment/${id}`}>{content}</Link>
+ </span>
+ <span>
+ {" "}
+ by <PersonListing person={commenter} />
+ </span>
+ {reason && (
<span>
- {mrc.mod_remove_comment.removed ? "Removed " : "Restored "}
- </span>
- <span>
- Comment{" "}
- <Link to={`/post/${mrc.post.id}/comment/${mrc.comment.id}`}>
- {mrc.comment.content}
- </Link>
- </span>
- <span>
- {" "}
- by <PersonListing person={mrc.commenter} />
- </span>
- {reason && (
- <span>
- <div>reason: {reason}</div>
- </span>
- )}
- </>
- );
- }
- case ModlogActionType.ModRemoveCommunity: {
- let mrco = i.view as ModRemoveCommunityView;
- let reason = mrco.mod_remove_community.reason;
- let expires = mrco.mod_remove_community.expires;
- return (
- <>
- <span>
- {mrco.mod_remove_community.removed ? "Removed " : "Restored "}
- </span>
- <span>
- Community <CommunityLink community={mrco.community} />
- </span>
- {reason && (
- <span>
- <div>reason: {reason}</div>
- </span>
- )}
- {expires && (
- <span>
- <div>expires: {moment.utc(expires).fromNow()}</div>
- </span>
- )}
- </>
- );
- }
- case ModlogActionType.ModBanFromCommunity: {
- let mbfc = i.view as ModBanFromCommunityView;
- let reason = mbfc.mod_ban_from_community.reason;
- let expires = mbfc.mod_ban_from_community.expires;
- return (
- <>
- <span>
- {mbfc.mod_ban_from_community.banned ? "Banned " : "Unbanned "}{" "}
+ <div>reason: {reason}</div>
</span>
+ )}
+ </>
+ );
+ }
+
+ case ModlogActionType.ModRemoveCommunity: {
+ const mrco = view as ModRemoveCommunityView;
+ const {
+ mod_remove_community: { reason, expires, removed },
+ community,
+ } = mrco;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Restored "}</span>
+ <span>
+ Community <CommunityLink community={community} />
+ </span>
+ {reason && (
<span>
- <PersonListing person={mbfc.banned_person} />
+ <div>reason: {reason}</div>
</span>
- <span> from the community </span>
+ )}
+ {expires && (
<span>
- <CommunityLink community={mbfc.community} />
+ <div>expires: {moment.utc(expires).fromNow()}</div>
</span>
- {reason && (
- <span>
- <div>reason: {reason}</div>
- </span>
- )}
- {expires && (
- <span>
- <div>expires: {moment.utc(expires).fromNow()}</div>
- </span>
- )}
- </>
- );
- }
- case ModlogActionType.ModAddCommunity: {
- let mac = i.view as ModAddCommunityView;
- return (
- <>
+ )}
+ </>
+ );
+ }
+
+ case ModlogActionType.ModBanFromCommunity: {
+ const mbfc = view as ModBanFromCommunityView;
+ const {
+ mod_ban_from_community: { reason, expires, banned },
+ banned_person,
+ community,
+ } = mbfc;
+
+ return (
+ <>
+ <span>{banned ? "Banned " : "Unbanned "}</span>
+ <span>
+ <PersonListing person={banned_person} />
+ </span>
+ <span> from the community </span>
+ <span>
+ <CommunityLink community={community} />
+ </span>
+ {reason && (
<span>
- {mac.mod_add_community.removed ? "Removed " : "Appointed "}{" "}
+ <div>reason: {reason}</div>
</span>
+ )}
+ {expires && (
<span>
- <PersonListing person={mac.modded_person} />
+ <div>expires: {moment.utc(expires).fromNow()}</div>
</span>
- <span> as a mod to the community </span>
+ )}
+ </>
+ );
+ }
+
+ case ModlogActionType.ModAddCommunity: {
+ const {
+ mod_add_community: { removed },
+ modded_person,
+ community,
+ } = view as ModAddCommunityView;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Appointed "}</span>
+ <span>
+ <PersonListing person={modded_person} />
+ </span>
+ <span> as a mod to the community </span>
+ <span>
+ <CommunityLink community={community} />
+ </span>
+ </>
+ );
+ }
+
+ case ModlogActionType.ModTransferCommunity: {
+ const {
+ mod_transfer_community: { removed },
+ community,
+ modded_person,
+ } = view as ModTransferCommunityView;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Transferred "}</span>
+ <span>
+ <CommunityLink community={community} />
+ </span>
+ <span> to </span>
+ <span>
+ <PersonListing person={modded_person} />
+ </span>
+ </>
+ );
+ }
+
+ case ModlogActionType.ModBan: {
+ const {
+ mod_ban: { reason, expires, banned },
+ banned_person,
+ } = view as ModBanView;
+
+ return (
+ <>
+ <span>{banned ? "Banned " : "Unbanned "}</span>
+ <span>
+ <PersonListing person={banned_person} />
+ </span>
+ {reason && (
<span>
- <CommunityLink community={mac.community} />
+ <div>reason: {reason}</div>
</span>
- </>
- );
- }
- case ModlogActionType.ModTransferCommunity: {
- let mtc = i.view as ModTransferCommunityView;
- return (
- <>
+ )}
+ {expires && (
<span>
- {mtc.mod_transfer_community.removed ? "Removed " : "Transferred "}{" "}
+ <div>expires: {moment.utc(expires).fromNow()}</div>
</span>
+ )}
+ </>
+ );
+ }
+
+ case ModlogActionType.ModAdd: {
+ const {
+ mod_add: { removed },
+ modded_person,
+ } = view as ModAddView;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Appointed "}</span>
+ <span>
+ <PersonListing person={modded_person} />
+ </span>
+ <span> as an admin </span>
+ </>
+ );
+ }
+ case ModlogActionType.AdminPurgePerson: {
+ const {
+ admin_purge_person: { reason },
+ } = view as AdminPurgePersonView;
+
+ return (
+ <>
+ <span>Purged a Person</span>
+ {reason && (
<span>
- <CommunityLink community={mtc.community} />
+ <div>reason: {reason}</div>
</span>
- <span> to </span>
+ )}
+ </>
+ );
+ }
+
+ case ModlogActionType.AdminPurgeCommunity: {
+ const {
+ admin_purge_community: { reason },
+ } = view as AdminPurgeCommunityView;
+
+ return (
+ <>
+ <span>Purged a Community</span>
+ {reason && (
<span>
- <PersonListing person={mtc.modded_person} />
+ <div>reason: {reason}</div>
</span>
- </>
- );
- }
- case ModlogActionType.ModBan: {
- let mb = i.view as ModBanView;
- let reason = mb.mod_ban.reason;
- let expires = mb.mod_ban.expires;
- return (
- <>
- <span>{mb.mod_ban.banned ? "Banned " : "Unbanned "} </span>
+ )}
+ </>
+ );
+ }
+
+ case ModlogActionType.AdminPurgePost: {
+ const {
+ admin_purge_post: { reason },
+ community,
+ } = view as AdminPurgePostView;
+
+ return (
+ <>
+ <span>Purged a Post from from </span>
+ <CommunityLink community={community} />
+ {reason && (
<span>
- <PersonListing person={mb.banned_person} />
+ <div>reason: {reason}</div>
</span>
- {reason && (
- <span>
- <div>reason: {reason}</div>
- </span>
- )}
- {expires && (
- <span>
- <div>expires: {moment.utc(expires).fromNow()}</div>
- </span>
- )}
- </>
- );
- }
- case ModlogActionType.ModAdd: {
- let ma = i.view as ModAddView;
- return (
- <>
- <span>{ma.mod_add.removed ? "Removed " : "Appointed "} </span>
+ )}
+ </>
+ );
+ }
+
+ case ModlogActionType.AdminPurgeComment: {
+ const {
+ admin_purge_comment: { reason },
+ post: { id, name },
+ } = view as AdminPurgeCommentView;
+
+ return (
+ <>
+ <span>
+ Purged a Comment from <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ {reason && (
<span>
- <PersonListing person={ma.modded_person} />
+ <div>reason: {reason}</div>
</span>
- <span> as an admin </span>
- </>
- );
- }
- case ModlogActionType.AdminPurgePerson: {
- let ap = i.view as AdminPurgePersonView;
- let reason = ap.admin_purge_person.reason;
- return (
- <>
- <span>Purged a Person</span>
- {reason && (
- <span>
- <div>reason: {reason}</div>
- </span>
- )}
- </>
- );
- }
- case ModlogActionType.AdminPurgeCommunity: {
- let ap = i.view as AdminPurgeCommunityView;
- let reason = ap.admin_purge_community.reason;
- return (
- <>
- <span>Purged a Community</span>
- {reason && (
- <span>
- <div>reason: {reason}</div>
- </span>
- )}
- </>
- );
- }
- case ModlogActionType.AdminPurgePost: {
- let ap = i.view as AdminPurgePostView;
- let reason = ap.admin_purge_post.reason;
- return (
- <>
- <span>Purged a Post from from </span>
- <CommunityLink community={ap.community} />
- {reason && (
- <span>
- <div>reason: {reason}</div>
- </span>
- )}
- </>
- );
+ )}
+ </>
+ );
+ }
+
+ default:
+ return <></>;
+ }
+}
+
+const Filter = ({
+ filterType,
+ onChange,
+ value,
+ onSearch,
+ options,
+ loading,
+}: {
+ filterType: FilterType;
+ onChange: (option: Choice) => void;
+ value?: number | null;
+ onSearch: (text: string) => void;
+ options: Choice[];
+ loading: boolean;
+}) => (
+ <div className="col-sm-6 form-group">
+ <label className="col-form-label" htmlFor={`filter-${filterType}`}>
+ {i18n.t(`filter_by_${filterType}` as NoOptionI18nKeys)}
+ </label>
+ <SearchableSelect
+ id={`filter-${filterType}`}
+ value={value ?? 0}
+ options={[
+ {
+ label: i18n.t("all"),
+ value: "0",
+ },
+ ].concat(options)}
+ onChange={onChange}
+ onSearch={onSearch}
+ loading={loading}
+ />
+ </div>
+);
+
+async function createNewOptions({
+ id,
+ oldOptions,
+ text,
+}: {
+ id?: number | null;
+ oldOptions: Choice[];
+ text: string;
+}) {
+ const newOptions: Choice[] = [];
+
+ if (id) {
+ const selectedUser = oldOptions.find(
+ ({ value }) => value === id.toString()
+ );
+
+ if (selectedUser) {
+ newOptions.push(selectedUser);
+ }
+ }
+
+ if (text.length > 0) {
+ newOptions.push(
+ ...(await fetchUsers(text)).users
+ .slice(0, fetchLimit)
+ .map<Choice>(personToChoice)
+ );
+ }
+
+ return newOptions;
+}
+
+export class Modlog extends Component<
+ RouteComponentProps<{ communityId?: string }>,
+ ModlogState
+> {
+ private isoData = setIsoData(this.context);
+ private subscription?: Subscription;
+
+ state: ModlogState = {
+ loadingModlog: true,
+ loadingModSearch: false,
+ loadingUserSearch: false,
+ userSearchOptions: [],
+ modSearchOptions: [],
+ };
+
+ constructor(
+ props: RouteComponentProps<{ communityId?: string }>,
+ context: any
+ ) {
+ super(props, context);
+ this.handlePageChange = this.handlePageChange.bind(this);
+ this.handleUserChange = this.handleUserChange.bind(this);
+ this.handleModChange = this.handleModChange.bind(this);
+
+ this.parseMessage = this.parseMessage.bind(this);
+ this.subscription = wsSubscribe(this.parseMessage);
+
+ // Only fetch the data if coming from another route
+ if (this.isoData.path === this.context.router.route.match.url) {
+ this.state = {
+ ...this.state,
+ res: this.isoData.routeData[0] as GetModlogResponse,
+ };
+
+ const communityRes: GetCommunityResponse | undefined =
+ this.isoData.routeData[1];
+
+ // Getting the moderators
+ this.state = {
+ ...this.state,
+ communityMods: communityRes?.moderators,
+ };
+
+ const filteredModRes: GetPersonDetailsResponse | undefined =
+ this.isoData.routeData[2];
+ if (filteredModRes) {
+ this.state = {
+ ...this.state,
+ modSearchOptions: [personToChoice(filteredModRes.person_view)],
+ };
}
- case ModlogActionType.AdminPurgeComment: {
- let ap = i.view as AdminPurgeCommentView;
- let reason = ap.admin_purge_comment.reason;
- return (
- <>
- <span>
- Purged a Comment from{" "}
- <Link to={`/post/${ap.post.id}`}>{ap.post.name}</Link>
- </span>
- {reason && (
- <span>
- <div>reason: {reason}</div>
- </span>
- )}
- </>
- );
+
+ const filteredUserRes: GetPersonDetailsResponse | undefined =
+ this.isoData.routeData[3];
+ if (filteredUserRes) {
+ this.state = {
+ ...this.state,
+ userSearchOptions: [personToChoice(filteredUserRes.person_view)],
+ };
}
- default:
- return <div />;
+
+ this.state = { ...this.state, loadingModlog: false };
+ } else {
+ this.refetch();
+ }
+ }
+
+ componentWillUnmount() {
+ if (isBrowser()) {
+ this.subscription?.unsubscribe();
}
}
- combined() {
- let res = this.state.res;
- let combined = res ? this.buildCombined(res) : [];
+ get combined() {
+ const res = this.state.res;
+ const combined = res ? buildCombined(res) : [];
return (
<tbody>
<div>{this.modOrAdminText(i.moderator)}</div>
)}
</td>
- <td>{this.renderModlogType(i)}</td>
+ <td>{renderModlogType(i)}</td>
</tr>
))}
</tbody>
}
modOrAdminText(person?: PersonSafe): string {
- return person
- ? this.isoData.site_res.admins.map(a => a.person.id).includes(person.id)
- ? i18n.t("admin")
- : i18n.t("mod")
+ return person &&
+ this.isoData.site_res.admins.some(
+ ({ person: { id } }) => id === person.id
+ )
+ ? i18n.t("admin")
: i18n.t("mod");
}
get documentTitle(): string {
- return `Modlog - ${this.state.siteRes.site_view.site.name}`;
+ return `Modlog - ${this.isoData.site_res.site_view.site.name}`;
}
render() {
- let communityName = this.state.communityName;
+ const {
+ communityName,
+ loadingModlog,
+ loadingModSearch,
+ loadingUserSearch,
+ userSearchOptions,
+ modSearchOptions,
+ } = this.state;
+ const { actionType, page, modId, userId } = getModlogQueryParams();
+
return (
<div className="container-lg">
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
- {this.state.loading ? (
+
+ <div>
<h5>
- <Spinner large />
+ {communityName && (
+ <Link className="text-body" to={`/c/${communityName}`}>
+ /c/{communityName}{" "}
+ </Link>
+ )}
+ <span>{i18n.t("modlog")}</span>
</h5>
- ) : (
- <div>
- <h5>
- {communityName && (
- <Link className="text-body" to={`/c/${communityName}`}>
- /c/{communityName}{" "}
- </Link>
- )}
- <span>{i18n.t("modlog")}</span>
- </h5>
- <div className="form-row">
- <div className="form-group col-sm-6">
- <select
- value={this.state.filter_action}
- onChange={linkEvent(this, this.handleFilterActionChange)}
- className="custom-select mb-2"
- aria-label="action"
- >
- <option disabled aria-hidden="true">
- {i18n.t("filter_by_action")}
- </option>
- <option value={ModlogActionType.All}>{i18n.t("all")}</option>
- <option value={ModlogActionType.ModRemovePost}>
- Removing Posts
- </option>
- <option value={ModlogActionType.ModLockPost}>
- Locking Posts
- </option>
- <option value={ModlogActionType.ModFeaturePost}>
- Featuring Posts
- </option>
- <option value={ModlogActionType.ModRemoveComment}>
- Removing Comments
- </option>
- <option value={ModlogActionType.ModRemoveCommunity}>
- Removing Communities
- </option>
- <option value={ModlogActionType.ModBanFromCommunity}>
- Banning From Communities
- </option>
- <option value={ModlogActionType.ModAddCommunity}>
- Adding Mod to Community
- </option>
- <option value={ModlogActionType.ModTransferCommunity}>
- Transfering Communities
- </option>
- <option value={ModlogActionType.ModAdd}>
- Adding Mod to Site
- </option>
- <option value={ModlogActionType.ModBan}>
- Banning From Site
- </option>
- </select>
- </div>
- {!this.state.siteRes.site_view.local_site
- .hide_modlog_mod_names && (
- <div className="form-group col-sm-6">
- <select
- id="filter-mod"
- className="form-control"
- value={this.state.filter_mod}
- >
- <option>{i18n.t("filter_by_mod")}</option>
- </select>
- </div>
- )}
- <div className="form-group col-sm-6">
- <select
- id="filter-user"
- className="form-control"
- value={this.state.filter_user}
- >
- <option>{i18n.t("filter_by_user")}</option>
- </select>
- </div>
- </div>
- <div className="table-responsive">
+ <div className="form-row">
+ <select
+ value={actionType}
+ onChange={linkEvent(this, this.handleFilterActionChange)}
+ className="custom-select col-sm-6"
+ aria-label="action"
+ >
+ <option disabled aria-hidden="true">
+ {i18n.t("filter_by_action")}
+ </option>
+ <option value={ModlogActionType.All}>{i18n.t("all")}</option>
+ <option value={ModlogActionType.ModRemovePost}>
+ Removing Posts
+ </option>
+ <option value={ModlogActionType.ModLockPost}>
+ Locking Posts
+ </option>
+ <option value={ModlogActionType.ModFeaturePost}>
+ Featuring Posts
+ </option>
+ <option value={ModlogActionType.ModRemoveComment}>
+ Removing Comments
+ </option>
+ <option value={ModlogActionType.ModRemoveCommunity}>
+ Removing Communities
+ </option>
+ <option value={ModlogActionType.ModBanFromCommunity}>
+ Banning From Communities
+ </option>
+ <option value={ModlogActionType.ModAddCommunity}>
+ Adding Mod to Community
+ </option>
+ <option value={ModlogActionType.ModTransferCommunity}>
+ Transferring Communities
+ </option>
+ <option value={ModlogActionType.ModAdd}>
+ Adding Mod to Site
+ </option>
+ <option value={ModlogActionType.ModBan}>Banning From Site</option>
+ </select>
+ </div>
+ <div className="form-row mb-2">
+ <Filter
+ filterType="user"
+ onChange={this.handleUserChange}
+ onSearch={this.handleSearchUsers}
+ value={userId}
+ options={userSearchOptions}
+ loading={loadingUserSearch}
+ />
+ {!this.isoData.site_res.site_view.local_site
+ .hide_modlog_mod_names && (
+ <Filter
+ filterType="mod"
+ onChange={this.handleModChange}
+ onSearch={this.handleSearchMods}
+ value={modId}
+ options={modSearchOptions}
+ loading={loadingModSearch}
+ />
+ )}
+ </div>
+ <div className="table-responsive">
+ {loadingModlog ? (
+ <h5>
+ <Spinner large />
+ </h5>
+ ) : (
<table id="modlog_table" className="table table-sm table-hover">
<thead className="pointer">
<tr>
<th>{i18n.t("action")}</th>
</tr>
</thead>
- {this.combined()}
+ {this.combined}
</table>
- <Paginator
- page={this.state.page}
- onChange={this.handlePageChange}
- />
- </div>
+ )}
+ <Paginator page={page} onChange={this.handlePageChange} />
</div>
- )}
+ </div>
</div>
);
}
handleFilterActionChange(i: Modlog, event: any) {
- i.setState({ filter_action: event.target.value });
- i.refetch();
+ i.updateUrl({
+ actionType: ModlogActionType[event.target.value],
+ page: 1,
+ });
+ }
+
+ handlePageChange(page: number) {
+ this.updateUrl({ page });
+ }
+
+ handleUserChange(option: Choice) {
+ this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 });
+ }
+
+ handleModChange(option: Choice) {
+ this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 });
}
- handlePageChange(val: number) {
- this.setState({ page: val });
+ handleSearchUsers = debounce(async (text: string) => {
+ const { userId } = getModlogQueryParams();
+ const { userSearchOptions } = this.state;
+ this.setState({ loadingUserSearch: true });
+
+ const newOptions = await createNewOptions({
+ id: userId,
+ text,
+ oldOptions: userSearchOptions,
+ });
+
+ this.setState({
+ userSearchOptions: newOptions,
+ loadingUserSearch: false,
+ });
+ });
+
+ handleSearchMods = debounce(async (text: string) => {
+ const { modId } = getModlogQueryParams();
+ const { modSearchOptions } = this.state;
+ this.setState({ loadingModSearch: true });
+
+ const newOptions = await createNewOptions({
+ id: modId,
+ text,
+ oldOptions: modSearchOptions,
+ });
+
+ this.setState({
+ modSearchOptions: newOptions,
+ loadingModSearch: false,
+ });
+ });
+
+ updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
+ const {
+ page: urlPage,
+ actionType: urlActionType,
+ modId: urlModId,
+ userId: urlUserId,
+ } = getModlogQueryParams();
+
+ const queryParams: QueryParams<ModlogProps> = {
+ page: (page ?? urlPage).toString(),
+ actionType: actionType ?? urlActionType,
+ modId: getUpdatedSearchId(modId, urlModId),
+ userId: getUpdatedSearchId(userId, urlUserId),
+ };
+
+ const communityId = this.props.match.params.communityId;
+
+ this.props.history.push(
+ `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
+ queryParams
+ )}`
+ );
+
+ this.setState({
+ loadingModlog: true,
+ res: undefined,
+ });
+
this.refetch();
}
refetch() {
- let auth = myAuth(false);
- let modlogForm: GetModlog = {
- community_id: this.state.communityId,
- page: this.state.page,
+ const auth = myAuth(false);
+ const { actionType, page, modId, userId } = getModlogQueryParams();
+ const { communityId: urlCommunityId } = this.props.match.params;
+ const communityId = getIdFromString(urlCommunityId);
+
+ const modlogForm: GetModlog = {
+ community_id: communityId,
+ page,
limit: fetchLimit,
- type_: this.state.filter_action,
- other_person_id: this.state.filter_user,
- mod_person_id: this.state.filter_mod,
+ type_: actionType,
+ other_person_id: userId ?? undefined,
+ mod_person_id: !this.isoData.site_res.site_view.local_site
+ .hide_modlog_mod_names
+ ? modId ?? undefined
+ : undefined,
auth,
};
+
WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
- let communityId = this.state.communityId;
if (communityId) {
- let communityForm: GetCommunity = {
+ const communityForm: GetCommunity = {
id: communityId,
auth,
};
- WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
- }
- }
- setupUserFilter() {
- if (isBrowser()) {
- let selectId: any = document.getElementById("filter-user");
- if (selectId) {
- this.userChoices = new Choices(selectId, choicesConfig);
- this.userChoices.passedElement.element.addEventListener(
- "choice",
- (e: any) => {
- this.setState({ filter_user: Number(e.detail.choice.value) });
- this.refetch();
- },
- false
- );
- this.userChoices.passedElement.element.addEventListener(
- "search",
- debounce(async (e: any) => {
- try {
- let users = (await fetchUsers(e.detail.value)).users;
- this.userChoices.setChoices(
- users.map(u => {
- return {
- value: u.person.id.toString(),
- label: u.person.name,
- };
- }),
- "value",
- "label",
- true
- );
- } catch (err) {
- console.log(err);
- }
- }),
- false
- );
- }
- }
- }
-
- setupModFilter() {
- if (isBrowser()) {
- let selectId: any = document.getElementById("filter-mod");
- if (selectId) {
- this.modChoices = new Choices(selectId, choicesConfig);
- this.modChoices.passedElement.element.addEventListener(
- "choice",
- (e: any) => {
- this.setState({ filter_mod: Number(e.detail.choice.value) });
- this.refetch();
- },
- false
- );
- this.modChoices.passedElement.element.addEventListener(
- "search",
- debounce(async (e: any) => {
- try {
- let mods = (await fetchUsers(e.detail.value)).users;
- this.modChoices.setChoices(
- mods.map(u => {
- return {
- value: u.person.id.toString(),
- label: u.person.name,
- };
- }),
- "value",
- "label",
- true
- );
- } catch (err) {
- console.log(err);
- }
- }),
- false
- );
- }
+ WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
}
}
- static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
- let pathSplit = req.path.split("/");
- let communityId = pathSplit[3] ? Number(pathSplit[3]) : undefined;
- let auth = req.auth;
- let promises: Promise<any>[] = [];
+ static fetchInitialData({
+ client,
+ path,
+ query: { modId: urlModId, page, userId: urlUserId, actionType },
+ auth,
+ site,
+ }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<any>[] {
+ const pathSplit = path.split("/");
+ const promises: Promise<any>[] = [];
+ const communityId = getIdFromString(pathSplit[2]);
+ const modId = !site.site_view.local_site.hide_modlog_mod_names
+ ? getIdFromString(urlModId)
+ : undefined;
+ const userId = getIdFromString(urlUserId);
- let modlogForm: GetModlog = {
- page: 1,
+ const modlogForm: GetModlog = {
+ page: getPageFromString(page),
limit: fetchLimit,
community_id: communityId,
- type_: ModlogActionType.All,
+ type_: getActionFromString(actionType),
+ mod_person_id: modId,
+ other_person_id: userId,
auth,
};
- promises.push(req.client.getModlog(modlogForm));
+ promises.push(client.getModlog(modlogForm));
if (communityId) {
- let communityForm: GetCommunity = {
+ const communityForm: GetCommunity = {
id: communityId,
- auth: req.auth,
+ auth,
};
- promises.push(req.client.getCommunity(communityForm));
+ promises.push(client.getCommunity(communityForm));
} else {
promises.push(Promise.resolve());
}
+
+ if (modId) {
+ const getPersonForm: GetPersonDetails = {
+ person_id: modId,
+ auth,
+ };
+
+ promises.push(client.getPersonDetails(getPersonForm));
+ } else {
+ promises.push(Promise.resolve());
+ }
+
+ if (userId) {
+ const getPersonForm: GetPersonDetails = {
+ person_id: userId,
+ auth,
+ };
+
+ promises.push(client.getPersonDetails(getPersonForm));
+ } else {
+ promises.push(Promise.resolve());
+ }
+
return promises;
}
parseMessage(msg: any) {
- let op = wsUserOp(msg);
+ const op = wsUserOp(msg);
console.log(msg);
+
if (msg.error) {
toast(i18n.t(msg.error), "danger");
- return;
- } else if (op == UserOperation.GetModlog) {
- let data = wsJsonToRes<GetModlogResponse>(msg);
- window.scrollTo(0, 0);
- this.setState({ res: data, loading: false });
- this.setupUserFilter();
- this.setupModFilter();
- } else if (op == UserOperation.GetCommunity) {
- let data = wsJsonToRes<GetCommunityResponse>(msg);
- this.setState({
- communityMods: data.moderators,
- communityName: data.community_view.community.name,
- });
+ } else {
+ switch (op) {
+ case UserOperation.GetModlog: {
+ const res = wsJsonToRes<GetModlogResponse>(msg);
+ window.scrollTo(0, 0);
+ this.setState({ res, loadingModlog: false });
+
+ break;
+ }
+
+ case UserOperation.GetCommunity: {
+ const {
+ moderators,
+ community_view: {
+ community: { name },
+ },
+ } = wsJsonToRes<GetCommunityResponse>(msg);
+ this.setState({
+ communityMods: moderators,
+ communityName: name,
+ });
+
+ break;
+ }
+ }
}
}
}
+import classNames from "classnames";
+import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router";
+import { RouteComponentProps } from "inferno-router/dist/Route";
import {
AddAdminResponse,
BanPerson,
BlockPerson,
BlockPersonResponse,
CommentResponse,
+ CommunityModeratorView,
+ CommunitySafe,
GetPersonDetails,
GetPersonDetailsResponse,
GetSiteResponse,
enableNsfw,
fetchLimit,
futureDaysToUnixTime,
- getUsernameFromProps,
+ getPageFromString,
+ getQueryParams,
+ getQueryString,
isAdmin,
isBanned,
mdToHtml,
myAuth,
numToSI,
+ QueryParams,
relTags,
restoreScrollPosition,
routeSortTypeToEnum,
interface ProfileState {
personRes?: GetPersonDetailsResponse;
- userName: string;
- view: PersonDetailsView;
- sort: SortType;
- page: number;
loading: boolean;
personBlocked: boolean;
banReason?: string;
view: PersonDetailsView;
sort: SortType;
page: number;
- person_id?: number;
- username: string;
}
-interface UrlParams {
- view?: string;
- sort?: SortType;
- page?: number;
+const getProfileQueryParams = () =>
+ getQueryParams<ProfileProps>({
+ view: getViewFromProps,
+ page: getPageFromString,
+ sort: getSortTypeFromQuery,
+ });
+
+const getSortTypeFromQuery = (sort?: string): SortType =>
+ sort ? routeSortTypeToEnum(sort, SortType.New) : SortType.New;
+
+const getViewFromProps = (view?: string): PersonDetailsView =>
+ view
+ ? PersonDetailsView[view] ?? PersonDetailsView.Overview
+ : PersonDetailsView.Overview;
+
+function toggleBlockPerson(recipientId: number, block: boolean) {
+ const auth = myAuth();
+
+ if (auth) {
+ const blockUserForm: BlockPerson = {
+ person_id: recipientId,
+ block,
+ auth,
+ };
+
+ WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
+ }
}
-export class Profile extends Component<any, ProfileState> {
+const handleUnblockPerson = (personId: number) =>
+ toggleBlockPerson(personId, false);
+
+const handleBlockPerson = (personId: number) =>
+ toggleBlockPerson(personId, true);
+
+const getCommunitiesListing = (
+ translationKey: NoOptionI18nKeys,
+ communityViews?: { community: CommunitySafe }[]
+) =>
+ communityViews &&
+ communityViews.length > 0 && (
+ <div className="card border-secondary mb-3">
+ <div className="card-body">
+ <h5>{i18n.t(translationKey)}</h5>
+ <ul className="list-unstyled mb-0">
+ {communityViews.map(({ community }) => (
+ <li key={community.id}>
+ <CommunityLink community={community} />
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+ );
+
+const Moderates = ({ moderates }: { moderates?: CommunityModeratorView[] }) =>
+ getCommunitiesListing("moderates", moderates);
+
+const Follows = () =>
+ getCommunitiesListing("subscribed", UserService.Instance.myUserInfo?.follows);
+
+export class Profile extends Component<
+ RouteComponentProps<{ username: string }>,
+ ProfileState
+> {
private isoData = setIsoData(this.context);
private subscription?: Subscription;
state: ProfileState = {
- userName: getUsernameFromProps(this.props),
loading: true,
- view: Profile.getViewFromProps(this.props.match.view),
- sort: Profile.getSortTypeFromProps(this.props.match.sort),
- page: Profile.getPageFromProps(this.props.match.page),
personBlocked: false,
siteRes: this.isoData.site_res,
showBanDialog: false,
removeData: false,
};
- constructor(props: any, context: any) {
+ constructor(props: RouteComponentProps<{ username: string }>, context: any) {
super(props, context);
this.handleSortChange = this.handleSortChange.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
// Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
+ if (this.isoData.path === this.context.router.route.match.url) {
this.state = {
...this.state,
personRes: this.isoData.routeData[0] as GetPersonDetailsResponse,
}
fetchUserData() {
- let form: GetPersonDetails = {
- username: this.state.userName,
- sort: this.state.sort,
- saved_only: this.state.view === PersonDetailsView.Saved,
- page: this.state.page,
+ const { page, sort, view } = getProfileQueryParams();
+
+ const form: GetPersonDetails = {
+ username: this.props.match.params.username,
+ sort,
+ saved_only: view === PersonDetailsView.Saved,
+ page,
limit: fetchLimit,
auth: myAuth(false),
};
+
WebSocketService.Instance.send(wsClient.getPersonDetails(form));
}
get amCurrentUser() {
return (
- UserService.Instance.myUserInfo?.local_user_view.person.id ==
+ UserService.Instance.myUserInfo?.local_user_view.person.id ===
this.state.personRes?.person_view.person.id
);
}
setPersonBlock() {
- let mui = UserService.Instance.myUserInfo;
- let res = this.state.personRes;
+ const mui = UserService.Instance.myUserInfo;
+ const res = this.state.personRes;
+
if (mui && res) {
this.setState({
- personBlocked: mui.person_blocks
- .map(a => a.target.id)
- .includes(res.person_view.person.id),
+ personBlocked: mui.person_blocks.some(
+ ({ target: { id } }) => id === res.person_view.person.id
+ ),
});
}
}
- static getViewFromProps(view: string): PersonDetailsView {
- return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
- }
-
- static getSortTypeFromProps(sort: string): SortType {
- return sort ? routeSortTypeToEnum(sort) : SortType.New;
- }
-
- static getPageFromProps(page: number): number {
- return page ? Number(page) : 1;
- }
-
- static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
- let pathSplit = req.path.split("/");
+ static fetchInitialData({
+ client,
+ path,
+ query: { page, sort, view: urlView },
+ auth,
+ }: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<any>[] {
+ const pathSplit = path.split("/");
- let username = pathSplit[2];
- let view = this.getViewFromProps(pathSplit[4]);
- let sort = this.getSortTypeFromProps(pathSplit[6]);
- let page = this.getPageFromProps(Number(pathSplit[8]));
+ const username = pathSplit[2];
+ const view = getViewFromProps(urlView);
- let form: GetPersonDetails = {
+ const form: GetPersonDetails = {
username: username,
- sort,
+ sort: getSortTypeFromQuery(sort),
saved_only: view === PersonDetailsView.Saved,
- page,
+ page: getPageFromString(page),
limit: fetchLimit,
- auth: req.auth,
+ auth,
};
- return [req.client.getPersonDetails(form)];
+
+ return [client.getPersonDetails(form)];
}
componentDidMount() {
saveScrollPosition(this.context);
}
- static getDerivedStateFromProps(props: any): ProfileProps {
- return {
- view: this.getViewFromProps(props.match.params.view),
- sort: this.getSortTypeFromProps(props.match.params.sort),
- page: this.getPageFromProps(props.match.params.page),
- person_id: Number(props.match.params.id),
- username: props.match.params.username,
- };
- }
-
- componentDidUpdate(lastProps: any) {
- // Necessary if you are on a post and you click another post (same route)
- if (
- lastProps.location.pathname.split("/")[2] !==
- lastProps.history.location.pathname.split("/")[2]
- ) {
- // Couldnt get a refresh working. This does for now.
- location.reload();
- }
- }
-
get documentTitle(): string {
- let res = this.state.personRes;
+ const res = this.state.personRes;
return res
? `@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`
: "";
}
render() {
- let res = this.state.personRes;
+ const { personRes, loading, siteRes } = this.state;
+ const { page, sort, view } = getProfileQueryParams();
+
return (
<div className="container-lg">
- {this.state.loading ? (
+ {loading ? (
<h5>
<Spinner large />
</h5>
) : (
- res && (
+ personRes && (
<div className="row">
<div className="col-12 col-md-8">
- <>
- <HtmlTags
- title={this.documentTitle}
- path={this.context.router.route.match.url}
- description={res.person_view.person.bio}
- image={res.person_view.person.avatar}
- />
- {this.userInfo()}
- <hr />
- </>
- {!this.state.loading && this.selects()}
+ <HtmlTags
+ title={this.documentTitle}
+ path={this.context.router.route.match.url}
+ description={personRes.person_view.person.bio}
+ image={personRes.person_view.person.avatar}
+ />
+
+ {this.userInfo}
+
+ <hr />
+
+ {this.selects}
+
<PersonDetails
- personRes={res}
- admins={this.state.siteRes.admins}
- sort={this.state.sort}
- page={this.state.page}
+ personRes={personRes}
+ admins={siteRes.admins}
+ sort={sort}
+ page={page}
limit={fetchLimit}
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- enableNsfw={enableNsfw(this.state.siteRes)}
- view={this.state.view}
+ enableDownvotes={enableDownvotes(siteRes)}
+ enableNsfw={enableNsfw(siteRes)}
+ view={view}
onPageChange={this.handlePageChange}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
+ allLanguages={siteRes.all_languages}
+ siteLanguages={siteRes.discussion_languages}
/>
</div>
- {!this.state.loading && (
- <div className="col-12 col-md-4">
- {this.moderates()}
- {this.amCurrentUser && this.follows()}
- </div>
- )}
+ <div className="col-12 col-md-4">
+ <Moderates moderates={personRes.moderates} />
+ {this.amCurrentUser && <Follows />}
+ </div>
</div>
)
)}
);
}
- viewRadios() {
+ get viewRadios() {
return (
<div className="btn-group btn-group-toggle flex-wrap mb-2">
- <label
- className={`btn btn-outline-secondary pointer
- ${this.state.view == PersonDetailsView.Overview && "active"}
- `}
- >
- <input
- type="radio"
- value={PersonDetailsView.Overview}
- checked={this.state.view === PersonDetailsView.Overview}
- onChange={linkEvent(this, this.handleViewChange)}
- />
- {i18n.t("overview")}
- </label>
- <label
- className={`btn btn-outline-secondary pointer
- ${this.state.view == PersonDetailsView.Comments && "active"}
- `}
- >
- <input
- type="radio"
- value={PersonDetailsView.Comments}
- checked={this.state.view == PersonDetailsView.Comments}
- onChange={linkEvent(this, this.handleViewChange)}
- />
- {i18n.t("comments")}
- </label>
- <label
- className={`btn btn-outline-secondary pointer
- ${this.state.view == PersonDetailsView.Posts && "active"}
- `}
- >
- <input
- type="radio"
- value={PersonDetailsView.Posts}
- checked={this.state.view == PersonDetailsView.Posts}
- onChange={linkEvent(this, this.handleViewChange)}
- />
- {i18n.t("posts")}
- </label>
- <label
- className={`btn btn-outline-secondary pointer
- ${this.state.view == PersonDetailsView.Saved && "active"}
- `}
- >
- <input
- type="radio"
- value={PersonDetailsView.Saved}
- checked={this.state.view == PersonDetailsView.Saved}
- onChange={linkEvent(this, this.handleViewChange)}
- />
- {i18n.t("saved")}
- </label>
+ {this.getRadio(PersonDetailsView.Overview)}
+ {this.getRadio(PersonDetailsView.Comments)}
+ {this.getRadio(PersonDetailsView.Posts)}
+ {this.getRadio(PersonDetailsView.Saved)}
</div>
);
}
- selects() {
- let profileRss = `/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`;
+ getRadio(view: PersonDetailsView) {
+ const { view: urlView } = getProfileQueryParams();
+ const active = view === urlView;
+
+ return (
+ <label
+ className={classNames("btn btn-outline-secondary pointer", {
+ active,
+ })}
+ >
+ <input
+ type="radio"
+ value={view}
+ checked={active}
+ onChange={linkEvent(this, this.handleViewChange)}
+ />
+ {i18n.t(view.toLowerCase() as NoOptionI18nKeys)}
+ </label>
+ );
+ }
+
+ get selects() {
+ const { sort } = getProfileQueryParams();
+ const { username } = this.props.match.params;
+
+ const profileRss = `/feeds/u/${username}.xml?sort=${sort}`;
return (
<div className="mb-2">
- <span className="mr-3">{this.viewRadios()}</span>
+ <span className="mr-3">{this.viewRadios}</span>
<SortSelect
- sort={this.state.sort}
+ sort={sort}
onChange={this.handleSortChange}
hideHot
hideMostComments
</div>
);
}
- handleBlockPerson(personId: number) {
- let auth = myAuth();
- if (auth) {
- if (personId != 0) {
- let blockUserForm: BlockPerson = {
- person_id: personId,
- block: true,
- auth,
- };
- WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
- }
- }
- }
- handleUnblockPerson(recipientId: number) {
- let auth = myAuth();
- if (auth) {
- let blockUserForm: BlockPerson = {
- person_id: recipientId,
- block: false,
- auth,
- };
- WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
- }
- }
- userInfo() {
- let pv = this.state.personRes?.person_view;
+ get userInfo() {
+ const pv = this.state.personRes?.person_view;
+ const {
+ personBlocked,
+ siteRes: { admins },
+ showBanDialog,
+ } = this.state;
+
return (
pv && (
<div>
)}
</ul>
</div>
- {this.banDialog()}
+ {this.banDialog}
<div className="flex-grow-1 unselectable pointer mx-2"></div>
{!this.amCurrentUser && UserService.Instance.myUserInfo && (
<>
className={
"d-flex align-self-start btn btn-secondary mr-2"
}
- to={`/create_private_message/recipient/${pv.person.id}`}
+ to={`/create_private_message/${pv.person.id}`}
>
{i18n.t("send_message")}
</Link>
- {this.state.personBlocked ? (
+ {personBlocked ? (
<button
className={
"d-flex align-self-start btn btn-secondary mr-2"
}
- onClick={linkEvent(
- pv.person.id,
- this.handleUnblockPerson
- )}
+ onClick={linkEvent(pv.person.id, handleUnblockPerson)}
>
{i18n.t("unblock_user")}
</button>
className={
"d-flex align-self-start btn btn-secondary mr-2"
}
- onClick={linkEvent(
- pv.person.id,
- this.handleBlockPerson
- )}
+ onClick={linkEvent(pv.person.id, handleBlockPerson)}
>
{i18n.t("block_user")}
</button>
</>
)}
- {canMod(pv.person.id, undefined, this.state.siteRes.admins) &&
- !isAdmin(pv.person.id, this.state.siteRes.admins) &&
- !this.state.showBanDialog &&
+ {canMod(pv.person.id, undefined, admins) &&
+ !isAdmin(pv.person.id, admins) &&
+ !showBanDialog &&
(!isBanned(pv.person) ? (
<button
className={
);
}
- banDialog() {
- let pv = this.state.personRes?.person_view;
+ get banDialog() {
+ const pv = this.state.personRes?.person_view;
+ const { showBanDialog } = this.state;
+
return (
pv && (
<>
- {this.state.showBanDialog && (
+ {showBanDialog && (
<form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
<div className="form-group row col-12">
<label className="col-form-label" htmlFor="profile-ban-reason">
);
}
- moderates() {
- let moderates = this.state.personRes?.moderates;
- return (
- moderates &&
- moderates.length > 0 && (
- <div className="card border-secondary mb-3">
- <div className="card-body">
- <h5>{i18n.t("moderates")}</h5>
- <ul className="list-unstyled mb-0">
- {moderates.map(cmv => (
- <li key={cmv.community.id}>
- <CommunityLink community={cmv.community} />
- </li>
- ))}
- </ul>
- </div>
- </div>
- )
- );
- }
+ updateUrl({ page, sort, view }: Partial<ProfileProps>) {
+ const {
+ page: urlPage,
+ sort: urlSort,
+ view: urlView,
+ } = getProfileQueryParams();
- follows() {
- let follows = UserService.Instance.myUserInfo?.follows;
- return (
- follows &&
- follows.length > 0 && (
- <div className="card border-secondary mb-3">
- <div className="card-body">
- <h5>{i18n.t("subscribed")}</h5>
- <ul className="list-unstyled mb-0">
- {follows.map(cfv => (
- <li key={cfv.community.id}>
- <CommunityLink community={cfv.community} />
- </li>
- ))}
- </ul>
- </div>
- </div>
- )
- );
- }
+ const queryParams: QueryParams<ProfileProps> = {
+ page: (page ?? urlPage).toString(),
+ sort: sort ?? urlSort,
+ view: view ?? urlView,
+ };
- updateUrl(paramUpdates: UrlParams) {
- const page = paramUpdates.page || this.state.page;
- const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
- const sortStr = paramUpdates.sort || this.state.sort;
+ const { username } = this.props.match.params;
- let typeView = `/u/${this.state.userName}`;
+ this.props.history.push(`/u/${username}${getQueryString(queryParams)}`);
- this.props.history.push(
- `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
- );
this.setState({ loading: true });
this.fetchUserData();
}
handlePageChange(page: number) {
- this.updateUrl({ page: page });
+ this.updateUrl({ page });
}
- handleSortChange(val: SortType) {
- this.updateUrl({ sort: val, page: 1 });
+ handleSortChange(sort: SortType) {
+ this.updateUrl({ sort, page: 1 });
}
handleViewChange(i: Profile, event: any) {
i.updateUrl({
- view: PersonDetailsView[Number(event.target.value)],
+ view: PersonDetailsView[event.target.value],
page: 1,
});
}
handleModBanSubmit(i: Profile, event?: any) {
if (event) event.preventDefault();
- let person = i.state.personRes?.person_view.person;
- let auth = myAuth();
+ const { personRes, removeData, banReason, banExpireDays } = i.state;
+
+ const person = personRes?.person_view.person;
+ const auth = myAuth();
+
if (person && auth) {
+ const ban = !person.banned;
+
// If its an unban, restore all their data
- let ban = !person.banned;
- if (ban == false) {
+ if (!ban) {
i.setState({ removeData: false });
}
- let form: BanPerson = {
+
+ const form: BanPerson = {
person_id: person.id,
ban,
- remove_data: i.state.removeData,
- reason: i.state.banReason,
- expires: futureDaysToUnixTime(i.state.banExpireDays),
+ remove_data: removeData,
+ reason: banReason,
+ expires: futureDaysToUnixTime(banExpireDays),
auth,
};
WebSocketService.Instance.send(wsClient.banPerson(form));
}
parseMessage(msg: any) {
- let op = wsUserOp(msg);
+ const op = wsUserOp(msg);
console.log(msg);
+
if (msg.error) {
toast(i18n.t(msg.error), "danger");
- if (msg.error == "couldnt_find_that_username_or_email") {
+
+ if (msg.error === "couldnt_find_that_username_or_email") {
this.context.router.history.push("/");
}
- return;
} else if (msg.reconnect) {
this.fetchUserData();
- } else if (op == UserOperation.GetPersonDetails) {
- // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
- // and set the parent state if it is not set or differs
- // TODO this might need to get abstracted
- let data = wsJsonToRes<GetPersonDetailsResponse>(msg);
- this.setState({ personRes: data, loading: false });
- this.setPersonBlock();
- restoreScrollPosition(this.context);
- } else if (op == UserOperation.AddAdmin) {
- let data = wsJsonToRes<AddAdminResponse>(msg);
- this.setState(s => ((s.siteRes.admins = data.admins), s));
- } else if (op == UserOperation.CreateCommentLike) {
- let data = wsJsonToRes<CommentResponse>(msg);
- createCommentLikeRes(data.comment_view, this.state.personRes?.comments);
- this.setState(this.state);
- } else if (
- op == UserOperation.EditComment ||
- op == UserOperation.DeleteComment ||
- op == UserOperation.RemoveComment
- ) {
- let data = wsJsonToRes<CommentResponse>(msg);
- editCommentRes(data.comment_view, this.state.personRes?.comments);
- this.setState(this.state);
- } else if (op == UserOperation.CreateComment) {
- let data = wsJsonToRes<CommentResponse>(msg);
- let mui = UserService.Instance.myUserInfo;
- if (data.comment_view.creator.id == mui?.local_user_view.person.id) {
- toast(i18n.t("reply_sent"));
- }
- } else if (op == UserOperation.SaveComment) {
- let data = wsJsonToRes<CommentResponse>(msg);
- saveCommentRes(data.comment_view, this.state.personRes?.comments);
- this.setState(this.state);
- } else if (
- op == UserOperation.EditPost ||
- op == UserOperation.DeletePost ||
- op == UserOperation.RemovePost ||
- op == UserOperation.LockPost ||
- op == UserOperation.FeaturePost ||
- op == UserOperation.SavePost
- ) {
- let data = wsJsonToRes<PostResponse>(msg);
- editPostFindRes(data.post_view, this.state.personRes?.posts);
- this.setState(this.state);
- } else if (op == UserOperation.CreatePostLike) {
- let data = wsJsonToRes<PostResponse>(msg);
- createPostLikeFindRes(data.post_view, this.state.personRes?.posts);
- this.setState(this.state);
- } else if (op == UserOperation.BanPerson) {
- let data = wsJsonToRes<BanPersonResponse>(msg);
- let res = this.state.personRes;
- res?.comments
- .filter(c => c.creator.id == data.person_view.person.id)
- .forEach(c => (c.creator.banned = data.banned));
- res?.posts
- .filter(c => c.creator.id == data.person_view.person.id)
- .forEach(c => (c.creator.banned = data.banned));
- let pv = res?.person_view;
-
- if (pv?.person.id == data.person_view.person.id) {
- pv.person.banned = data.banned;
- }
- this.setState(this.state);
- } else if (op == UserOperation.BlockPerson) {
- let data = wsJsonToRes<BlockPersonResponse>(msg);
- updatePersonBlock(data);
- this.setPersonBlock();
- this.setState(this.state);
- } else if (
- op == UserOperation.PurgePerson ||
- op == UserOperation.PurgePost ||
- op == UserOperation.PurgeComment ||
- op == UserOperation.PurgeCommunity
- ) {
- let data = wsJsonToRes<PurgeItemResponse>(msg);
- if (data.success) {
- toast(i18n.t("purge_success"));
- this.context.router.history.push(`/`);
+ } else {
+ switch (op) {
+ case UserOperation.GetPersonDetails: {
+ // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
+ // and set the parent state if it is not set or differs
+ // TODO this might need to get abstracted
+ const data = wsJsonToRes<GetPersonDetailsResponse>(msg);
+ this.setState({ personRes: data, loading: false });
+ this.setPersonBlock();
+ restoreScrollPosition(this.context);
+
+ break;
+ }
+
+ case UserOperation.AddAdmin: {
+ const { admins } = wsJsonToRes<AddAdminResponse>(msg);
+ this.setState(s => ((s.siteRes.admins = admins), s));
+
+ break;
+ }
+
+ case UserOperation.CreateCommentLike: {
+ const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+ createCommentLikeRes(comment_view, this.state.personRes?.comments);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.EditComment:
+ case UserOperation.DeleteComment:
+ case UserOperation.RemoveComment: {
+ const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+ editCommentRes(comment_view, this.state.personRes?.comments);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.CreateComment: {
+ const {
+ comment_view: {
+ creator: { id },
+ },
+ } = wsJsonToRes<CommentResponse>(msg);
+ const mui = UserService.Instance.myUserInfo;
+
+ if (id === mui?.local_user_view.person.id) {
+ toast(i18n.t("reply_sent"));
+ }
+
+ break;
+ }
+
+ case UserOperation.SaveComment: {
+ const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+ saveCommentRes(comment_view, this.state.personRes?.comments);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.EditPost:
+ case UserOperation.DeletePost:
+ case UserOperation.RemovePost:
+ case UserOperation.LockPost:
+ case UserOperation.FeaturePost:
+ case UserOperation.SavePost: {
+ const { post_view } = wsJsonToRes<PostResponse>(msg);
+ editPostFindRes(post_view, this.state.personRes?.posts);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.CreatePostLike: {
+ const { post_view } = wsJsonToRes<PostResponse>(msg);
+ createPostLikeFindRes(post_view, this.state.personRes?.posts);
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.BanPerson: {
+ const data = wsJsonToRes<BanPersonResponse>(msg);
+ const res = this.state.personRes;
+ res?.comments
+ .filter(c => c.creator.id === data.person_view.person.id)
+ .forEach(c => (c.creator.banned = data.banned));
+ res?.posts
+ .filter(c => c.creator.id === data.person_view.person.id)
+ .forEach(c => (c.creator.banned = data.banned));
+ const pv = res?.person_view;
+
+ if (pv?.person.id === data.person_view.person.id) {
+ pv.person.banned = data.banned;
+ }
+ this.setState(this.state);
+
+ break;
+ }
+
+ case UserOperation.BlockPerson: {
+ const data = wsJsonToRes<BlockPersonResponse>(msg);
+ updatePersonBlock(data);
+ this.setPersonBlock();
+
+ break;
+ }
+
+ case UserOperation.PurgePerson:
+ case UserOperation.PurgePost:
+ case UserOperation.PurgeComment:
+ case UserOperation.PurgeCommunity: {
+ const { success } = wsJsonToRes<PurgeItemResponse>(msg);
+
+ if (success) {
+ toast(i18n.t("purge_success"));
+ this.context.router.history.push(`/`);
+ }
+ }
}
}
}
+import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import {
BlockCommunity,
BlockPersonResponse,
ChangePassword,
CommunityBlockView,
- CommunityView,
DeleteAccount,
GetSiteResponse,
ListingType,
LoginResponse,
PersonBlockView,
- PersonViewSafe,
SaveUserSettings,
SortType,
UserOperation,
import { UserService, WebSocketService } from "../../services";
import {
capitalizeFirstLetter,
- choicesConfig,
- communitySelectName,
+ Choice,
communityToChoice,
debounce,
elementUrl,
+ emDash,
enableNsfw,
fetchCommunities,
fetchThemeList,
fetchUsers,
getLanguages,
- isBrowser,
myAuth,
- personSelectName,
personToChoice,
relTags,
setIsoData,
import { LanguageSelect } from "../common/language-select";
import { ListingTypeSelect } from "../common/listing-type-select";
import { MarkdownTextArea } from "../common/markdown-textarea";
+import { SearchableSelect } from "../common/searchable-select";
import { SortSelect } from "../common/sort-select";
import { CommunityLink } from "../community/community-link";
import { PersonListing } from "./person-listing";
-var Choices: any;
-if (isBrowser()) {
- Choices = require("choices.js");
-}
-
interface SettingsState {
// TODO redo these forms
saveUserSettingsForm: {
password?: string;
};
personBlocks: PersonBlockView[];
- blockPerson?: PersonViewSafe;
communityBlocks: CommunityBlockView[];
- blockCommunityId: number;
- blockCommunity?: CommunityView;
currentTab: string;
themeList: string[];
saveUserSettingsLoading: boolean;
deleteAccountLoading: boolean;
deleteAccountShowConfirm: boolean;
siteRes: GetSiteResponse;
+ searchCommunityLoading: boolean;
+ searchCommunityOptions: Choice[];
+ searchPersonLoading: boolean;
+ searchPersonOptions: Choice[];
}
+type FilterType = "user" | "community";
+
+const Filter = ({
+ filterType,
+ options,
+ onChange,
+ onSearch,
+ loading,
+}: {
+ filterType: FilterType;
+ options: Choice[];
+ onSearch: (text: string) => void;
+ onChange: (choice: Choice) => void;
+ loading: boolean;
+}) => (
+ <div className="form-group row">
+ <label
+ className="col-md-4 col-form-label"
+ htmlFor={`block-${filterType}-filter`}
+ >
+ {i18n.t(`block_${filterType}` as NoOptionI18nKeys)}
+ </label>
+ <div className="col-md-8">
+ <SearchableSelect
+ id={`block-${filterType}-filter`}
+ options={[
+ { label: emDash, value: "0", disabled: true } as Choice,
+ ].concat(options)}
+ loading={loading}
+ onChange={onChange}
+ onSearch={onSearch}
+ />
+ </div>
+ </div>
+);
+
export class Settings extends Component<any, SettingsState> {
private isoData = setIsoData(this.context);
- private blockPersonChoices: any;
- private blockCommunityChoices: any;
private subscription?: Subscription;
state: SettingsState = {
saveUserSettingsForm: {},
deleteAccountForm: {},
personBlocks: [],
communityBlocks: [],
- blockCommunityId: 0,
currentTab: "settings",
siteRes: this.isoData.site_res,
themeList: [],
+ searchCommunityLoading: false,
+ searchCommunityOptions: [],
+ searchPersonLoading: false,
+ searchPersonOptions: [],
};
constructor(props: any, context: any) {
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
- let mui = UserService.Instance.myUserInfo;
+ const mui = UserService.Instance.myUserInfo;
if (mui) {
- let luv = mui.local_user_view;
+ const {
+ local_user: {
+ show_nsfw,
+ theme,
+ default_sort_type,
+ default_listing_type,
+ interface_language,
+ show_avatars,
+ show_bot_accounts,
+ show_scores,
+ show_read_posts,
+ show_new_post_notifs,
+ send_notifications_to_email,
+ email,
+ },
+ person: {
+ avatar,
+ banner,
+ display_name,
+ bot_account,
+ bio,
+ matrix_user_id,
+ },
+ } = mui.local_user_view;
+
this.state = {
...this.state,
personBlocks: mui.person_blocks,
communityBlocks: mui.community_blocks,
saveUserSettingsForm: {
...this.state.saveUserSettingsForm,
- show_nsfw: luv.local_user.show_nsfw,
- theme: luv.local_user.theme ? luv.local_user.theme : "browser",
- default_sort_type: luv.local_user.default_sort_type,
- default_listing_type: luv.local_user.default_listing_type,
- interface_language: luv.local_user.interface_language,
+ show_nsfw,
+ theme: theme ?? "browser",
+ default_sort_type,
+ default_listing_type,
+ interface_language,
discussion_languages: mui.discussion_languages,
- avatar: luv.person.avatar,
- banner: luv.person.banner,
- display_name: luv.person.display_name,
- show_avatars: luv.local_user.show_avatars,
- bot_account: luv.person.bot_account,
- show_bot_accounts: luv.local_user.show_bot_accounts,
- show_scores: luv.local_user.show_scores,
- show_read_posts: luv.local_user.show_read_posts,
- show_new_post_notifs: luv.local_user.show_new_post_notifs,
- email: luv.local_user.email,
- bio: luv.person.bio,
- send_notifications_to_email:
- luv.local_user.send_notifications_to_email,
- matrix_user_id: luv.person.matrix_user_id,
+ avatar,
+ banner,
+ display_name,
+ show_avatars,
+ bot_account,
+ show_bot_accounts,
+ show_scores,
+ show_read_posts,
+ show_new_post_notifs,
+ email,
+ bio,
+ send_notifications_to_email,
+ matrix_user_id,
},
};
}
}
blockUserCard() {
+ const { searchPersonLoading, searchPersonOptions } = this.state;
+
return (
<div>
- {this.blockUserForm()}
+ <Filter
+ filterType="user"
+ loading={searchPersonLoading}
+ onChange={this.handleBlockPerson}
+ onSearch={this.handlePersonSearch}
+ options={searchPersonOptions}
+ />
{this.blockedUsersList()}
</div>
);
);
}
- blockUserForm() {
- let blockPerson = this.state.blockPerson;
- return (
- <div className="form-group row">
- <label
- className="col-md-4 col-form-label"
- htmlFor="block-person-filter"
- >
- {i18n.t("block_user")}
- </label>
- <div className="col-md-8">
- <select
- className="form-control"
- id="block-person-filter"
- value={blockPerson?.person.id ?? 0}
- >
- <option value="0">—</option>
- {blockPerson && (
- <option value={blockPerson.person.id}>
- {personSelectName(blockPerson)}
- </option>
- )}
- </select>
- </div>
- </div>
- );
- }
-
blockCommunityCard() {
+ const { searchCommunityLoading, searchCommunityOptions } = this.state;
+
return (
<div>
- {this.blockCommunityForm()}
+ <Filter
+ filterType="community"
+ loading={searchCommunityLoading}
+ onChange={this.handleBlockCommunity}
+ onSearch={this.handleCommunitySearch}
+ options={searchCommunityOptions}
+ />
{this.blockedCommunitiesList()}
</div>
);
);
}
- blockCommunityForm() {
- return (
- <div className="form-group row">
- <label
- className="col-md-4 col-form-label"
- htmlFor="block-community-filter"
- >
- {i18n.t("block_community")}
- </label>
- <div className="col-md-8">
- <select
- className="form-control"
- id="block-community-filter"
- value={this.state.blockCommunityId}
- >
- <option value="0">—</option>
- {this.state.blockCommunity && (
- <option value={this.state.blockCommunity.community.id}>
- {communitySelectName(this.state.blockCommunity)}
- </option>
- )}
- </select>
- </div>
- </div>
- );
- }
-
saveUserSettingsHtmlForm() {
let selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
);
}
- setupBlockPersonChoices() {
- if (isBrowser()) {
- let selectId: any = document.getElementById("block-person-filter");
- if (selectId) {
- this.blockPersonChoices = new Choices(selectId, choicesConfig);
- this.blockPersonChoices.passedElement.element.addEventListener(
- "choice",
- (e: any) => {
- this.handleBlockPerson(Number(e.detail.choice.value));
- },
- false
- );
- this.blockPersonChoices.passedElement.element.addEventListener(
- "search",
- debounce(async (e: any) => {
- try {
- let persons = (await fetchUsers(e.detail.value)).users;
- let choices = persons.map(pvs => personToChoice(pvs));
- this.blockPersonChoices.setChoices(
- choices,
- "value",
- "label",
- true
- );
- } catch (err) {
- console.error(err);
- }
- }),
- false
- );
- }
+ handlePersonSearch = debounce(async (text: string) => {
+ this.setState({ searchPersonLoading: true });
+
+ const searchPersonOptions: Choice[] = [];
+
+ if (text.length > 0) {
+ searchPersonOptions.push(
+ ...(await fetchUsers(text)).users.map(personToChoice)
+ );
}
- }
- setupBlockCommunityChoices() {
- if (isBrowser()) {
- let selectId: any = document.getElementById("block-community-filter");
- if (selectId) {
- this.blockCommunityChoices = new Choices(selectId, choicesConfig);
- this.blockCommunityChoices.passedElement.element.addEventListener(
- "choice",
- (e: any) => {
- this.handleBlockCommunity(Number(e.detail.choice.value));
- },
- false
- );
- this.blockCommunityChoices.passedElement.element.addEventListener(
- "search",
- debounce(async (e: any) => {
- try {
- let communities = (await fetchCommunities(e.detail.value))
- .communities;
- let choices = communities.map(cv => communityToChoice(cv));
- this.blockCommunityChoices.setChoices(
- choices,
- "value",
- "label",
- true
- );
- } catch (err) {
- console.log(err);
- }
- }),
- false
- );
- }
+ this.setState({
+ searchPersonLoading: false,
+ searchPersonOptions,
+ });
+ });
+
+ handleCommunitySearch = debounce(async (text: string) => {
+ this.setState({ searchCommunityLoading: true });
+
+ const searchCommunityOptions: Choice[] = [];
+
+ if (text.length > 0) {
+ searchCommunityOptions.push(
+ ...(await fetchCommunities(text)).communities.map(communityToChoice)
+ );
}
- }
- handleBlockPerson(personId: number) {
- let auth = myAuth();
- if (auth && personId != 0) {
- let blockUserForm: BlockPerson = {
- person_id: personId,
+ this.setState({
+ searchCommunityLoading: false,
+ searchCommunityOptions,
+ });
+ });
+
+ handleBlockPerson({ value }: Choice) {
+ const auth = myAuth();
+ if (auth && value !== "0") {
+ const blockUserForm: BlockPerson = {
+ person_id: Number(value),
block: true,
auth,
};
+
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
}
}
handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
- let auth = myAuth();
+ const auth = myAuth();
if (auth) {
- let blockUserForm: BlockPerson = {
+ const blockUserForm: BlockPerson = {
person_id: i.recipientId,
block: false,
auth,
}
}
- handleBlockCommunity(community_id: number) {
- let auth = myAuth();
- if (auth && community_id != 0) {
- let blockCommunityForm: BlockCommunity = {
- community_id,
+ handleBlockCommunity({ value }: Choice) {
+ const auth = myAuth();
+ if (auth && value !== "0") {
+ const blockCommunityForm: BlockCommunity = {
+ community_id: Number(value),
block: true,
auth,
};
}
handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
- let auth = myAuth();
+ const auth = myAuth();
if (auth) {
- let blockCommunityForm: BlockCommunity = {
+ const blockCommunityForm: BlockCommunity = {
community_id: i.communityId,
block: false,
auth,
handleSwitchTab(i: { ctx: Settings; tab: string }) {
i.ctx.setState({ currentTab: i.tab });
-
- if (i.ctx.state.currentTab == "blocks") {
- i.ctx.setupBlockPersonChoices();
- i.ctx.setupBlockCommunityChoices();
- }
}
parseMessage(msg: any) {
import { Component } from "inferno";
+import { RouteComponentProps } from "inferno-router/dist/Route";
import {
GetCommunity,
GetCommunityResponse,
GetSiteResponse,
- ListCommunities,
- ListCommunitiesResponse,
- ListingType,
PostView,
- SortType,
UserOperation,
wsJsonToRes,
wsUserOp,
import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services";
import {
+ Choice,
enableDownvotes,
enableNsfw,
- fetchLimit,
+ getIdFromString,
+ getQueryParams,
+ getQueryString,
isBrowser,
myAuth,
+ QueryParams,
setIsoData,
toast,
wsClient,
import { Spinner } from "../common/icon";
import { PostForm } from "./post-form";
+export interface CreatePostProps {
+ communityId?: number;
+}
+
+function getCreatePostQueryParams() {
+ return getQueryParams<CreatePostProps>({
+ communityId: getIdFromString,
+ });
+}
+
interface CreatePostState {
- listCommunitiesResponse?: ListCommunitiesResponse;
siteRes: GetSiteResponse;
loading: boolean;
+ selectedCommunityChoice?: Choice;
}
-export class CreatePost extends Component<any, CreatePostState> {
+export class CreatePost extends Component<
+ RouteComponentProps<Record<string, never>>,
+ CreatePostState
+> {
private isoData = setIsoData(this.context);
private subscription?: Subscription;
state: CreatePostState = {
loading: true,
};
- constructor(props: any, context: any) {
+ constructor(props: RouteComponentProps<Record<string, never>>, context: any) {
super(props, context);
this.handlePostCreate = this.handlePostCreate.bind(this);
+ this.handleSelectedCommunityChange =
+ this.handleSelectedCommunityChange.bind(this);
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
}
// Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
+ if (this.isoData.path === this.context.router.route.match.url) {
+ const communityRes = this.isoData.routeData[0] as
+ | GetCommunityResponse
+ | undefined;
+
+ if (communityRes) {
+ const communityChoice: Choice = {
+ label: communityRes.community_view.community.name,
+ value: communityRes.community_view.community.id.toString(),
+ };
+
+ this.state = {
+ ...this.state,
+ selectedCommunityChoice: communityChoice,
+ };
+ }
+
this.state = {
...this.state,
- listCommunitiesResponse: this.isoData
- .routeData[0] as ListCommunitiesResponse,
loading: false,
};
} else {
- this.refetch();
+ this.fetchCommunity();
}
}
- refetch() {
- let nameOrId = this.params.nameOrId;
- let auth = myAuth(false);
- if (nameOrId) {
- if (typeof nameOrId === "string") {
- let form: GetCommunity = {
- name: nameOrId,
- auth,
- };
- WebSocketService.Instance.send(wsClient.getCommunity(form));
- } else {
- let form: GetCommunity = {
- id: nameOrId,
- auth,
- };
- WebSocketService.Instance.send(wsClient.getCommunity(form));
- }
- } else {
- let listCommunitiesForm: ListCommunities = {
- type_: ListingType.All,
- sort: SortType.TopAll,
- limit: fetchLimit,
+ fetchCommunity() {
+ const { communityId } = getCreatePostQueryParams();
+ const auth = myAuth(false);
+
+ if (communityId) {
+ const form: GetCommunity = {
+ id: communityId,
auth,
};
- WebSocketService.Instance.send(
- wsClient.listCommunities(listCommunitiesForm)
- );
+
+ WebSocketService.Instance.send(wsClient.getCommunity(form));
+ }
+ }
+
+ componentDidMount(): void {
+ const { communityId } = getCreatePostQueryParams();
+
+ if (communityId?.toString() !== this.state.selectedCommunityChoice?.value) {
+ this.fetchCommunity();
+ } else if (!communityId) {
+ this.setState({
+ selectedCommunityChoice: undefined,
+ loading: false,
+ });
}
}
}
render() {
- let res = this.state.listCommunitiesResponse;
+ const { selectedCommunityChoice } = this.state;
+
+ const locationState = this.props.history.location.state as
+ | PostFormParams
+ | undefined;
+
return (
<div className="container-lg">
<HtmlTags
<Spinner large />
</h5>
) : (
- res && (
- <div className="row">
- <div className="col-12 col-lg-6 offset-lg-3 mb-4">
- <h5>{i18n.t("create_post")}</h5>
- <PostForm
- communities={res.communities}
- onCreate={this.handlePostCreate}
- params={this.params}
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- enableNsfw={enableNsfw(this.state.siteRes)}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
- />
- </div>
+ <div className="row">
+ <div className="col-12 col-lg-6 offset-lg-3 mb-4">
+ <h5>{i18n.t("create_post")}</h5>
+ <PostForm
+ onCreate={this.handlePostCreate}
+ params={locationState}
+ enableDownvotes={enableDownvotes(this.state.siteRes)}
+ enableNsfw={enableNsfw(this.state.siteRes)}
+ allLanguages={this.state.siteRes.all_languages}
+ siteLanguages={this.state.siteRes.discussion_languages}
+ selectedCommunityChoice={selectedCommunityChoice}
+ onSelectCommunity={this.handleSelectedCommunityChange}
+ />
</div>
- )
+ </div>
)}
</div>
);
}
- get params(): PostFormParams {
- let urlParams = new URLSearchParams(this.props.location.search);
- let name = urlParams.get("community_name") ?? this.prevCommunityName;
- let communityIdParam = urlParams.get("community_id");
- let id = communityIdParam ? Number(communityIdParam) : this.prevCommunityId;
- let nameOrId: string | number | undefined;
- if (name) {
- nameOrId = name;
- } else if (id) {
- nameOrId = id;
- }
+ updateUrl({ communityId }: Partial<CreatePostProps>) {
+ const { communityId: urlCommunityId } = getCreatePostQueryParams();
- let params: PostFormParams = {
- name: urlParams.get("title") ?? undefined,
- nameOrId,
- body: urlParams.get("body") ?? undefined,
- url: urlParams.get("url") ?? undefined,
+ const queryParams: QueryParams<CreatePostProps> = {
+ communityId: (communityId ?? urlCommunityId)?.toString(),
};
- return params;
- }
+ const locationState = this.props.history.location.state as
+ | PostFormParams
+ | undefined;
- get prevCommunityName(): string | undefined {
- if (this.props.match.params.name) {
- return this.props.match.params.name;
- } else if (this.props.location.state) {
- let lastLocation = this.props.location.state.prevPath;
- if (lastLocation.includes("/c/")) {
- return lastLocation.split("/c/").at(1);
- }
- }
- return undefined;
+ this.props.history.push(
+ `/create_post${getQueryString(queryParams)}`,
+ locationState
+ );
+
+ this.fetchCommunity();
}
- get prevCommunityId(): number | undefined {
- // TODO is this actually a number? Whats the real return type
- let id = this.props.match.params.id;
- return id ?? undefined;
+ handleSelectedCommunityChange(choice: Choice) {
+ this.updateUrl({
+ communityId: getIdFromString(choice?.value),
+ });
}
handlePostCreate(post_view: PostView) {
- this.props.history.push(`/post/${post_view.post.id}`);
+ this.props.history.replace(`/post/${post_view.post.id}`);
}
- static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
- let listCommunitiesForm: ListCommunities = {
- type_: ListingType.All,
- sort: SortType.TopAll,
- limit: fetchLimit,
- auth: req.auth,
- };
- return [req.client.listCommunities(listCommunitiesForm)];
+ static fetchInitialData({
+ client,
+ query: { communityId },
+ auth,
+ }: InitialFetchRequest<QueryParams<CreatePostProps>>): Promise<any>[] {
+ const promises: Promise<any>[] = [];
+
+ if (communityId) {
+ const form: GetCommunity = {
+ auth,
+ id: getIdFromString(communityId),
+ };
+
+ promises.push(client.getCommunity(form));
+ } else {
+ promises.push(Promise.resolve());
+ }
+
+ return promises;
}
parseMessage(msg: any) {
- let op = wsUserOp(msg);
+ const op = wsUserOp(msg);
console.log(msg);
if (msg.error) {
toast(i18n.t(msg.error), "danger");
return;
- } else if (op == UserOperation.ListCommunities) {
- let data = wsJsonToRes<ListCommunitiesResponse>(msg);
- this.setState({ listCommunitiesResponse: data, loading: false });
- } else if (op == UserOperation.GetCommunity) {
- let data = wsJsonToRes<GetCommunityResponse>(msg);
- this.setState({
- listCommunitiesResponse: {
- communities: [data.community_view],
+ }
+
+ if (op === UserOperation.GetCommunity) {
+ const {
+ community_view: {
+ community: { name, id },
},
+ } = wsJsonToRes<GetCommunityResponse>(msg);
+
+ this.setState({
+ selectedCommunityChoice: { label: name, value: id.toString() },
loading: false,
});
}
import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router";
import {
- CommunityView,
CreatePost,
EditPost,
Language,
import {
archiveTodayUrl,
capitalizeFirstLetter,
- choicesConfig,
- communitySelectName,
+ Choice,
communityToChoice,
debounce,
fetchCommunities,
+ getIdFromString,
getSiteMetadata,
ghostArchiveUrl,
- isBrowser,
isImage,
myAuth,
myFirstDiscussionLanguageId,
import { Icon, Spinner } from "../common/icon";
import { LanguageSelect } from "../common/language-select";
import { MarkdownTextArea } from "../common/markdown-textarea";
+import { SearchableSelect } from "../common/searchable-select";
import { PostListings } from "./post-listings";
-var Choices: any;
-if (isBrowser()) {
- Choices = require("choices.js");
-}
-
const MAX_POST_TITLE_LENGTH = 200;
interface PostFormProps {
post_view?: PostView; // If a post is given, that means this is an edit
allLanguages: Language[];
siteLanguages: number[];
- communities?: CommunityView[];
params?: PostFormParams;
onCancel?(): any;
onCreate?(post: PostView): any;
onEdit?(post: PostView): any;
enableNsfw?: boolean;
enableDownvotes?: boolean;
+ selectedCommunityChoice?: Choice;
+ onSelectCommunity?: (choice: Choice) => void;
}
interface PostFormState {
loading: boolean;
imageLoading: boolean;
communitySearchLoading: boolean;
+ communitySearchOptions: Choice[];
previewMode: boolean;
}
export class PostForm extends Component<PostFormProps, PostFormState> {
private subscription?: Subscription;
- private choices: any;
state: PostFormState = {
form: {},
loading: false,
imageLoading: false,
communitySearchLoading: false,
previewMode: false,
+ communitySearchOptions: [],
};
- constructor(props: any, context: any) {
+ constructor(props: PostFormProps, context: any) {
super(props, context);
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this));
this.fetchPageTitle = debounce(this.fetchPageTitle.bind(this));
this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
this.handleLanguageChange = this.handleLanguageChange.bind(this);
+ this.handleCommunitySelect = this.handleCommunitySelect.bind(this);
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
// Means its an edit
- let pv = this.props.post_view;
+ const pv = this.props.post_view;
if (pv) {
this.state = {
...this.state,
};
}
- let params = this.props.params;
+ const selectedCommunityChoice = this.props.selectedCommunityChoice;
+
+ if (selectedCommunityChoice) {
+ this.state = {
+ ...this.state,
+ form: {
+ ...this.state.form,
+ community_id: getIdFromString(selectedCommunityChoice.value),
+ },
+ communitySearchOptions: [selectedCommunityChoice],
+ };
+ }
+
+ const params = this.props.params;
if (params) {
this.state = {
...this.state,
form: {
...this.state.form,
- name: params.name,
- url: params.url,
- body: params.body,
+ ...params,
},
};
}
componentDidMount() {
setupTippy();
- this.setupCommunities();
- let textarea: any = document.getElementById("post-title");
+ const textarea: any = document.getElementById("post-title");
+
if (textarea) {
autosize(textarea);
}
window.onbeforeunload = null;
}
+ static getDerivedStateFromProps(
+ { selectedCommunityChoice }: PostFormProps,
+ { form, ...restState }: PostFormState
+ ) {
+ return {
+ ...restState,
+ form: {
+ ...form,
+ community_id: getIdFromString(selectedCommunityChoice?.value),
+ },
+ };
+ }
+
render() {
let firstLang =
this.state.form.language_id ??
className="col-sm-2 col-form-label"
htmlFor="post-community"
>
- {this.state.communitySearchLoading ? (
- <Spinner />
- ) : (
- i18n.t("community")
- )}
+ {i18n.t("community")}
</label>
<div className="col-sm-10">
- <select
- className="form-control"
+ <SearchableSelect
id="post-community"
value={this.state.form.community_id}
- onInput={linkEvent(this, this.handlePostCommunityChange)}
- >
- <option>{i18n.t("select_a_community")}</option>
- {this.props.communities?.map(cv => (
- <option key={cv.community.id} value={cv.community.id}>
- {communitySelectName(cv)}
- </option>
- ))}
- </select>
+ options={[
+ {
+ label: i18n.t("select_a_community"),
+ value: "",
+ disabled: true,
+ } as Choice,
+ ].concat(this.state.communitySearchOptions)}
+ loading={this.state.communitySearchLoading}
+ onChange={this.handleCommunitySelect}
+ onSearch={this.handleCommunitySearch}
+ />
</div>
</div>
)}
});
}
- setupCommunities() {
- // Set up select searching
- if (isBrowser()) {
- let selectId: any = document.getElementById("post-community");
- if (selectId) {
- this.choices = new Choices(selectId, choicesConfig);
- this.choices.passedElement.element.addEventListener(
- "choice",
- (e: any) => {
- this.setState(
- s => ((s.form.community_id = Number(e.detail.choice.value)), s)
- );
- },
- false
- );
- this.choices.passedElement.element.addEventListener("search", () => {
- this.setState({ communitySearchLoading: true });
- });
- this.choices.passedElement.element.addEventListener(
- "search",
- debounce(async (e: any) => {
- try {
- let communities = (await fetchCommunities(e.detail.value))
- .communities;
- this.choices.setChoices(
- communities.map(cv => communityToChoice(cv)),
- "value",
- "label",
- true
- );
- this.setState({ communitySearchLoading: false });
- } catch (err) {
- console.log(err);
- }
- }),
- false
- );
- }
+ handleCommunitySearch = debounce(async (text: string) => {
+ const { selectedCommunityChoice } = this.props;
+ this.setState({ communitySearchLoading: true });
+
+ const newOptions: Choice[] = [];
+
+ if (selectedCommunityChoice) {
+ newOptions.push(selectedCommunityChoice);
}
- let pv = this.props.post_view;
- this.setState(s => ((s.form.community_id = pv?.community.id), s));
-
- let nameOrId = this.props.params?.nameOrId;
- if (nameOrId) {
- if (typeof nameOrId === "string") {
- let name_ = nameOrId;
- let foundCommunityId = this.props.communities?.find(
- r => r.community.name == name_
- )?.community.id;
- this.setState(s => ((s.form.community_id = foundCommunityId), s));
- } else {
- let id = nameOrId;
- this.setState(s => ((s.form.community_id = id), s));
- }
+ if (text.length > 0) {
+ newOptions.push(
+ ...(await fetchCommunities(text)).communities.map(communityToChoice)
+ );
+
+ this.setState({
+ communitySearchOptions: newOptions,
+ });
}
- if (isBrowser() && this.state.form.community_id) {
- this.choices.setChoiceByValue(this.state.form.community_id.toString());
+ this.setState({
+ communitySearchLoading: false,
+ });
+ });
+
+ handleCommunitySelect(choice: Choice) {
+ if (this.props.onSelectCommunity) {
+ this.setState({
+ loading: true,
+ });
+
+ this.props.onSelectCommunity(choice);
+
+ this.setState({ loading: false });
}
- this.setState(this.state);
}
parseMessage(msg: any) {
} from "lemmy-js-client";
import { externalHost } from "../../env";
import { i18n } from "../../i18next";
-import { BanType, PurgeType } from "../../interfaces";
+import { BanType, PostFormParams, PurgeType } from "../../interfaces";
import { UserService, WebSocketService } from "../../services";
import {
amAdmin,
}
render() {
- let post = this.props.post_view.post;
+ const post = this.props.post_view.post;
+
return (
<div className="post-listing">
{!this.state.showEdit ? (
return (
<Link
className="btn btn-link btn-animate text-muted py-0"
- to={`/create_post${this.crossPostParams}`}
+ to={{
+ /* Empty string properties are required to satisfy type*/
+ pathname: "/create_post",
+ state: { ...this.crossPostParams },
+ hash: "",
+ key: "",
+ search: "",
+ }}
title={i18n.t("cross_post")}
>
<Icon icon="copy" inline />
}
}
- get crossPostParams(): string {
- let post = this.props.post_view.post;
- let params = `?title=${encodeURIComponent(post.name)}`;
+ get crossPostParams(): PostFormParams {
+ const queryParams: PostFormParams = {};
+ const { name, url } = this.props.post_view.post;
- if (post.url) {
- params += `&url=${encodeURIComponent(post.url)}`;
+ queryParams.name = name;
+
+ if (url) {
+ queryParams.url = url;
}
- let crossPostBody = this.crossPostBody();
+
+ const crossPostBody = this.crossPostBody();
if (crossPostBody) {
- params += `&body=${encodeURIComponent(crossPostBody)}`;
+ queryParams.body = crossPostBody;
}
- return params;
+
+ return queryParams;
}
crossPostBody(): string | undefined {
+import type { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
import {
CommentResponse,
import { WebSocketService } from "../services";
import {
capitalizeFirstLetter,
- choicesConfig,
+ Choice,
commentsToFlatNodes,
- communitySelectName,
communityToChoice,
createCommentLikeRes,
createPostLikeFindRes,
fetchCommunities,
fetchLimit,
fetchUsers,
- isBrowser,
+ getIdFromString,
+ getPageFromString,
+ getQueryParams,
+ getQueryString,
+ getUpdatedSearchId,
myAuth,
numToSI,
- personSelectName,
personToChoice,
- pushNotNull,
+ QueryParams,
restoreScrollPosition,
routeListingTypeToEnum,
routeSearchTypeToEnum,
import { Spinner } from "./common/icon";
import { ListingTypeSelect } from "./common/listing-type-select";
import { Paginator } from "./common/paginator";
+import { SearchableSelect } from "./common/searchable-select";
import { SortSelect } from "./common/sort-select";
import { CommunityLink } from "./community/community-link";
import { PersonListing } from "./person/person-listing";
import { PostListing } from "./post/post-listing";
-var Choices: any;
-if (isBrowser()) {
- Choices = require("choices.js");
-}
-
interface SearchProps {
q?: string;
- type_: SearchType;
+ type: SearchType;
sort: SortType;
listingType: ListingType;
- communityId: number;
- creatorId: number;
+ communityId?: number | null;
+ creatorId?: number | null;
page: number;
}
+type FilterType = "creator" | "community";
+
interface SearchState {
- q?: string;
- type_: SearchType;
- sort: SortType;
- listingType: ListingType;
- communityId: number;
- creatorId: number;
- page: number;
searchResponse?: SearchResponse;
communities: CommunityView[];
creatorDetails?: GetPersonDetailsResponse;
- loading: boolean;
+ searchLoading: boolean;
+ searchCommunitiesLoading: boolean;
+ searchCreatorLoading: boolean;
siteRes: GetSiteResponse;
searchText?: string;
resolveObjectResponse?: ResolveObjectResponse;
-}
-
-interface UrlParams {
- q?: string;
- type_?: SearchType;
- sort?: SortType;
- listingType?: ListingType;
- communityId?: number;
- creatorId?: number;
- page?: number;
+ communitySearchOptions: Choice[];
+ creatorSearchOptions: Choice[];
}
interface Combined {
published: string;
}
+const defaultSearchType = SearchType.All;
+const defaultSortType = SortType.TopAll;
+const defaultListingType = ListingType.All;
+
+const searchTypes = [
+ SearchType.All,
+ SearchType.Comments,
+ SearchType.Posts,
+ SearchType.Communities,
+ SearchType.Users,
+ SearchType.Url,
+];
+
+const getSearchQueryParams = () =>
+ getQueryParams<SearchProps>({
+ q: getSearchQueryFromQuery,
+ type: getSearchTypeFromQuery,
+ sort: getSortTypeFromQuery,
+ listingType: getListingTypeFromQuery,
+ communityId: getIdFromString,
+ creatorId: getIdFromString,
+ page: getPageFromString,
+ });
+
+const getSearchQueryFromQuery = (q?: string): string | undefined =>
+ q ? decodeURIComponent(q) : undefined;
+
+const getSearchTypeFromQuery = (type_?: string): SearchType =>
+ routeSearchTypeToEnum(type_ ?? "", defaultSearchType);
+
+const getSortTypeFromQuery = (sort?: string): SortType =>
+ routeSortTypeToEnum(sort ?? "", defaultSortType);
+
+const getListingTypeFromQuery = (listingType?: string): ListingType =>
+ routeListingTypeToEnum(listingType ?? "", defaultListingType);
+
+const postViewToCombined = (data: PostView): Combined => ({
+ type_: "posts",
+ data,
+ published: data.post.published,
+});
+
+const commentViewToCombined = (data: CommentView): Combined => ({
+ type_: "comments",
+ data,
+ published: data.comment.published,
+});
+
+const communityViewToCombined = (data: CommunityView): Combined => ({
+ type_: "communities",
+ data,
+ published: data.community.published,
+});
+
+const personViewSafeToCombined = (data: PersonViewSafe): Combined => ({
+ type_: "users",
+ data,
+ published: data.person.published,
+});
+
+const Filter = ({
+ filterType,
+ options,
+ onChange,
+ onSearch,
+ value,
+ loading,
+}: {
+ filterType: FilterType;
+ options: Choice[];
+ onSearch: (text: string) => void;
+ onChange: (choice: Choice) => void;
+ value?: number | null;
+ loading: boolean;
+}) => {
+ return (
+ <div className="form-group col-sm-6">
+ <label className="col-form-label" htmlFor={`${filterType}-filter`}>
+ {capitalizeFirstLetter(i18n.t(filterType))}
+ </label>
+ <SearchableSelect
+ id={`${filterType}-filter`}
+ options={[
+ {
+ label: i18n.t("all"),
+ value: "0",
+ },
+ ].concat(options)}
+ value={value ?? 0}
+ onSearch={onSearch}
+ onChange={onChange}
+ loading={loading}
+ />
+ </div>
+ );
+};
+
+const communityListing = ({
+ community,
+ counts: { subscribers },
+}: CommunityView) =>
+ getListing(
+ <CommunityLink community={community} />,
+ subscribers,
+ "number_of_subscribers"
+ );
+
+const personListing = ({ person, counts: { comment_count } }: PersonViewSafe) =>
+ getListing(
+ <PersonListing person={person} showApubName />,
+ comment_count,
+ "number_of_comments"
+ );
+
+const getListing = (
+ listing: JSX.Element,
+ count: number,
+ translationKey: "number_of_comments" | "number_of_subscribers"
+) => (
+ <>
+ <span>{listing}</span>
+ <span>{` - ${i18n.t(translationKey, {
+ count,
+ formattedCount: numToSI(count),
+ })}`}</span>
+ </>
+);
+
export class Search extends Component<any, SearchState> {
private isoData = setIsoData(this.context);
- private communityChoices: any;
- private creatorChoices: any;
private subscription?: Subscription;
state: SearchState = {
- q: Search.getSearchQueryFromProps(this.props.match.params.q),
- type_: Search.getSearchTypeFromProps(this.props.match.params.type),
- sort: Search.getSortTypeFromProps(this.props.match.params.sort),
- listingType: Search.getListingTypeFromProps(
- this.props.match.params.listing_type
- ),
- page: Search.getPageFromProps(this.props.match.params.page),
- searchText: Search.getSearchQueryFromProps(this.props.match.params.q),
- communityId: Search.getCommunityIdFromProps(
- this.props.match.params.community_id
- ),
- creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id),
- loading: false,
+ searchLoading: false,
siteRes: this.isoData.site_res,
communities: [],
+ searchCommunitiesLoading: false,
+ searchCreatorLoading: false,
+ creatorSearchOptions: [],
+ communitySearchOptions: [],
};
- static getSearchQueryFromProps(q?: string): string | undefined {
- return q ? decodeURIComponent(q) : undefined;
- }
-
- static getSearchTypeFromProps(type_: string): SearchType {
- return type_ ? routeSearchTypeToEnum(type_) : SearchType.All;
- }
-
- static getSortTypeFromProps(sort: string): SortType {
- return sort ? routeSortTypeToEnum(sort) : SortType.TopAll;
- }
-
- static getListingTypeFromProps(listingType: string): ListingType {
- return listingType ? routeListingTypeToEnum(listingType) : ListingType.All;
- }
-
- static getCommunityIdFromProps(id: string): number {
- return id ? Number(id) : 0;
- }
-
- static getCreatorIdFromProps(id: string): number {
- return id ? Number(id) : 0;
- }
-
- static getPageFromProps(page: string): number {
- return page ? Number(page) : 1;
- }
-
constructor(props: any, context: any) {
super(props, context);
this.handleSortChange = this.handleSortChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
+ this.handleCommunityFilterChange =
+ this.handleCommunityFilterChange.bind(this);
+ this.handleCreatorFilterChange = this.handleCreatorFilterChange.bind(this);
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
+ const { q } = getSearchQueryParams();
+
+ this.state = {
+ ...this.state,
+ searchText: q,
+ };
+
// Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
- let communityRes = this.isoData.routeData[0] as
+ if (this.isoData.path === this.context.router.route.match.url) {
+ const communityRes = this.isoData.routeData[0] as
| GetCommunityResponse
| undefined;
- let communitiesRes = this.isoData.routeData[1] as
+ const communitiesRes = this.isoData.routeData[1] as
| ListCommunitiesResponse
| undefined;
// This can be single or multiple communities given
communities: communitiesRes.communities,
};
}
-
if (communityRes) {
this.state = {
...this.state,
communities: [communityRes.community_view],
+ communitySearchOptions: [
+ communityToChoice(communityRes.community_view),
+ ],
};
}
+ const creatorRes = this.isoData.routeData[2] as GetPersonDetailsResponse;
+
this.state = {
...this.state,
- creatorDetails: this.isoData.routeData[2] as GetPersonDetailsResponse,
+ creatorDetails: creatorRes,
+ creatorSearchOptions: creatorRes
+ ? [personToChoice(creatorRes.person_view)]
+ : [],
};
- if (this.state.q != "") {
+ if (q !== "") {
this.state = {
...this.state,
searchResponse: this.isoData.routeData[3] as SearchResponse,
resolveObjectResponse: this.isoData
.routeData[4] as ResolveObjectResponse,
- loading: false,
+ searchLoading: false,
};
} else {
this.search();
}
} else {
- this.fetchCommunities();
+ const listCommunitiesForm: ListCommunities = {
+ type_: defaultListingType,
+ sort: defaultSortType,
+ limit: fetchLimit,
+ auth: myAuth(false),
+ };
- if (this.state.q) {
+ WebSocketService.Instance.send(
+ wsClient.listCommunities(listCommunitiesForm)
+ );
+
+ if (q) {
this.search();
}
}
saveScrollPosition(this.context);
}
- componentDidMount() {
- this.setupCommunityFilter();
- this.setupCreatorFilter();
- }
+ static fetchInitialData({
+ client,
+ auth,
+ query: { communityId, creatorId, q, type, sort, listingType, page },
+ }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<any>[] {
+ const promises: Promise<any>[] = [];
- static getDerivedStateFromProps(
- props: any,
- prevState: SearchState
- ): SearchProps {
- return {
- q: Search.getSearchQueryFromProps(props.match.params.q),
- type_:
- prevState.type_ ??
- Search.getSearchTypeFromProps(props.match.params.type),
- sort:
- prevState.sort ?? Search.getSortTypeFromProps(props.match.params.sort),
- listingType:
- prevState.listingType ??
- Search.getListingTypeFromProps(props.match.params.listing_type),
- communityId: Search.getCommunityIdFromProps(
- props.match.params.community_id
- ),
- creatorId: Search.getCreatorIdFromProps(props.match.params.creator_id),
- page: Search.getPageFromProps(props.match.params.page),
- };
- }
-
- fetchCommunities() {
- let listCommunitiesForm: ListCommunities = {
- type_: ListingType.All,
- sort: SortType.TopAll,
- limit: fetchLimit,
- auth: myAuth(false),
- };
- WebSocketService.Instance.send(
- wsClient.listCommunities(listCommunitiesForm)
- );
- }
-
- static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
- let pathSplit = req.path.split("/");
- let promises: Promise<any>[] = [];
- let auth = req.auth;
-
- let communityId = this.getCommunityIdFromProps(pathSplit[11]);
- let community_id = communityId == 0 ? undefined : communityId;
+ const community_id = getIdFromString(communityId);
if (community_id) {
- let getCommunityForm: GetCommunity = {
+ const getCommunityForm: GetCommunity = {
id: community_id,
auth,
};
- promises.push(req.client.getCommunity(getCommunityForm));
+ promises.push(client.getCommunity(getCommunityForm));
promises.push(Promise.resolve());
} else {
- let listCommunitiesForm: ListCommunities = {
- type_: ListingType.All,
- sort: SortType.TopAll,
+ const listCommunitiesForm: ListCommunities = {
+ type_: defaultListingType,
+ sort: defaultSortType,
limit: fetchLimit,
- auth: req.auth,
+ auth,
};
promises.push(Promise.resolve());
- promises.push(req.client.listCommunities(listCommunitiesForm));
+ promises.push(client.listCommunities(listCommunitiesForm));
}
- let creatorId = this.getCreatorIdFromProps(pathSplit[13]);
- let creator_id = creatorId == 0 ? undefined : creatorId;
+ const creator_id = getIdFromString(creatorId);
if (creator_id) {
- let getCreatorForm: GetPersonDetails = {
+ const getCreatorForm: GetPersonDetails = {
person_id: creator_id,
- auth: req.auth,
+ auth,
};
- promises.push(req.client.getPersonDetails(getCreatorForm));
+ promises.push(client.getPersonDetails(getCreatorForm));
} else {
promises.push(Promise.resolve());
}
- let q = this.getSearchQueryFromProps(pathSplit[3]);
+ const query = getSearchQueryFromQuery(q);
- if (q) {
- let form: SearchForm = {
- q,
+ if (query) {
+ const form: SearchForm = {
+ q: query,
community_id,
creator_id,
- type_: this.getSearchTypeFromProps(pathSplit[5]),
- sort: this.getSortTypeFromProps(pathSplit[7]),
- listing_type: this.getListingTypeFromProps(pathSplit[9]),
- page: this.getPageFromProps(pathSplit[15]),
+ type_: getSearchTypeFromQuery(type),
+ sort: getSortTypeFromQuery(sort),
+ listing_type: getListingTypeFromQuery(listingType),
+ page: getIdFromString(page),
limit: fetchLimit,
- auth: req.auth,
+ auth,
};
- let resolveObjectForm: ResolveObject = {
- q,
- auth: req.auth,
+ const resolveObjectForm: ResolveObject = {
+ q: query,
+ auth,
};
- if (form.q != "") {
- promises.push(req.client.search(form));
- promises.push(req.client.resolveObject(resolveObjectForm));
+ if (query !== "") {
+ promises.push(client.search(form));
+ promises.push(client.resolveObject(resolveObjectForm));
} else {
promises.push(Promise.resolve());
promises.push(Promise.resolve());
return promises;
}
- componentDidUpdate(_: any, lastState: SearchState) {
- if (
- lastState.q !== this.state.q ||
- lastState.type_ !== this.state.type_ ||
- lastState.sort !== this.state.sort ||
- lastState.listingType !== this.state.listingType ||
- lastState.communityId !== this.state.communityId ||
- lastState.creatorId !== this.state.creatorId ||
- lastState.page !== this.state.page
- ) {
- if (this.state.q) {
- this.setState({
- loading: true,
- searchText: this.state.q,
- });
- this.search();
- }
- }
- }
-
get documentTitle(): string {
- let siteName = this.state.siteRes.site_view.site.name;
- return this.state.q
- ? `${i18n.t("search")} - ${this.state.q} - ${siteName}`
- : `${i18n.t("search")} - ${siteName}`;
+ const { q } = getSearchQueryParams();
+ const name = this.state.siteRes.site_view.site.name;
+ return `${i18n.t("search")} - ${q ? `${q} - ` : ""}${name}`;
}
render() {
+ const { type, page } = getSearchQueryParams();
+
return (
<div className="container-lg">
<HtmlTags
path={this.context.router.route.match.url}
/>
<h5>{i18n.t("search")}</h5>
- {this.selects()}
- {this.searchForm()}
- {this.state.type_ == SearchType.All && this.all()}
- {this.state.type_ == SearchType.Comments && this.comments()}
- {this.state.type_ == SearchType.Posts && this.posts()}
- {this.state.type_ == SearchType.Communities && this.communities()}
- {this.state.type_ == SearchType.Users && this.users()}
- {this.state.type_ == SearchType.Url && this.posts()}
- {this.resultsCount() == 0 && <span>{i18n.t("no_results")}</span>}
- <Paginator page={this.state.page} onChange={this.handlePageChange} />
+ {this.selects}
+ {this.searchForm}
+ {this.displayResults(type)}
+ {this.resultsCount === 0 && <span>{i18n.t("no_results")}</span>}
+ <Paginator page={page} onChange={this.handlePageChange} />
</div>
);
}
- searchForm() {
+ displayResults(type: SearchType) {
+ switch (type) {
+ case SearchType.All:
+ return this.all;
+ case SearchType.Comments:
+ return this.comments;
+ case SearchType.Posts:
+ case SearchType.Url:
+ return this.posts;
+ case SearchType.Communities:
+ return this.communities;
+ case SearchType.Users:
+ return this.users;
+ default:
+ return <></>;
+ }
+ }
+
+ get searchForm() {
return (
<form
className="form-inline"
minLength={1}
/>
<button type="submit" className="btn btn-secondary mr-2 mb-2">
- {this.state.loading ? <Spinner /> : <span>{i18n.t("search")}</span>}
+ {this.state.searchLoading ? (
+ <Spinner />
+ ) : (
+ <span>{i18n.t("search")}</span>
+ )}
</button>
</form>
);
}
- selects() {
+ get selects() {
+ const { type, listingType, sort, communityId, creatorId } =
+ getSearchQueryParams();
+ const {
+ communitySearchOptions,
+ creatorSearchOptions,
+ searchCommunitiesLoading,
+ searchCreatorLoading,
+ } = this.state;
+
return (
<div className="mb-2">
<select
- value={this.state.type_}
+ value={type}
onChange={linkEvent(this, this.handleTypeChange)}
className="custom-select w-auto mb-2"
aria-label={i18n.t("type")}
<option disabled aria-hidden="true">
{i18n.t("type")}
</option>
- <option value={SearchType.All}>{i18n.t("all")}</option>
- <option value={SearchType.Comments}>{i18n.t("comments")}</option>
- <option value={SearchType.Posts}>{i18n.t("posts")}</option>
- <option value={SearchType.Communities}>
- {i18n.t("communities")}
- </option>
- <option value={SearchType.Users}>{i18n.t("users")}</option>
- <option value={SearchType.Url}>{i18n.t("url")}</option>
+ {searchTypes.map(option => (
+ <option value={option} key={option}>
+ {i18n.t(option.toString().toLowerCase() as NoOptionI18nKeys)}
+ </option>
+ ))}
</select>
<span className="ml-2">
<ListingTypeSelect
- type_={this.state.listingType}
+ type_={listingType}
showLocal={showLocal(this.isoData)}
showSubscribed
onChange={this.handleListingTypeChange}
</span>
<span className="ml-2">
<SortSelect
- sort={this.state.sort}
+ sort={sort}
onChange={this.handleSortChange}
hideHot
hideMostComments
/>
</span>
<div className="form-row">
- {this.state.communities.length > 0 && this.communityFilter()}
- {this.creatorFilter()}
+ {this.state.communities.length > 0 && (
+ <Filter
+ filterType="community"
+ onChange={this.handleCommunityFilterChange}
+ onSearch={this.handleCommunitySearch}
+ options={communitySearchOptions}
+ loading={searchCommunitiesLoading}
+ value={communityId}
+ />
+ )}
+ <Filter
+ filterType="creator"
+ onChange={this.handleCreatorFilterChange}
+ onSearch={this.handleCreatorSearch}
+ options={creatorSearchOptions}
+ loading={searchCreatorLoading}
+ value={creatorId}
+ />
</div>
</div>
);
}
- postViewToCombined(postView: PostView): Combined {
- return {
- type_: "posts",
- data: postView,
- published: postView.post.published,
- };
- }
-
- commentViewToCombined(commentView: CommentView): Combined {
- return {
- type_: "comments",
- data: commentView,
- published: commentView.comment.published,
- };
- }
-
- communityViewToCombined(communityView: CommunityView): Combined {
- return {
- type_: "communities",
- data: communityView,
- published: communityView.community.published,
- };
- }
-
- personViewSafeToCombined(personViewSafe: PersonViewSafe): Combined {
- return {
- type_: "users",
- data: personViewSafe,
- published: personViewSafe.person.published,
- };
- }
-
buildCombined(): Combined[] {
- let combined: Combined[] = [];
+ const combined: Combined[] = [];
+ const { resolveObjectResponse, searchResponse } = this.state;
- let resolveRes = this.state.resolveObjectResponse;
// Push the possible resolve / federated objects first
- if (resolveRes) {
- let resolveComment = resolveRes.comment;
- if (resolveComment) {
- combined.push(this.commentViewToCombined(resolveComment));
+ if (resolveObjectResponse) {
+ const { comment, post, community, person } = resolveObjectResponse;
+
+ if (comment) {
+ combined.push(commentViewToCombined(comment));
}
- let resolvePost = resolveRes.post;
- if (resolvePost) {
- combined.push(this.postViewToCombined(resolvePost));
+ if (post) {
+ combined.push(postViewToCombined(post));
}
- let resolveCommunity = resolveRes.community;
- if (resolveCommunity) {
- combined.push(this.communityViewToCombined(resolveCommunity));
+ if (community) {
+ combined.push(communityViewToCombined(community));
}
- let resolveUser = resolveRes.person;
- if (resolveUser) {
- combined.push(this.personViewSafeToCombined(resolveUser));
+ if (person) {
+ combined.push(personViewSafeToCombined(person));
}
}
// Push the search results
- let searchRes = this.state.searchResponse;
- if (searchRes) {
- pushNotNull(
- combined,
- searchRes.comments?.map(e => this.commentViewToCombined(e))
- );
- pushNotNull(
- combined,
- searchRes.posts?.map(e => this.postViewToCombined(e))
- );
- pushNotNull(
- combined,
- searchRes.communities?.map(e => this.communityViewToCombined(e))
- );
- pushNotNull(
- combined,
- searchRes.users?.map(e => this.personViewSafeToCombined(e))
+ if (searchResponse) {
+ const { comments, posts, communities, users } = searchResponse;
+
+ combined.push(
+ ...[
+ ...(comments?.map(commentViewToCombined) ?? []),
+ ...(posts?.map(postViewToCombined) ?? []),
+ ...(communities?.map(communityViewToCombined) ?? []),
+ ...(users?.map(personViewSafeToCombined) ?? []),
+ ]
);
}
+ const { sort } = getSearchQueryParams();
+
// Sort it
- if (this.state.sort == SortType.New) {
+ if (sort === SortType.New) {
combined.sort((a, b) => b.published.localeCompare(a.published));
} else {
combined.sort(
(a.data as PersonViewSafe).counts.comment_score)
);
}
+
return combined;
}
- all() {
- let combined = this.buildCombined();
+ get all() {
+ const combined = this.buildCombined();
+
return (
<div>
{combined.map(i => (
<div key={i.published} className="row">
<div className="col-12">
- {i.type_ == "posts" && (
+ {i.type_ === "posts" && (
<PostListing
key={(i.data as PostView).post.id}
post_view={i.data as PostView}
viewOnly
/>
)}
- {i.type_ == "comments" && (
+ {i.type_ === "comments" && (
<CommentNodes
key={(i.data as CommentView).comment.id}
nodes={[
siteLanguages={this.state.siteRes.discussion_languages}
/>
)}
- {i.type_ == "communities" && (
- <div>{this.communityListing(i.data as CommunityView)}</div>
+ {i.type_ === "communities" && (
+ <div>{communityListing(i.data as CommunityView)}</div>
)}
- {i.type_ == "users" && (
- <div>{this.personListing(i.data as PersonViewSafe)}</div>
+ {i.type_ === "users" && (
+ <div>{personListing(i.data as PersonViewSafe)}</div>
)}
</div>
</div>
);
}
- comments() {
- let comments: CommentView[] = [];
- pushNotNull(comments, this.state.resolveObjectResponse?.comment);
- pushNotNull(comments, this.state.searchResponse?.comments);
+ get comments() {
+ const { searchResponse, resolveObjectResponse, siteRes } = this.state;
+ const comments = searchResponse?.comments ?? [];
+
+ if (resolveObjectResponse?.comment) {
+ comments.unshift(resolveObjectResponse?.comment);
+ }
return (
<CommentNodes
viewOnly
locked
noIndent
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
+ enableDownvotes={enableDownvotes(siteRes)}
+ allLanguages={siteRes.all_languages}
+ siteLanguages={siteRes.discussion_languages}
/>
);
}
- posts() {
- let posts: PostView[] = [];
+ get posts() {
+ const { searchResponse, resolveObjectResponse, siteRes } = this.state;
+ const posts = searchResponse?.posts ?? [];
- pushNotNull(posts, this.state.resolveObjectResponse?.post);
- pushNotNull(posts, this.state.searchResponse?.posts);
+ if (resolveObjectResponse?.post) {
+ posts.unshift(resolveObjectResponse.post);
+ }
return (
<>
<PostListing
post_view={pv}
showCommunity
- enableDownvotes={enableDownvotes(this.state.siteRes)}
- enableNsfw={enableNsfw(this.state.siteRes)}
- allLanguages={this.state.siteRes.all_languages}
- siteLanguages={this.state.siteRes.discussion_languages}
+ enableDownvotes={enableDownvotes(siteRes)}
+ enableNsfw={enableNsfw(siteRes)}
+ allLanguages={siteRes.all_languages}
+ siteLanguages={siteRes.discussion_languages}
viewOnly
/>
</div>
);
}
- communities() {
- let communities: CommunityView[] = [];
+ get communities() {
+ const { searchResponse, resolveObjectResponse } = this.state;
+ const communities = searchResponse?.communities ?? [];
- pushNotNull(communities, this.state.resolveObjectResponse?.community);
- pushNotNull(communities, this.state.searchResponse?.communities);
+ if (resolveObjectResponse?.community) {
+ communities.unshift(resolveObjectResponse.community);
+ }
return (
<>
{communities.map(cv => (
<div key={cv.community.id} className="row">
- <div className="col-12">{this.communityListing(cv)}</div>
+ <div className="col-12">{communityListing(cv)}</div>
</div>
))}
</>
);
}
- users() {
- let users: PersonViewSafe[] = [];
+ get users() {
+ const { searchResponse, resolveObjectResponse } = this.state;
+ const users = searchResponse?.users ?? [];
- pushNotNull(users, this.state.resolveObjectResponse?.person);
- pushNotNull(users, this.state.searchResponse?.users);
+ if (resolveObjectResponse?.person) {
+ users.unshift(resolveObjectResponse.person);
+ }
return (
<>
{users.map(pvs => (
<div key={pvs.person.id} className="row">
- <div className="col-12">{this.personListing(pvs)}</div>
+ <div className="col-12">{personListing(pvs)}</div>
</div>
))}
</>
);
}
- communityListing(community_view: CommunityView) {
- return (
- <>
- <span>
- <CommunityLink community={community_view.community} />
- </span>
- <span>{` -
- ${i18n.t("number_of_subscribers", {
- count: community_view.counts.subscribers,
- formattedCount: numToSI(community_view.counts.subscribers),
- })}
- `}</span>
- </>
- );
- }
-
- personListing(person_view: PersonViewSafe) {
- return (
- <>
- <span>
- <PersonListing person={person_view.person} showApubName />
- </span>
- <span>{` - ${i18n.t("number_of_comments", {
- count: person_view.counts.comment_count,
- formattedCount: numToSI(person_view.counts.comment_count),
- })}`}</span>
- </>
- );
- }
-
- communityFilter() {
- return (
- <div className="form-group col-sm-6">
- <label className="col-form-label" htmlFor="community-filter">
- {i18n.t("community")}
- </label>
- <div>
- <select
- className="form-control"
- id="community-filter"
- value={this.state.communityId}
- >
- <option value="0">{i18n.t("all")}</option>
- {this.state.communities.map(cv => (
- <option key={cv.community.id} value={cv.community.id}>
- {communitySelectName(cv)}
- </option>
- ))}
- </select>
- </div>
- </div>
- );
- }
-
- creatorFilter() {
- let creatorPv = this.state.creatorDetails?.person_view;
- return (
- <div className="form-group col-sm-6">
- <label className="col-form-label" htmlFor="creator-filter">
- {capitalizeFirstLetter(i18n.t("creator"))}
- </label>
- <div>
- <select
- className="form-control"
- id="creator-filter"
- value={this.state.creatorId}
- >
- <option value="0">{i18n.t("all")}</option>
- {creatorPv && (
- <option value={creatorPv.person.id}>
- {personSelectName(creatorPv)}
- </option>
- )}
- </select>
- </div>
- </div>
- );
- }
-
- resultsCount(): number {
- let r = this.state.searchResponse;
+ get resultsCount(): number {
+ const { searchResponse: r, resolveObjectResponse: resolveRes } = this.state;
- let searchCount = r
- ? r.posts?.length +
- r.comments?.length +
- r.communities?.length +
- r.users?.length
+ const searchCount = r
+ ? r.posts.length +
+ r.comments.length +
+ r.communities.length +
+ r.users.length
: 0;
- let resolveRes = this.state.resolveObjectResponse;
- let resObjCount = resolveRes
+ const resObjCount = resolveRes
? resolveRes.post ||
resolveRes.person ||
resolveRes.community ||
return resObjCount + searchCount;
}
- handlePageChange(page: number) {
- this.updateUrl({ page });
- }
-
search() {
- let community_id =
- this.state.communityId == 0 ? undefined : this.state.communityId;
- let creator_id =
- this.state.creatorId == 0 ? undefined : this.state.creatorId;
-
- let auth = myAuth(false);
- if (this.state.q && this.state.q != "") {
- let form: SearchForm = {
- q: this.state.q,
- community_id,
- creator_id,
- type_: this.state.type_,
- sort: this.state.sort,
- listing_type: this.state.listingType,
- page: this.state.page,
+ const auth = myAuth(false);
+ const { searchText: q } = this.state;
+ const { communityId, creatorId, type, sort, listingType, page } =
+ getSearchQueryParams();
+
+ if (q && q !== "") {
+ const form: SearchForm = {
+ q,
+ community_id: communityId ?? undefined,
+ creator_id: creatorId ?? undefined,
+ type_: type,
+ sort,
+ listing_type: listingType,
+ page,
limit: fetchLimit,
auth,
};
- let resolveObjectForm: ResolveObject = {
- q: this.state.q,
+ const resolveObjectForm: ResolveObject = {
+ q,
auth,
};
this.setState({
searchResponse: undefined,
resolveObjectResponse: undefined,
- loading: true,
+ searchLoading: true,
});
+
WebSocketService.Instance.send(wsClient.search(form));
WebSocketService.Instance.send(wsClient.resolveObject(resolveObjectForm));
}
}
- setupCommunityFilter() {
- if (isBrowser()) {
- let selectId: any = document.getElementById("community-filter");
- if (selectId) {
- this.communityChoices = new Choices(selectId, choicesConfig);
- this.communityChoices.passedElement.element.addEventListener(
- "choice",
- (e: any) => {
- this.handleCommunityFilterChange(Number(e.detail.choice.value));
- },
- false
- );
- this.communityChoices.passedElement.element.addEventListener(
- "search",
- debounce(async (e: any) => {
- try {
- let communities = (await fetchCommunities(e.detail.value))
- .communities;
- let choices = communities.map(cv => communityToChoice(cv));
- choices.unshift({ value: "0", label: i18n.t("all") });
- this.communityChoices.setChoices(choices, "value", "label", true);
- } catch (err) {
- console.error(err);
- }
- }),
- false
- );
- }
+ handleCreatorSearch = debounce(async (text: string) => {
+ const { creatorId } = getSearchQueryParams();
+ const { creatorSearchOptions } = this.state;
+ this.setState({
+ searchCreatorLoading: true,
+ });
+
+ const newOptions: Choice[] = [];
+
+ const selectedChoice = creatorSearchOptions.find(
+ choice => getIdFromString(choice.value) === creatorId
+ );
+
+ if (selectedChoice) {
+ newOptions.push(selectedChoice);
}
- }
- setupCreatorFilter() {
- if (isBrowser()) {
- let selectId: any = document.getElementById("creator-filter");
- if (selectId) {
- this.creatorChoices = new Choices(selectId, choicesConfig);
- this.creatorChoices.passedElement.element.addEventListener(
- "choice",
- (e: any) => {
- this.handleCreatorFilterChange(Number(e.detail.choice.value));
- },
- false
- );
- this.creatorChoices.passedElement.element.addEventListener(
- "search",
- debounce(async (e: any) => {
- try {
- let creators = (await fetchUsers(e.detail.value)).users;
- let choices = creators.map(pvs => personToChoice(pvs));
- choices.unshift({ value: "0", label: i18n.t("all") });
- this.creatorChoices.setChoices(choices, "value", "label", true);
- } catch (err) {
- console.log(err);
- }
- }),
- false
- );
- }
+ if (text.length > 0) {
+ newOptions.push(...(await fetchUsers(text)).users.map(personToChoice));
}
- }
- handleSortChange(val: SortType) {
- const updateObj = { sort: val, page: 1 };
- this.setState(updateObj);
- this.updateUrl(updateObj);
+ this.setState({
+ searchCreatorLoading: false,
+ creatorSearchOptions: newOptions,
+ });
+ });
+
+ handleCommunitySearch = debounce(async (text: string) => {
+ const { communityId } = getSearchQueryParams();
+ const { communitySearchOptions } = this.state;
+ this.setState({
+ searchCommunitiesLoading: true,
+ });
+
+ const newOptions: Choice[] = [];
+
+ const selectedChoice = communitySearchOptions.find(
+ choice => getIdFromString(choice.value) === communityId
+ );
+
+ if (selectedChoice) {
+ newOptions.push(selectedChoice);
+ }
+
+ if (text.length > 0) {
+ newOptions.push(
+ ...(await fetchCommunities(text)).communities.map(communityToChoice)
+ );
+ }
+
+ this.setState({
+ searchCommunitiesLoading: false,
+ communitySearchOptions: newOptions,
+ });
+ });
+
+ handleSortChange(sort: SortType) {
+ this.updateUrl({ sort, page: 1 });
}
handleTypeChange(i: Search, event: any) {
- const updateObj = {
- type_: SearchType[event.target.value],
+ const type = SearchType[event.target.value];
+
+ i.updateUrl({
+ type,
page: 1,
- };
- i.setState(updateObj);
- i.updateUrl(updateObj);
+ });
}
- handleListingTypeChange(val: ListingType) {
- const updateObj = {
- listingType: val,
+ handlePageChange(page: number) {
+ this.updateUrl({ page });
+ }
+
+ handleListingTypeChange(listingType: ListingType) {
+ this.updateUrl({
+ listingType,
page: 1,
- };
- this.setState(updateObj);
- this.updateUrl(updateObj);
+ });
}
- handleCommunityFilterChange(communityId: number) {
+ handleCommunityFilterChange({ value }: Choice) {
this.updateUrl({
- communityId,
+ communityId: getIdFromString(value) ?? null,
page: 1,
});
}
- handleCreatorFilterChange(creatorId: number) {
+ handleCreatorFilterChange({ value }: Choice) {
this.updateUrl({
- creatorId,
+ creatorId: getIdFromString(value) ?? null,
page: 1,
});
}
handleSearchSubmit(i: Search, event: any) {
event.preventDefault();
+
i.updateUrl({
q: i.state.searchText,
- type_: i.state.type_,
- listingType: i.state.listingType,
- communityId: i.state.communityId,
- creatorId: i.state.creatorId,
- sort: i.state.sort,
- page: i.state.page,
+ page: 1,
});
}
i.setState({ searchText: event.target.value });
}
- updateUrl(paramUpdates: UrlParams) {
- const qStr = paramUpdates.q || this.state.q;
- const qStrEncoded = encodeURIComponent(qStr || "");
- const typeStr = paramUpdates.type_ || this.state.type_;
- const listingTypeStr = paramUpdates.listingType || this.state.listingType;
- const sortStr = paramUpdates.sort || this.state.sort;
- const communityId =
- paramUpdates.communityId == 0
- ? 0
- : paramUpdates.communityId || this.state.communityId;
- const creatorId =
- paramUpdates.creatorId == 0
- ? 0
- : paramUpdates.creatorId || this.state.creatorId;
- const page = paramUpdates.page || this.state.page;
- this.props.history.push(
- `/search/q/${qStrEncoded}/type/${typeStr}/sort/${sortStr}/listing_type/${listingTypeStr}/community_id/${communityId}/creator_id/${creatorId}/page/${page}`
- );
+ updateUrl({
+ q,
+ type,
+ listingType,
+ sort,
+ communityId,
+ creatorId,
+ page,
+ }: Partial<SearchProps>) {
+ const {
+ q: urlQ,
+ type: urlType,
+ listingType: urlListingType,
+ communityId: urlCommunityId,
+ sort: urlSort,
+ creatorId: urlCreatorId,
+ page: urlPage,
+ } = getSearchQueryParams();
+
+ let query = q ?? this.state.searchText ?? urlQ;
+
+ if (query && query.length > 0) {
+ query = encodeURIComponent(query);
+ }
+
+ const queryParams: QueryParams<SearchProps> = {
+ q: query,
+ type: type ?? urlType,
+ listingType: listingType ?? urlListingType,
+ communityId: getUpdatedSearchId(communityId, urlCommunityId),
+ creatorId: getUpdatedSearchId(creatorId, urlCreatorId),
+ page: (page ?? urlPage).toString(),
+ sort: sort ?? urlSort,
+ };
+
+ this.props.history.push(`/search${getQueryString(queryParams)}`);
+
+ this.search();
}
parseMessage(msg: any) {
console.log(msg);
- let op = wsUserOp(msg);
+ const op = wsUserOp(msg);
if (msg.error) {
- if (msg.error == "couldnt_find_object") {
+ if (msg.error === "couldnt_find_object") {
this.setState({
resolveObjectResponse: {},
});
this.checkFinishedLoading();
} else {
toast(i18n.t(msg.error), "danger");
- return;
}
- } else if (op == UserOperation.Search) {
- let data = wsJsonToRes<SearchResponse>(msg);
- this.setState({ searchResponse: data });
- window.scrollTo(0, 0);
- this.checkFinishedLoading();
- restoreScrollPosition(this.context);
- } else if (op == UserOperation.CreateCommentLike) {
- let data = wsJsonToRes<CommentResponse>(msg);
- createCommentLikeRes(
- data.comment_view,
- this.state.searchResponse?.comments
- );
- this.setState(this.state);
- } else if (op == UserOperation.CreatePostLike) {
- let data = wsJsonToRes<PostResponse>(msg);
- createPostLikeFindRes(data.post_view, this.state.searchResponse?.posts);
- this.setState(this.state);
- } else if (op == UserOperation.ListCommunities) {
- let data = wsJsonToRes<ListCommunitiesResponse>(msg);
- this.setState({ communities: data.communities });
- this.setupCommunityFilter();
- } else if (op == UserOperation.ResolveObject) {
- let data = wsJsonToRes<ResolveObjectResponse>(msg);
- this.setState({ resolveObjectResponse: data });
- this.checkFinishedLoading();
+ } else {
+ switch (op) {
+ case UserOperation.Search: {
+ const searchResponse = wsJsonToRes<SearchResponse>(msg);
+ this.setState({ searchResponse });
+ window.scrollTo(0, 0);
+ this.checkFinishedLoading();
+ restoreScrollPosition(this.context);
+
+ break;
+ }
+
+ case UserOperation.CreateCommentLike: {
+ const { comment_view } = wsJsonToRes<CommentResponse>(msg);
+ createCommentLikeRes(
+ comment_view,
+ this.state.searchResponse?.comments
+ );
+
+ break;
+ }
+
+ case UserOperation.CreatePostLike: {
+ const { post_view } = wsJsonToRes<PostResponse>(msg);
+ createPostLikeFindRes(post_view, this.state.searchResponse?.posts);
+
+ break;
+ }
+
+ case UserOperation.ListCommunities: {
+ const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
+ this.setState({ communities });
+
+ break;
+ }
+
+ case UserOperation.ResolveObject: {
+ const resolveObjectResponse = wsJsonToRes<ResolveObjectResponse>(msg);
+ this.setState({ resolveObjectResponse });
+ this.checkFinishedLoading();
+
+ break;
+ }
+ }
}
}
checkFinishedLoading() {
if (this.state.searchResponse && this.state.resolveObjectResponse) {
- this.setState({ loading: false });
+ this.setState({ searchLoading: false });
}
}
}
import { GetSiteResponse, LemmyHttp } from "lemmy-js-client";
+import type { ParsedQs } from "qs";
/**
* This contains serialized data, it needs to be deserialized before use.
}
}
-export interface InitialFetchRequest {
+export interface InitialFetchRequest<T extends ParsedQs = ParsedQs> {
auth?: string;
client: LemmyHttp;
path: string;
+ query: T;
+ site: GetSiteResponse;
}
export interface PostFormParams {
name?: string;
url?: string;
body?: string;
- nameOrId?: string | number;
}
export enum CommentViewType {
}
export enum PersonDetailsView {
- Overview,
- Comments,
- Posts,
- Saved,
+ Overview = "Overview",
+ Comments = "Comments",
+ Posts = "Posts",
+ Saved = "Saved",
}
export enum PurgeType {
-import { Inferno } from "inferno";
import { IRouteProps } from "inferno-router/dist/Route";
import { Communities } from "./components/community/communities";
import { Community } from "./components/community/community";
interface IRoutePropsWithFetch extends IRouteProps {
// TODO Make sure this one is good.
- component: Inferno.ComponentClass;
fetchInitialData?(req: InitialFetchRequest): Promise<any>[];
}
export const routes: IRoutePropsWithFetch[] = [
{
path: `/`,
- exact: true,
component: Home,
- fetchInitialData: req => Home.fetchInitialData(req),
- },
- {
- path: `/home/data_type/:data_type/listing_type/:listing_type/sort/:sort/page/:page`,
- component: Home,
- fetchInitialData: req => Home.fetchInitialData(req),
+ fetchInitialData: Home.fetchInitialData,
+ exact: true,
},
{
path: `/login`,
{
path: `/create_post`,
component: CreatePost,
- fetchInitialData: req => CreatePost.fetchInitialData(req),
+ fetchInitialData: CreatePost.fetchInitialData,
},
{
path: `/create_community`,
component: CreateCommunity,
},
{
- path: `/create_private_message/recipient/:recipient_id`,
+ path: `/create_private_message/:recipient_id`,
component: CreatePrivateMessage,
- fetchInitialData: req => CreatePrivateMessage.fetchInitialData(req),
- },
- {
- path: `/communities/listing_type/:listing_type/page/:page`,
- component: Communities,
- fetchInitialData: req => Communities.fetchInitialData(req),
+ fetchInitialData: CreatePrivateMessage.fetchInitialData,
},
{
path: `/communities`,
component: Communities,
- fetchInitialData: req => Communities.fetchInitialData(req),
+ fetchInitialData: Communities.fetchInitialData,
},
{
path: `/post/:post_id`,
component: Post,
- fetchInitialData: req => Post.fetchInitialData(req),
+ fetchInitialData: Post.fetchInitialData,
},
{
path: `/comment/:comment_id`,
component: Post,
- fetchInitialData: req => Post.fetchInitialData(req),
- },
- {
- path: `/c/:name/data_type/:data_type/sort/:sort/page/:page`,
- component: Community,
- fetchInitialData: req => Community.fetchInitialData(req),
+ fetchInitialData: Post.fetchInitialData,
},
{
path: `/c/:name`,
component: Community,
- fetchInitialData: req => Community.fetchInitialData(req),
- },
- {
- path: `/u/:username/view/:view/sort/:sort/page/:page`,
- component: Profile,
- fetchInitialData: req => Profile.fetchInitialData(req),
+ fetchInitialData: Community.fetchInitialData,
},
{
path: `/u/:username`,
component: Profile,
- fetchInitialData: req => Profile.fetchInitialData(req),
+ fetchInitialData: Profile.fetchInitialData,
},
{
path: `/inbox`,
component: Inbox,
- fetchInitialData: req => Inbox.fetchInitialData(req),
+ fetchInitialData: Inbox.fetchInitialData,
},
{
path: `/settings`,
component: Settings,
},
{
- path: `/modlog/community/:community_id`,
+ path: `/modlog`,
component: Modlog,
- fetchInitialData: req => Modlog.fetchInitialData(req),
+ fetchInitialData: Modlog.fetchInitialData,
},
{
- path: `/modlog`,
+ path: `/modlog/:communityId`,
component: Modlog,
- fetchInitialData: req => Modlog.fetchInitialData(req),
+ fetchInitialData: Modlog.fetchInitialData,
},
{ path: `/setup`, component: Setup },
{
path: `/admin`,
component: AdminSettings,
- fetchInitialData: req => AdminSettings.fetchInitialData(req),
+ fetchInitialData: AdminSettings.fetchInitialData,
},
{
path: `/reports`,
component: Reports,
- fetchInitialData: req => Reports.fetchInitialData(req),
+ fetchInitialData: Reports.fetchInitialData,
},
{
path: `/registration_applications`,
component: RegistrationApplications,
- fetchInitialData: req => RegistrationApplications.fetchInitialData(req),
- },
- {
- path: `/search/q/:q/type/:type/sort/:sort/listing_type/:listing_type/community_id/:community_id/creator_id/:creator_id/page/:page`,
- component: Search,
- fetchInitialData: req => Search.fetchInitialData(req),
+ fetchInitialData: RegistrationApplications.fetchInitialData,
},
{
path: `/search`,
component: Search,
- fetchInitialData: req => Search.fetchInitialData(req),
+ fetchInitialData: Search.fetchInitialData,
},
{
path: `/password_change/:token`,
export const relTags = "noopener nofollow";
+export const emDash = "\u2014";
+
export type ThemeColor =
| "primary"
| "secondary"
return alphabet.charAt(Math.floor(Math.random() * alphabet.length));
}
+export function getIdFromString(id?: string): number | undefined {
+ return id && id !== "0" && !Number.isNaN(Number(id)) ? Number(id) : undefined;
+}
+
+export function getPageFromString(page?: string): number {
+ return page && !Number.isNaN(Number(page)) ? Number(page) : 1;
+}
+
export function randomStr(
idDesiredLength = 20,
alphabet = DEFAULT_ALPHABET
return str.charAt(0).toUpperCase() + str.slice(1);
}
-export function routeSortTypeToEnum(sort: string): SortType {
- return SortType[sort];
+export function routeSortTypeToEnum(
+ sort: string,
+ defaultValue: SortType
+): SortType {
+ return SortType[sort] ?? defaultValue;
}
export function listingTypeFromNum(type_: number): ListingType {
return Object.values(SortType)[type_];
}
-export function routeListingTypeToEnum(type: string): ListingType {
- return ListingType[type];
+export function routeListingTypeToEnum(
+ type: string,
+ defaultValue: ListingType
+): ListingType {
+ return ListingType[type] ?? defaultValue;
}
-export function routeDataTypeToEnum(type: string): DataType {
- return DataType[capitalizeFirstLetter(type)];
+export function routeDataTypeToEnum(
+ type: string,
+ defaultValue: DataType
+): DataType {
+ return DataType[type] ?? defaultValue;
}
-export function routeSearchTypeToEnum(type: string): SearchType {
- return SearchType[type];
+export function routeSearchTypeToEnum(
+ type: string,
+ defaultValue: SearchType
+): SearchType {
+ return SearchType[type] ?? defaultValue;
}
export async function getSiteMetadata(url: string) {
return client.getSiteMetadata(form);
}
-export function debounce(func: any, wait = 1000, immediate = false) {
+export function getDataTypeString(dt: DataType) {
+ return dt === DataType.Post ? "Post" : "Comment";
+}
+
+export function debounce<T extends any[], R>(
+ func: (...e: T) => R,
+ wait = 1000,
+ immediate = false
+) {
// 'private' variable for instance
// The returned function will be able to reference this due to closure.
// Each call to the returned function will share this common timer.
- let timeout: any;
+ let timeout: NodeJS.Timeout | null;
// Calling debounce returns a new anonymous function
return function () {
// reference the context and args for the setTimeout function
- var args = arguments;
+ const args = arguments;
// Should the function be called now? If immediate is true
// and not already in a timeout then the answer is: Yes
- var callNow = immediate && !timeout;
+ const callNow = immediate && !timeout;
- // This is the basic debounce behaviour where you can call this
+ // This is the basic debounce behavior where you can call this
// function several times, but it will only execute once
// [before or after imposing a delay].
// Each time the returned function is called, the timer starts over.
- clearTimeout(timeout);
+ clearTimeout(timeout ?? undefined);
// Set the new timeout
timeout = setTimeout(function () {
// Immediate mode and no wait timer? Execute the function..
if (callNow) func.apply(this, args);
- };
+ } as (...e: T) => R;
}
export function getLanguages(
return communities;
}
-export function getListingTypeFromProps(
- props: any,
- defaultListingType: ListingType,
- myUserInfo = UserService.Instance.myUserInfo
-): ListingType {
- let myLt = myUserInfo?.local_user_view.local_user.default_listing_type;
- return props.match.params.listing_type
- ? routeListingTypeToEnum(props.match.params.listing_type)
- : myLt
- ? Object.values(ListingType)[myLt]
- : defaultListingType;
-}
-
-export function getListingTypeFromPropsNoDefault(props: any): ListingType {
- return props.match.params.listing_type
- ? routeListingTypeToEnum(props.match.params.listing_type)
- : ListingType.Local;
-}
-
-export function getDataTypeFromProps(props: any): DataType {
- return props.match.params.data_type
- ? routeDataTypeToEnum(props.match.params.data_type)
- : DataType.Post;
-}
-
-export function getSortTypeFromProps(
- props: any,
- myUserInfo = UserService.Instance.myUserInfo
-): SortType {
- let mySortType = myUserInfo?.local_user_view.local_user.default_sort_type;
- return props.match.params.sort
- ? routeSortTypeToEnum(props.match.params.sort)
- : mySortType
- ? Object.values(SortType)[mySortType]
- : SortType.Active;
-}
-
-export function getPageFromProps(props: any): number {
- return props.match.params.page ? Number(props.match.params.page) : 1;
-}
-
export function getRecipientIdFromProps(props: any): number {
return props.match.params.recipient_id
? Number(props.match.params.recipient_id)
return id ? Number(id) : undefined;
}
-export function getUsernameFromProps(props: any): string {
- return props.match.params.username;
-}
-
export function editCommentRes(data: CommentView, comments?: CommentView[]) {
let found = comments?.find(c => c.comment.id == data.comment.id);
if (found) {
return linked ? linked.length > 0 : false;
}
-export interface ChoicesValue {
+export interface Choice {
value: string;
label: string;
+ disabled?: boolean;
+}
+
+export function getUpdatedSearchId(id?: number | null, urlId?: number | null) {
+ return id === null
+ ? undefined
+ : ((id ?? urlId) === 0 ? undefined : id ?? urlId)?.toString();
}
-export function communityToChoice(cv: CommunityView): ChoicesValue {
- let choice: ChoicesValue = {
+export function communityToChoice(cv: CommunityView): Choice {
+ return {
value: cv.community.id.toString(),
label: communitySelectName(cv),
};
- return choice;
}
-export function personToChoice(pvs: PersonViewSafe): ChoicesValue {
- let choice: ChoicesValue = {
+export function personToChoice(pvs: PersonViewSafe): Choice {
+ return {
value: pvs.person.id.toString(),
label: personSelectName(pvs),
};
- return choice;
}
export async function fetchCommunities(q: string) {
return client.search(form);
}
-export const choicesConfig = {
- shouldSort: false,
- searchResultLimit: fetchLimit,
- classNames: {
- containerOuter: "choices custom-select px-0",
- containerInner:
- "choices__inner bg-secondary border-0 py-0 modlog-choices-font-size",
- input: "form-control",
- inputCloned: "choices__input--cloned",
- list: "choices__list",
- listItems: "choices__list--multiple",
- listSingle: "choices__list--single py-0",
- listDropdown: "choices__list--dropdown",
- item: "choices__item bg-secondary",
- 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",
- },
-};
-
export function communitySelectName(cv: CommunityView): string {
return cv.community.local
? cv.community.title
: `${hostname(cv.community.actor_id)}/${cv.community.title}`;
}
-export function personSelectName(pvs: PersonViewSafe): string {
- let pName = pvs.person.display_name ?? pvs.person.name;
- return pvs.person.local ? pName : `${hostname(pvs.person.actor_id)}/${pName}`;
+export function personSelectName({
+ person: { display_name, name, local, actor_id },
+}: PersonViewSafe): string {
+ const pName = display_name ?? name;
+ return local ? pName : `${hostname(actor_id)}/${pName}`;
}
export function initializeSite(site: GetSiteResponse) {
}
}
-export function pushNotNull(array: any[], new_item?: any) {
- if (new_item) {
- array.push(...new_item);
- }
-}
-
export function myAuth(throwErr = true): string | undefined {
return UserService.Instance.auth(throwErr);
}
}
export function postToCommentSortType(sort: SortType): CommentSortType {
- if ([SortType.Active, SortType.Hot].includes(sort)) {
- return CommentSortType.Hot;
- } else if ([SortType.New, SortType.NewComments].includes(sort)) {
- return CommentSortType.New;
- } else if (sort == SortType.Old) {
- return CommentSortType.Old;
- } else {
- return CommentSortType.Top;
+ switch (sort) {
+ case SortType.Active:
+ case SortType.Hot:
+ return CommentSortType.Hot;
+ case SortType.New:
+ case SortType.NewComments:
+ return CommentSortType.New;
+ case SortType.Old:
+ return CommentSortType.Old;
+ default:
+ return CommentSortType.Top;
}
}
siteRes: GetSiteResponse,
myUserInfo = UserService.Instance.myUserInfo
): boolean {
- let adminOnly = siteRes.site_view.local_site.community_creation_admin_only;
+ const adminOnly = siteRes.site_view.local_site.community_creation_admin_only;
+ // TODO: Make this check if user is logged on as well
return !adminOnly || amAdmin(myUserInfo);
}
(acc[predicate(value, index, array)] ||= []).push(value);
return acc;
}, {} as { [key: string]: T[] });
+
+export type QueryParams<T extends Record<string, any>> = {
+ [key in keyof T]?: string;
+};
+
+export function getQueryParams<T extends Record<string, any>>(processors: {
+ [K in keyof T]: (param: string) => T[K];
+}): T {
+ if (isBrowser()) {
+ const searchParams = new URLSearchParams(window.location.search);
+
+ return Array.from(Object.entries(processors)).reduce(
+ (acc, [key, process]) => ({
+ ...acc,
+ [key]: process(searchParams.get(key)),
+ }),
+ {} as T
+ );
+ }
+
+ return {} as T;
+}
+
+export function getQueryString<T extends Record<string, string | undefined>>(
+ obj: T
+) {
+ return Object.entries(obj)
+ .filter(([, val]) => val !== undefined && val !== null)
+ .reduce(
+ (acc, [key, val], index) => `${acc}${index > 0 ? "&" : ""}${key}=${val}`,
+ "?"
+ );
+}
dependencies:
regenerator-runtime "^0.13.11"
-"@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
-"@types/node-fetch@^2.6.2":
- version "2.6.2"
- resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da"
- integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==
- dependencies:
- "@types/node" "*"
- form-data "^3.0.0"
-
"@types/node@*":
version "18.7.23"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.23.tgz#75c580983846181ebe5f4abc40fe9dfb2d65665f"
resolved "https://registry.yarnpkg.com/check-password-strength/-/check-password-strength-2.0.7.tgz#d8fd6c1a274267c7ddd9cd15c71a3cfb6ad35baa"
integrity sha512-VyklBkB6dOKnCIh63zdVr7QKVMN9/npwUqNAXxWrz8HabVZH/n/d+lyNm1O/vbXFJlT/Hytb5ouYKYGkoeZirQ==
-choices.js@^10.2.0:
- version "10.2.0"
- resolved "https://registry.yarnpkg.com/choices.js/-/choices.js-10.2.0.tgz#3fe915a12b469a87b9552cd7158e413c8f65ab4f"
- integrity sha512-8PKy6wq7BMjNwDTZwr3+Zry6G2+opJaAJDDA/j3yxvqSCnvkKe7ZIFfIyOhoc7htIWFhsfzF9tJpGUATcpUtPg==
- dependencies:
- deepmerge "^4.2.2"
- fuse.js "^6.6.2"
- redux "^4.2.0"
-
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==
-form-data@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
- integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
- dependencies:
- asynckit "^0.4.0"
- combined-stream "^1.0.8"
- mime-types "^2.1.12"
-
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
-fuse.js@^6.6.2:
- version "6.6.2"
- resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111"
- integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==
-
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
json-parse-better-errors "^1.0.0"
safe-buffer "^5.1.1"
-node-fetch@2.6.7, node-fetch@^2.6.1:
+node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
resolve "^1.20.0"
-redux@^4.2.0:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
- integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
- dependencies:
- "@babel/runtime" "^7.9.2"
-
regenerate-unicode-properties@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"