mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-22 19:01:26 +00:00
Make pages use query params instead of route params where appropriate (#977)
* feat: Add multiple image upload * refactor: Slight cleanup * feat: Add progress bar for multi-image upload * fix: Fix progress bar * fix: Messed up fix last time * refactor: Use await where possible * Add query params to search page * Update translation logic * Did suggested PR changes * Updating translations * Fix i18 issue * Make prettier actually check src in hopes it will fix CI issue * Make home page use query params in URL * Remove unnecessary part of private message url * Make communities page use query params * Make community page use query params * Make user profile use query params * Make modlog use query params * Replace choices.js searchable select entirely * Make 404 screen show up when expected * Refactor query params code * Remove unnecessary boolean literal * Fix query param bug * Address bug with searchable select and initial fetch * Only import what is needed from bootstrap * Undo change to comment nodes component * Convert closure style functions to normal functions * Updated translations * Use translation for loading * Fix create post select community bug * Fix community query params bug
This commit is contained in:
parent
699c3ff4b1
commit
3526baf465
25 changed files with 3591 additions and 3057 deletions
|
@ -1 +1 @@
|
|||
Subproject commit d2b85d582071d84b559f7b9db1ab623f6596c586
|
||||
Subproject commit 5c50ce3ebaf058ad5d4e9bcd445653960cbc98b1
|
|
@ -36,8 +36,8 @@
|
|||
"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",
|
||||
|
@ -66,7 +66,6 @@
|
|||
"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",
|
||||
|
@ -90,13 +89,11 @@
|
|||
"@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",
|
||||
|
|
|
@ -3,6 +3,8 @@ import { BrowserRouter } from "inferno-router";
|
|||
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);
|
||||
|
||||
|
@ -12,7 +14,7 @@ const wrapper = (
|
|||
</BrowserRouter>
|
||||
);
|
||||
|
||||
let root = document.getElementById("root");
|
||||
const root = document.getElementById("root");
|
||||
if (root) {
|
||||
hydrate(wrapper, root);
|
||||
}
|
||||
|
|
|
@ -105,51 +105,54 @@ server.get("/*", async (req, res) => {
|
|||
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,
|
||||
|
@ -170,6 +173,7 @@ server.get("/*", async (req, res) => {
|
|||
<script>eruda.init();</script>
|
||||
</>
|
||||
);
|
||||
|
||||
const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : "";
|
||||
const root = renderToString(wrapper);
|
||||
const helmet = Helmet.renderStatic();
|
||||
|
|
|
@ -40,17 +40,10 @@ export class App extends Component<any, any> {
|
|||
<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} />
|
||||
|
|
|
@ -43,7 +43,6 @@ interface NavbarState {
|
|||
unreadInboxCount: number;
|
||||
unreadReportCount: number;
|
||||
unreadApplicationCount: number;
|
||||
searchParam: string;
|
||||
showDropdown: boolean;
|
||||
onSiteBanner?(url: string): any;
|
||||
}
|
||||
|
@ -59,7 +58,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
unreadReportCount: 0,
|
||||
unreadApplicationCount: 0,
|
||||
expanded: false,
|
||||
searchParam: "",
|
||||
showDropdown: false,
|
||||
};
|
||||
subscription: any;
|
||||
|
@ -115,20 +113,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
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();
|
||||
}
|
||||
|
@ -488,10 +472,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
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();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// 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";
|
||||
|
|
|
@ -430,7 +430,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
<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" />
|
||||
|
|
|
@ -28,7 +28,7 @@ interface CommentNodesProps {
|
|||
}
|
||||
|
||||
export class CommentNodes extends Component<CommentNodesProps, any> {
|
||||
constructor(props: any, context: any) {
|
||||
constructor(props: CommentNodesProps, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
|
|
204
src/shared/components/common/searchable-select.tsx
Normal file
204
src/shared/components/common/searchable-select.tsx
Normal file
|
@ -0,0 +1,204 @@
|
|||
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: "" });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,11 +17,14 @@ import { InitialFetchRequest } from "shared/interfaces";
|
|||
import { i18n } from "../../i18next";
|
||||
import { WebSocketService } from "../../services";
|
||||
import {
|
||||
getListingTypeFromPropsNoDefault,
|
||||
getPageFromProps,
|
||||
getPageFromString,
|
||||
getQueryParams,
|
||||
getQueryString,
|
||||
isBrowser,
|
||||
myAuth,
|
||||
numToSI,
|
||||
QueryParams,
|
||||
routeListingTypeToEnum,
|
||||
setIsoData,
|
||||
showLocal,
|
||||
toast,
|
||||
|
@ -38,16 +41,52 @@ const communityLimit = 50;
|
|||
|
||||
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> {
|
||||
|
@ -55,8 +94,6 @@ 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: "",
|
||||
};
|
||||
|
@ -70,15 +107,15 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,23 +125,6 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -112,6 +132,8 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { listingType, page } = getCommunitiesQueryParams();
|
||||
|
||||
return (
|
||||
<div className="container-lg">
|
||||
<HtmlTags
|
||||
|
@ -129,7 +151,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
<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}
|
||||
|
@ -192,7 +214,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
{i18n.t("unsubscribe")}
|
||||
</button>
|
||||
)}
|
||||
{cv.subscribed == SubscribedType.NotSubscribed && (
|
||||
{cv.subscribed === SubscribedType.NotSubscribed && (
|
||||
<button
|
||||
className="btn btn-link d-inline-block"
|
||||
onClick={linkEvent(
|
||||
|
@ -203,7 +225,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
{i18n.t("subscribe")}
|
||||
</button>
|
||||
)}
|
||||
{cv.subscribed == SubscribedType.Pending && (
|
||||
{cv.subscribed === SubscribedType.Pending && (
|
||||
<div className="text-warning d-inline-block">
|
||||
{i18n.t("subscribe_pending")}
|
||||
</div>
|
||||
|
@ -214,10 +236,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Paginator
|
||||
page={this.state.page}
|
||||
onChange={this.handlePageChange}
|
||||
/>
|
||||
<Paginator page={page} onChange={this.handlePageChange} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -250,12 +269,18 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -270,27 +295,11 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -299,61 +308,50 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
|
||||
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`
|
||||
);
|
||||
i.context.router.history.push(`/search?q=${searchParamEncoded}`);
|
||||
}
|
||||
|
||||
refetch() {
|
||||
let listCommunitiesForm: ListCommunities = {
|
||||
type_: this.state.listingType,
|
||||
static fetchInitialData({
|
||||
query: { listingType, page },
|
||||
client,
|
||||
auth,
|
||||
}: InitialFetchRequest<QueryParams<CommunitiesProps>>): Promise<any>[] {
|
||||
const listCommunitiesForm: ListCommunities = {
|
||||
type_: getListingTypeFromQuery(listingType),
|
||||
sort: SortType.TopMonth,
|
||||
limit: communityLimit,
|
||||
page: this.state.page,
|
||||
auth: myAuth(false),
|
||||
page: getPageFromString(page),
|
||||
auth: auth,
|
||||
};
|
||||
|
||||
WebSocketService.Instance.send(
|
||||
wsClient.listCommunities(listCommunitiesForm)
|
||||
);
|
||||
}
|
||||
|
||||
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_,
|
||||
sort: SortType.TopMonth,
|
||||
limit: communityLimit,
|
||||
page,
|
||||
auth: req.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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Component, linkEvent } from "inferno";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import {
|
||||
AddModToCommunityResponse,
|
||||
BanFromCommunityResponse,
|
||||
BlockCommunityResponse,
|
||||
BlockPersonResponse,
|
||||
CommentReportResponse,
|
||||
CommentResponse,
|
||||
CommentView,
|
||||
CommunityResponse,
|
||||
|
@ -14,7 +14,6 @@ import {
|
|||
GetCommunityResponse,
|
||||
GetPosts,
|
||||
GetPostsResponse,
|
||||
GetSiteResponse,
|
||||
ListingType,
|
||||
PostReportResponse,
|
||||
PostResponse,
|
||||
|
@ -43,16 +42,20 @@ import {
|
|||
enableDownvotes,
|
||||
enableNsfw,
|
||||
fetchLimit,
|
||||
getDataTypeFromProps,
|
||||
getPageFromProps,
|
||||
getSortTypeFromProps,
|
||||
getDataTypeString,
|
||||
getPageFromString,
|
||||
getQueryParams,
|
||||
getQueryString,
|
||||
isPostBlocked,
|
||||
myAuth,
|
||||
notifyPost,
|
||||
nsfwCheck,
|
||||
postToCommentSortType,
|
||||
QueryParams,
|
||||
relTags,
|
||||
restoreScrollPosition,
|
||||
routeDataTypeToEnum,
|
||||
routeSortTypeToEnum,
|
||||
saveCommentRes,
|
||||
saveScrollPosition,
|
||||
setIsoData,
|
||||
|
@ -78,16 +81,10 @@ import { CommunityLink } from "./community-link";
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -97,30 +94,43 @@ interface CommunityProps {
|
|||
page: number;
|
||||
}
|
||||
|
||||
interface UrlParams {
|
||||
dataType?: string;
|
||||
sort?: SortType;
|
||||
page?: number;
|
||||
function getCommunityQueryParams() {
|
||||
return getQueryParams<CommunityProps>({
|
||||
dataType: getDataTypeFromQuery,
|
||||
page: getPageFromString,
|
||||
sort: getSortTypeFromQuery,
|
||||
});
|
||||
}
|
||||
|
||||
export class Community extends Component<any, State> {
|
||||
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<
|
||||
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);
|
||||
|
@ -136,8 +146,10 @@ export class Community extends Component<any, State> {
|
|||
...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;
|
||||
|
||||
|
@ -152,8 +164,7 @@ export class Community extends Component<any, State> {
|
|||
this.state = {
|
||||
...this.state,
|
||||
communityLoading: false,
|
||||
postsLoading: false,
|
||||
commentsLoading: false,
|
||||
listingsLoading: false,
|
||||
};
|
||||
} else {
|
||||
this.fetchCommunity();
|
||||
|
@ -162,8 +173,8 @@ export class Community extends Component<any, State> {
|
|||
}
|
||||
|
||||
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));
|
||||
|
@ -178,95 +189,67 @@ export class Community extends Component<any, State> {
|
|||
this.subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: any): CommunityProps {
|
||||
return {
|
||||
dataType: getDataTypeFromProps(props),
|
||||
sort: getSortTypeFromProps(props),
|
||||
page: getPageFromProps(props),
|
||||
};
|
||||
}
|
||||
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>[] = [];
|
||||
|
||||
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
|
||||
let pathSplit = req.path.split("/");
|
||||
let promises: Promise<any>[] = [];
|
||||
|
||||
let communityName = pathSplit[2];
|
||||
let communityForm: GetCommunity = {
|
||||
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">
|
||||
|
@ -286,7 +269,7 @@ export class Community extends Component<any, State> {
|
|||
|
||||
<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"
|
||||
|
@ -302,55 +285,14 @@ export class Community extends Component<any, State> {
|
|||
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>
|
||||
</>
|
||||
|
@ -360,43 +302,82 @@ export class Community extends Component<any, State> {
|
|||
);
|
||||
}
|
||||
|
||||
listings() {
|
||||
return this.state.dataType == DataType.Post ? (
|
||||
this.state.postsLoading ? (
|
||||
<h5>
|
||||
<Spinner large />
|
||||
</h5>
|
||||
) : (
|
||||
<PostListings
|
||||
posts={this.state.posts}
|
||||
removeDuplicates
|
||||
enableDownvotes={enableDownvotes(this.state.siteRes)}
|
||||
enableNsfw={enableNsfw(this.state.siteRes)}
|
||||
allLanguages={this.state.siteRes.all_languages}
|
||||
siteLanguages={this.state.siteRes.discussion_languages}
|
||||
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}
|
||||
/>
|
||||
)
|
||||
) : 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}
|
||||
/>
|
||||
{!community_view.community.local && site && (
|
||||
<SiteSidebar site={site} showLocal={showLocal(this.isoData)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
communityInfo() {
|
||||
let community = this.state.communityRes?.community_view.community;
|
||||
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={posts}
|
||||
removeDuplicates
|
||||
enableDownvotes={enableDownvotes(site_res)}
|
||||
enableNsfw={enableNsfw(site_res)}
|
||||
allLanguages={site_res.all_languages}
|
||||
siteLanguages={site_res.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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get communityInfo() {
|
||||
const community = this.state.communityRes?.community_view.community;
|
||||
|
||||
return (
|
||||
community && (
|
||||
<div className="mb-2">
|
||||
|
@ -414,25 +395,26 @@ export class Community extends Component<any, State> {
|
|||
);
|
||||
}
|
||||
|
||||
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 && (
|
||||
<>
|
||||
|
@ -455,66 +437,90 @@ export class Community extends Component<any, State> {
|
|||
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;
|
||||
updateUrl({ dataType, page, sort }: Partial<CommunityProps>) {
|
||||
const {
|
||||
dataType: urlDataType,
|
||||
page: urlPage,
|
||||
sort: urlSort,
|
||||
} = getCommunityQueryParams();
|
||||
|
||||
let typeView = `/c/${this.state.communityName}`;
|
||||
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(
|
||||
|
@ -523,143 +529,225 @@ export class Community extends Component<any, State> {
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
switch (op) {
|
||||
case UserOperation.GetCommunity: {
|
||||
const data = wsJsonToRes<GetCommunityResponse>(msg);
|
||||
|
||||
let showPostNotifs =
|
||||
UserService.Instance.myUserInfo?.local_user_view.local_user
|
||||
.show_new_post_notifs;
|
||||
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,
|
||||
})
|
||||
);
|
||||
|
||||
// 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);
|
||||
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));
|
||||
case UserOperation.EditCommunity:
|
||||
case UserOperation.DeleteCommunity:
|
||||
case UserOperation.RemoveCommunity: {
|
||||
const { community_view, discussion_languages } =
|
||||
wsJsonToRes<CommunityResponse>(msg);
|
||||
|
||||
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);
|
||||
if (res) {
|
||||
res.community_view = community_view;
|
||||
res.discussion_languages = discussion_languages;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
// Necessary since it might be a user reply
|
||||
if (data.form_id) {
|
||||
this.state.comments.unshift(data.comment_view);
|
||||
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;
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -280,7 +280,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
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>
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,8 @@
|
|||
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,
|
||||
|
@ -7,6 +10,8 @@ import {
|
|||
BlockPerson,
|
||||
BlockPersonResponse,
|
||||
CommentResponse,
|
||||
CommunityModeratorView,
|
||||
CommunitySafe,
|
||||
GetPersonDetails,
|
||||
GetPersonDetailsResponse,
|
||||
GetSiteResponse,
|
||||
|
@ -33,12 +38,15 @@ import {
|
|||
enableNsfw,
|
||||
fetchLimit,
|
||||
futureDaysToUnixTime,
|
||||
getUsernameFromProps,
|
||||
getPageFromString,
|
||||
getQueryParams,
|
||||
getQueryString,
|
||||
isAdmin,
|
||||
isBanned,
|
||||
mdToHtml,
|
||||
myAuth,
|
||||
numToSI,
|
||||
QueryParams,
|
||||
relTags,
|
||||
restoreScrollPosition,
|
||||
routeSortTypeToEnum,
|
||||
|
@ -62,10 +70,6 @@ import { PersonListing } from "./person-listing";
|
|||
|
||||
interface ProfileState {
|
||||
personRes?: GetPersonDetailsResponse;
|
||||
userName: string;
|
||||
view: PersonDetailsView;
|
||||
sort: SortType;
|
||||
page: number;
|
||||
loading: boolean;
|
||||
personBlocked: boolean;
|
||||
banReason?: string;
|
||||
|
@ -79,32 +83,84 @@ interface ProfileProps {
|
|||
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);
|
||||
|
@ -114,7 +170,7 @@ export class Profile extends Component<any, ProfileState> {
|
|||
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,
|
||||
|
@ -126,65 +182,61 @@ export class Profile extends Component<any, ProfileState> {
|
|||
}
|
||||
|
||||
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 fetchInitialData({
|
||||
client,
|
||||
path,
|
||||
query: { page, sort, view: urlView },
|
||||
auth,
|
||||
}: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<any>[] {
|
||||
const pathSplit = path.split("/");
|
||||
|
||||
static getSortTypeFromProps(sort: string): SortType {
|
||||
return sort ? routeSortTypeToEnum(sort) : SortType.New;
|
||||
}
|
||||
const username = pathSplit[2];
|
||||
const view = getViewFromProps(urlView);
|
||||
|
||||
static getPageFromProps(page: number): number {
|
||||
return page ? Number(page) : 1;
|
||||
}
|
||||
|
||||
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
|
||||
let pathSplit = req.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]));
|
||||
|
||||
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() {
|
||||
|
@ -197,78 +249,59 @@ export class Profile extends Component<any, ProfileState> {
|
|||
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>
|
||||
)
|
||||
)}
|
||||
|
@ -276,73 +309,49 @@ export class Profile extends Component<any, ProfileState> {
|
|||
);
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -354,33 +363,15 @@ export class Profile extends Component<any, ProfileState> {
|
|||
</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>
|
||||
|
@ -429,7 +420,7 @@ export class Profile extends Component<any, ProfileState> {
|
|||
)}
|
||||
</ul>
|
||||
</div>
|
||||
{this.banDialog()}
|
||||
{this.banDialog}
|
||||
<div className="flex-grow-1 unselectable pointer mx-2"></div>
|
||||
{!this.amCurrentUser && UserService.Instance.myUserInfo && (
|
||||
<>
|
||||
|
@ -446,19 +437,16 @@ export class Profile extends Component<any, ProfileState> {
|
|||
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>
|
||||
|
@ -467,10 +455,7 @@ export class Profile extends Component<any, ProfileState> {
|
|||
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>
|
||||
|
@ -478,9 +463,9 @@ export class Profile extends Component<any, ProfileState> {
|
|||
</>
|
||||
)}
|
||||
|
||||
{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={
|
||||
|
@ -552,12 +537,14 @@ export class Profile extends Component<any, ProfileState> {
|
|||
);
|
||||
}
|
||||
|
||||
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">
|
||||
|
@ -630,73 +617,38 @@ export class Profile extends Component<any, ProfileState> {
|
|||
);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
@ -724,20 +676,25 @@ export class Profile extends Component<any, ProfileState> {
|
|||
|
||||
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));
|
||||
|
@ -747,94 +704,138 @@ export class Profile extends Component<any, ProfileState> {
|
|||
}
|
||||
|
||||
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;
|
||||
} 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);
|
||||
|
||||
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(`/`);
|
||||
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(`/`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { NoOptionI18nKeys } from "i18next";
|
||||
import { Component, linkEvent } from "inferno";
|
||||
import {
|
||||
BlockCommunity,
|
||||
|
@ -6,13 +7,11 @@ import {
|
|||
BlockPersonResponse,
|
||||
ChangePassword,
|
||||
CommunityBlockView,
|
||||
CommunityView,
|
||||
DeleteAccount,
|
||||
GetSiteResponse,
|
||||
ListingType,
|
||||
LoginResponse,
|
||||
PersonBlockView,
|
||||
PersonViewSafe,
|
||||
SaveUserSettings,
|
||||
SortType,
|
||||
UserOperation,
|
||||
|
@ -24,19 +23,17 @@ import { i18n, languages } from "../../i18next";
|
|||
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,
|
||||
|
@ -55,15 +52,11 @@ import { ImageUploadForm } from "../common/image-upload-form";
|
|||
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: {
|
||||
|
@ -97,10 +90,7 @@ interface SettingsState {
|
|||
password?: string;
|
||||
};
|
||||
personBlocks: PersonBlockView[];
|
||||
blockPerson?: PersonViewSafe;
|
||||
communityBlocks: CommunityBlockView[];
|
||||
blockCommunityId: number;
|
||||
blockCommunity?: CommunityView;
|
||||
currentTab: string;
|
||||
themeList: string[];
|
||||
saveUserSettingsLoading: boolean;
|
||||
|
@ -108,12 +98,50 @@ interface SettingsState {
|
|||
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: {},
|
||||
|
@ -125,10 +153,13 @@ export class Settings extends Component<any, SettingsState> {
|
|||
deleteAccountForm: {},
|
||||
personBlocks: [],
|
||||
communityBlocks: [],
|
||||
blockCommunityId: 0,
|
||||
currentTab: "settings",
|
||||
siteRes: this.isoData.site_res,
|
||||
themeList: [],
|
||||
searchCommunityLoading: false,
|
||||
searchCommunityOptions: [],
|
||||
searchPersonLoading: false,
|
||||
searchPersonOptions: [],
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -149,35 +180,58 @@ export class Settings extends Component<any, SettingsState> {
|
|||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -349,9 +403,17 @@ export class Settings extends Component<any, SettingsState> {
|
|||
}
|
||||
|
||||
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>
|
||||
);
|
||||
|
@ -384,38 +446,18 @@ export class Settings extends Component<any, SettingsState> {
|
|||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
|
@ -448,33 +490,6 @@ export class Settings extends Component<any, SettingsState> {
|
|||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
@ -907,91 +922,57 @@ export class Settings extends Component<any, SettingsState> {
|
|||
);
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const searchPersonOptions: Choice[] = [];
|
||||
|
||||
handleBlockPerson(personId: number) {
|
||||
let auth = myAuth();
|
||||
if (auth && personId != 0) {
|
||||
let blockUserForm: BlockPerson = {
|
||||
person_id: personId,
|
||||
if (text.length > 0) {
|
||||
searchPersonOptions.push(
|
||||
...(await fetchUsers(text)).users.map(personToChoice)
|
||||
);
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -1000,11 +981,11 @@ export class Settings extends Component<any, SettingsState> {
|
|||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
@ -1015,9 +996,9 @@ export class Settings extends Component<any, SettingsState> {
|
|||
}
|
||||
|
||||
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,
|
||||
|
@ -1249,11 +1230,6 @@ export class Settings extends Component<any, SettingsState> {
|
|||
|
||||
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) {
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import { Component } from "inferno";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import {
|
||||
GetCommunity,
|
||||
GetCommunityResponse,
|
||||
GetSiteResponse,
|
||||
ListCommunities,
|
||||
ListCommunitiesResponse,
|
||||
ListingType,
|
||||
PostView,
|
||||
SortType,
|
||||
UserOperation,
|
||||
wsJsonToRes,
|
||||
wsUserOp,
|
||||
|
@ -17,11 +14,15 @@ import { InitialFetchRequest, PostFormParams } from "shared/interfaces";
|
|||
import { i18n } from "../../i18next";
|
||||
import { UserService, WebSocketService } from "../../services";
|
||||
import {
|
||||
Choice,
|
||||
enableDownvotes,
|
||||
enableNsfw,
|
||||
fetchLimit,
|
||||
getIdFromString,
|
||||
getQueryParams,
|
||||
getQueryString,
|
||||
isBrowser,
|
||||
myAuth,
|
||||
QueryParams,
|
||||
setIsoData,
|
||||
toast,
|
||||
wsClient,
|
||||
|
@ -31,13 +32,26 @@ import { HtmlTags } from "../common/html-tags";
|
|||
import { Spinner } from "../common/icon";
|
||||
import { PostForm } from "./post-form";
|
||||
|
||||
interface CreatePostState {
|
||||
listCommunitiesResponse?: ListCommunitiesResponse;
|
||||
siteRes: GetSiteResponse;
|
||||
loading: boolean;
|
||||
export interface CreatePostProps {
|
||||
communityId?: number;
|
||||
}
|
||||
|
||||
export class CreatePost extends Component<any, CreatePostState> {
|
||||
function getCreatePostQueryParams() {
|
||||
return getQueryParams<CreatePostProps>({
|
||||
communityId: getIdFromString,
|
||||
});
|
||||
}
|
||||
|
||||
interface CreatePostState {
|
||||
siteRes: GetSiteResponse;
|
||||
loading: boolean;
|
||||
selectedCommunityChoice?: Choice;
|
||||
}
|
||||
|
||||
export class CreatePost extends Component<
|
||||
RouteComponentProps<Record<string, never>>,
|
||||
CreatePostState
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private subscription?: Subscription;
|
||||
state: CreatePostState = {
|
||||
|
@ -45,10 +59,12 @@ export class CreatePost extends Component<any, 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);
|
||||
|
@ -59,45 +75,56 @@ export class CreatePost extends Component<any, CreatePostState> {
|
|||
}
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,7 +141,12 @@ export class CreatePost extends Component<any, CreatePostState> {
|
|||
}
|
||||
|
||||
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
|
||||
|
@ -126,96 +158,93 @@ export class CreatePost extends Component<any, CreatePostState> {
|
|||
<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;
|
||||
|
||||
this.props.history.push(
|
||||
`/create_post${getQueryString(queryParams)}`,
|
||||
locationState
|
||||
);
|
||||
|
||||
this.fetchCommunity();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import autosize from "autosize";
|
|||
import { Component, linkEvent } from "inferno";
|
||||
import { Prompt } from "inferno-router";
|
||||
import {
|
||||
CommunityView,
|
||||
CreatePost,
|
||||
EditPost,
|
||||
Language,
|
||||
|
@ -24,14 +23,13 @@ import { UserService, WebSocketService } from "../../services";
|
|||
import {
|
||||
archiveTodayUrl,
|
||||
capitalizeFirstLetter,
|
||||
choicesConfig,
|
||||
communitySelectName,
|
||||
Choice,
|
||||
communityToChoice,
|
||||
debounce,
|
||||
fetchCommunities,
|
||||
getIdFromString,
|
||||
getSiteMetadata,
|
||||
ghostArchiveUrl,
|
||||
isBrowser,
|
||||
isImage,
|
||||
myAuth,
|
||||
myFirstDiscussionLanguageId,
|
||||
|
@ -50,26 +48,23 @@ import {
|
|||
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 {
|
||||
|
@ -88,32 +83,34 @@ 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,
|
||||
|
@ -128,15 +125,26 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
};
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -144,8 +152,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
|
||||
componentDidMount() {
|
||||
setupTippy();
|
||||
this.setupCommunities();
|
||||
let textarea: any = document.getElementById("post-title");
|
||||
const textarea: any = document.getElementById("post-title");
|
||||
|
||||
if (textarea) {
|
||||
autosize(textarea);
|
||||
}
|
||||
|
@ -168,6 +176,19 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
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 ??
|
||||
|
@ -342,26 +363,23 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
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>
|
||||
)}
|
||||
|
@ -609,67 +627,41 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
});
|
||||
}
|
||||
|
||||
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));
|
||||
if (text.length > 0) {
|
||||
newOptions.push(
|
||||
...(await fetchCommunities(text)).communities.map(communityToChoice)
|
||||
);
|
||||
|
||||
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));
|
||||
}
|
||||
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) {
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
} 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,
|
||||
|
@ -147,7 +147,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
let post = this.props.post_view.post;
|
||||
const post = this.props.post_view.post;
|
||||
|
||||
return (
|
||||
<div className="post-listing">
|
||||
{!this.state.showEdit ? (
|
||||
|
@ -734,7 +735,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
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 />
|
||||
|
@ -1461,18 +1469,22 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,5 @@
|
|||
import { GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
||||
import type { ParsedQs } from "qs";
|
||||
|
||||
/**
|
||||
* This contains serialized data, it needs to be deserialized before use.
|
||||
|
@ -20,17 +21,18 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -49,10 +51,10 @@ export enum BanType {
|
|||
}
|
||||
|
||||
export enum PersonDetailsView {
|
||||
Overview,
|
||||
Comments,
|
||||
Posts,
|
||||
Saved,
|
||||
Overview = "Overview",
|
||||
Comments = "Comments",
|
||||
Posts = "Posts",
|
||||
Saved = "Saved",
|
||||
}
|
||||
|
||||
export enum PurgeType {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Inferno } from "inferno";
|
||||
import { IRouteProps } from "inferno-router/dist/Route";
|
||||
import { Communities } from "./components/community/communities";
|
||||
import { Community } from "./components/community/community";
|
||||
|
@ -26,21 +25,15 @@ import { InitialFetchRequest } from "./interfaces";
|
|||
|
||||
interface IRoutePropsWithFetch extends IRouteProps {
|
||||
// TODO Make sure this one is good.
|
||||
component: Inferno.ComponentClass;
|
||||
fetchInitialData?(req: InitialFetchRequest): Promise<any>[];
|
||||
}
|
||||
|
||||
export const routes: IRoutePropsWithFetch[] = [
|
||||
{
|
||||
path: `/`,
|
||||
component: Home,
|
||||
fetchInitialData: Home.fetchInitialData,
|
||||
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),
|
||||
},
|
||||
{
|
||||
path: `/login`,
|
||||
|
@ -53,101 +46,81 @@ export const routes: IRoutePropsWithFetch[] = [
|
|||
{
|
||||
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`,
|
||||
component: Modlog,
|
||||
fetchInitialData: req => Modlog.fetchInitialData(req),
|
||||
},
|
||||
{
|
||||
path: `/modlog`,
|
||||
component: Modlog,
|
||||
fetchInitialData: req => Modlog.fetchInitialData(req),
|
||||
fetchInitialData: Modlog.fetchInitialData,
|
||||
},
|
||||
{
|
||||
path: `/modlog/:communityId`,
|
||||
component: Modlog,
|
||||
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`,
|
||||
|
|
|
@ -82,6 +82,8 @@ export const concurrentImageUpload = 4;
|
|||
|
||||
export const relTags = "noopener nofollow";
|
||||
|
||||
export const emDash = "\u2014";
|
||||
|
||||
export type ThemeColor =
|
||||
| "primary"
|
||||
| "secondary"
|
||||
|
@ -118,6 +120,14 @@ function getRandomCharFromAlphabet(alphabet: string): string {
|
|||
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
|
||||
|
@ -332,8 +342,11 @@ export function capitalizeFirstLetter(str: string): string {
|
|||
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 {
|
||||
|
@ -344,16 +357,25 @@ export function sortTypeFromNum(type_: number): SortType {
|
|||
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) {
|
||||
|
@ -362,26 +384,34 @@ 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 () {
|
||||
|
@ -400,7 +430,7 @@ export function debounce(func: any, wait = 1000, immediate = false) {
|
|||
|
||||
// Immediate mode and no wait timer? Execute the function..
|
||||
if (callNow) func.apply(this, args);
|
||||
};
|
||||
} as (...e: T) => R;
|
||||
}
|
||||
|
||||
export function getLanguages(
|
||||
|
@ -903,47 +933,6 @@ async function communitySearch(text: string): Promise<CommunityTribute[]> {
|
|||
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)
|
||||
|
@ -960,10 +949,6 @@ export function getCommentIdFromProps(props: any): number | undefined {
|
|||
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) {
|
||||
|
@ -1378,25 +1363,30 @@ export function showLocal(isoData: IsoData): boolean {
|
|||
return linked ? linked.length > 0 : false;
|
||||
}
|
||||
|
||||
export interface ChoicesValue {
|
||||
export interface Choice {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function communityToChoice(cv: CommunityView): ChoicesValue {
|
||||
let choice: ChoicesValue = {
|
||||
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): 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) {
|
||||
|
@ -1427,49 +1417,17 @@ export async function fetchUsers(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) {
|
||||
|
@ -1505,12 +1463,6 @@ export function isBanned(ps: PersonSafe): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -1524,14 +1476,17 @@ export function enableNsfw(siteRes: GetSiteResponse): boolean {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1553,7 +1508,8 @@ export function canCreateCommunity(
|
|||
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);
|
||||
}
|
||||
|
||||
|
@ -1651,3 +1607,36 @@ const groupBy = <T>(
|
|||
(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}`,
|
||||
"?"
|
||||
);
|
||||
}
|
||||
|
|
42
yarn.lock
42
yarn.lock
|
@ -1115,7 +1115,7 @@
|
|||
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==
|
||||
|
@ -1534,14 +1534,6 @@
|
|||
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"
|
||||
|
@ -2530,15 +2522,6 @@ check-password-strength@^2.0.7:
|
|||
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"
|
||||
|
@ -3980,15 +3963,6 @@ forever-agent@~0.6.1:
|
|||
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"
|
||||
|
@ -4104,11 +4078,6 @@ functions-have-names@^1.2.2:
|
|||
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"
|
||||
|
@ -6089,7 +6058,7 @@ node-fetch-npm@^2.0.2:
|
|||
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==
|
||||
|
@ -7386,13 +7355,6 @@ rechoir@^0.8.0:
|
|||
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"
|
||||
|
|
Loading…
Reference in a new issue