mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-26 18:54:12 +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
|
@ -1 +1 @@
|
||||||
Subproject commit d2b85d582071d84b559f7b9db1ab623f6596c586
|
Subproject commit 5c50ce3ebaf058ad5d4e9bcd445653960cbc98b1
|
|
@ -36,8 +36,8 @@
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"babel-loader": "^9.1.2",
|
"babel-loader": "^9.1.2",
|
||||||
"babel-plugin-inferno": "^6.6.0",
|
"babel-plugin-inferno": "^6.6.0",
|
||||||
|
"bootstrap": "^5.2.3",
|
||||||
"check-password-strength": "^2.0.7",
|
"check-password-strength": "^2.0.7",
|
||||||
"choices.js": "^10.2.0",
|
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
@ -66,7 +66,6 @@
|
||||||
"markdown-it-sup": "^1.0.0",
|
"markdown-it-sup": "^1.0.0",
|
||||||
"mini-css-extract-plugin": "^2.7.2",
|
"mini-css-extract-plugin": "^2.7.2",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"node-fetch": "^2.6.1",
|
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"run-node-webpack-plugin": "^1.3.0",
|
"run-node-webpack-plugin": "^1.3.0",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
|
@ -90,13 +89,11 @@
|
||||||
"@types/markdown-it": "^12.2.3",
|
"@types/markdown-it": "^12.2.3",
|
||||||
"@types/markdown-it-container": "^2.0.5",
|
"@types/markdown-it-container": "^2.0.5",
|
||||||
"@types/node": "^18.14.0",
|
"@types/node": "^18.14.0",
|
||||||
"@types/node-fetch": "^2.6.2",
|
|
||||||
"@types/sanitize-html": "^2.8.0",
|
"@types/sanitize-html": "^2.8.0",
|
||||||
"@types/serialize-javascript": "^5.0.1",
|
"@types/serialize-javascript": "^5.0.1",
|
||||||
"@types/toastify-js": "^1.11.1",
|
"@types/toastify-js": "^1.11.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
||||||
"@typescript-eslint/parser": "^5.53.0",
|
"@typescript-eslint/parser": "^5.53.0",
|
||||||
"bootstrap": "^5.2.3",
|
|
||||||
"bootswatch": "^5.2.3",
|
"bootswatch": "^5.2.3",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"eslint-plugin-inferno": "^7.32.1",
|
"eslint-plugin-inferno": "^7.32.1",
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { BrowserRouter } from "inferno-router";
|
||||||
import { App } from "../shared/components/app/app";
|
import { App } from "../shared/components/app/app";
|
||||||
import { initializeSite } from "../shared/utils";
|
import { initializeSite } from "../shared/utils";
|
||||||
|
|
||||||
|
import "bootstrap/js/dist/dropdown";
|
||||||
|
|
||||||
const site = window.isoData.site_res;
|
const site = window.isoData.site_res;
|
||||||
initializeSite(site);
|
initializeSite(site);
|
||||||
|
|
||||||
|
@ -12,7 +14,7 @@ const wrapper = (
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
let root = document.getElementById("root");
|
const root = document.getElementById("root");
|
||||||
if (root) {
|
if (root) {
|
||||||
hydrate(wrapper, root);
|
hydrate(wrapper, root);
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,51 +105,54 @@ server.get("/*", async (req, res) => {
|
||||||
const context = {} as any;
|
const context = {} as any;
|
||||||
let auth: string | undefined = IsomorphicCookie.load("jwt", req);
|
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);
|
const headers = setForwardedHeaders(req.headers);
|
||||||
|
const client = new LemmyHttp(httpBaseInternal, headers);
|
||||||
let initialFetchReq: InitialFetchRequest = {
|
|
||||||
client: new LemmyHttp(httpBaseInternal, headers),
|
|
||||||
auth,
|
|
||||||
path: req.path,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get site data first
|
// Get site data first
|
||||||
// This bypasses errors, so that the client can hit the error on its own,
|
// 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
|
// 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") {
|
if (try_site.error == "not_logged_in") {
|
||||||
console.error(
|
console.error(
|
||||||
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
|
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
|
||||||
);
|
);
|
||||||
getSiteForm.auth = undefined;
|
getSiteForm.auth = undefined;
|
||||||
initialFetchReq.auth = undefined;
|
auth = undefined;
|
||||||
try_site = await initialFetchReq.client.getSite(getSiteForm);
|
try_site = await client.getSite(getSiteForm);
|
||||||
}
|
}
|
||||||
let site: GetSiteResponse = try_site;
|
const site: GetSiteResponse = try_site;
|
||||||
initializeSite(site);
|
initializeSite(site);
|
||||||
|
|
||||||
|
const initialFetchReq: InitialFetchRequest = {
|
||||||
|
client,
|
||||||
|
auth,
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
site,
|
||||||
|
};
|
||||||
|
|
||||||
if (activeRoute?.fetchInitialData) {
|
if (activeRoute?.fetchInitialData) {
|
||||||
promises.push(...activeRoute.fetchInitialData(initialFetchReq));
|
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
|
// Redirect to the 404 if there's an API error
|
||||||
if (routeData[0] && routeData[0].error) {
|
if (routeData[0] && routeData[0].error) {
|
||||||
let errCode = routeData[0].error;
|
const error = routeData[0].error;
|
||||||
console.error(errCode);
|
console.error(error);
|
||||||
if (errCode == "instance_is_private") {
|
if (error === "instance_is_private") {
|
||||||
return res.redirect(`/signup`);
|
return res.redirect(`/signup`);
|
||||||
} else {
|
} else {
|
||||||
return res.send(`404: ${removeAuthParam(errCode)}`);
|
return res.send(`404: ${removeAuthParam(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let isoData: IsoData = {
|
const isoData: IsoData = {
|
||||||
path: req.path,
|
path: req.path,
|
||||||
site_res: site,
|
site_res: site,
|
||||||
routeData,
|
routeData,
|
||||||
|
@ -170,6 +173,7 @@ server.get("/*", async (req, res) => {
|
||||||
<script>eruda.init();</script>
|
<script>eruda.init();</script>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : "";
|
const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : "";
|
||||||
const root = renderToString(wrapper);
|
const root = renderToString(wrapper);
|
||||||
const helmet = Helmet.renderStatic();
|
const helmet = Helmet.renderStatic();
|
||||||
|
|
|
@ -40,17 +40,10 @@ export class App extends Component<any, any> {
|
||||||
<Navbar siteRes={siteRes} />
|
<Navbar siteRes={siteRes} />
|
||||||
<div className="mt-4 p-0 fl-1">
|
<div className="mt-4 p-0 fl-1">
|
||||||
<Switch>
|
<Switch>
|
||||||
{routes.map(
|
{routes.map(({ path, component }) => (
|
||||||
({ path, exact, component: Component, ...rest }) => (
|
<Route key={path} path={path} exact component={component} />
|
||||||
<Route
|
))}
|
||||||
key={path}
|
<Route component={NoMatch} />
|
||||||
path={path}
|
|
||||||
exact={exact}
|
|
||||||
render={props => <Component {...props} {...rest} />}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<Route render={props => <NoMatch {...props} />} />
|
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<Footer site={siteRes} />
|
<Footer site={siteRes} />
|
||||||
|
|
|
@ -43,7 +43,6 @@ interface NavbarState {
|
||||||
unreadInboxCount: number;
|
unreadInboxCount: number;
|
||||||
unreadReportCount: number;
|
unreadReportCount: number;
|
||||||
unreadApplicationCount: number;
|
unreadApplicationCount: number;
|
||||||
searchParam: string;
|
|
||||||
showDropdown: boolean;
|
showDropdown: boolean;
|
||||||
onSiteBanner?(url: string): any;
|
onSiteBanner?(url: string): any;
|
||||||
}
|
}
|
||||||
|
@ -59,7 +58,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
unreadReportCount: 0,
|
unreadReportCount: 0,
|
||||||
unreadApplicationCount: 0,
|
unreadApplicationCount: 0,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
searchParam: "",
|
|
||||||
showDropdown: false,
|
showDropdown: false,
|
||||||
};
|
};
|
||||||
subscription: any;
|
subscription: any;
|
||||||
|
@ -115,20 +113,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
this.unreadApplicationCountSub.unsubscribe();
|
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() {
|
render() {
|
||||||
return this.navbar();
|
return this.navbar();
|
||||||
}
|
}
|
||||||
|
@ -488,10 +472,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
i.setState({ expanded: false, showDropdown: false });
|
i.setState({ expanded: false, showDropdown: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchParam(i: Navbar, event: any) {
|
|
||||||
i.setState({ searchParam: event.target.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLogoutClick(i: Navbar) {
|
handleLogoutClick(i: Navbar) {
|
||||||
i.setState({ showDropdown: false, expanded: false });
|
i.setState({ showDropdown: false, expanded: false });
|
||||||
UserService.Instance.logout();
|
UserService.Instance.logout();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Custom css
|
// Custom css
|
||||||
@import "../../../../node_modules/tributejs/dist/tribute.css";
|
@import "../../../../node_modules/tributejs/dist/tribute.css";
|
||||||
@import "../../../../node_modules/toastify-js/src/toastify.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/tippy.js/dist/tippy.css";
|
||||||
|
@import "../../../../node_modules/bootstrap/dist/css/bootstrap-utilities.min.css";
|
||||||
@import "../../../assets/css/main.css";
|
@import "../../../assets/css/main.css";
|
||||||
|
|
|
@ -430,7 +430,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
<button className="btn btn-link btn-animate">
|
<button className="btn btn-link btn-animate">
|
||||||
<Link
|
<Link
|
||||||
className="text-muted"
|
className="text-muted"
|
||||||
to={`/create_private_message/recipient/${cv.creator.id}`}
|
to={`/create_private_message/${cv.creator.id}`}
|
||||||
title={i18n.t("message").toLowerCase()}
|
title={i18n.t("message").toLowerCase()}
|
||||||
>
|
>
|
||||||
<Icon icon="mail" />
|
<Icon icon="mail" />
|
||||||
|
|
|
@ -28,7 +28,7 @@ interface CommentNodesProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommentNodes extends Component<CommentNodesProps, any> {
|
export class CommentNodes extends Component<CommentNodesProps, any> {
|
||||||
constructor(props: any, context: any) {
|
constructor(props: CommentNodesProps, context: any) {
|
||||||
super(props, context);
|
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 { i18n } from "../../i18next";
|
||||||
import { WebSocketService } from "../../services";
|
import { WebSocketService } from "../../services";
|
||||||
import {
|
import {
|
||||||
getListingTypeFromPropsNoDefault,
|
getPageFromString,
|
||||||
getPageFromProps,
|
getQueryParams,
|
||||||
|
getQueryString,
|
||||||
isBrowser,
|
isBrowser,
|
||||||
myAuth,
|
myAuth,
|
||||||
numToSI,
|
numToSI,
|
||||||
|
QueryParams,
|
||||||
|
routeListingTypeToEnum,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
showLocal,
|
showLocal,
|
||||||
toast,
|
toast,
|
||||||
|
@ -38,16 +41,52 @@ const communityLimit = 50;
|
||||||
|
|
||||||
interface CommunitiesState {
|
interface CommunitiesState {
|
||||||
listCommunitiesResponse?: ListCommunitiesResponse;
|
listCommunitiesResponse?: ListCommunitiesResponse;
|
||||||
page: number;
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
siteRes: GetSiteResponse;
|
siteRes: GetSiteResponse;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
listingType: ListingType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommunitiesProps {
|
interface CommunitiesProps {
|
||||||
listingType?: ListingType;
|
listingType: ListingType;
|
||||||
page?: number;
|
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> {
|
export class Communities extends Component<any, CommunitiesState> {
|
||||||
|
@ -55,8 +94,6 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
state: CommunitiesState = {
|
state: CommunitiesState = {
|
||||||
loading: true,
|
loading: true,
|
||||||
page: getPageFromProps(this.props),
|
|
||||||
listingType: getListingTypeFromPropsNoDefault(this.props),
|
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
searchText: "",
|
searchText: "",
|
||||||
};
|
};
|
||||||
|
@ -70,15 +107,15 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
this.subscription = wsSubscribe(this.parseMessage);
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// 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) {
|
||||||
let listRes = this.isoData.routeData[0] as ListCommunitiesResponse;
|
const listRes = this.isoData.routeData[0] as ListCommunitiesResponse;
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
listCommunitiesResponse: listRes,
|
listCommunitiesResponse: listRes,
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
} else {
|
} 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 {
|
get documentTitle(): string {
|
||||||
return `${i18n.t("communities")} - ${
|
return `${i18n.t("communities")} - ${
|
||||||
this.state.siteRes.site_view.site.name
|
this.state.siteRes.site_view.site.name
|
||||||
|
@ -112,6 +132,8 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { listingType, page } = getCommunitiesQueryParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-lg">
|
<div className="container-lg">
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
|
@ -129,7 +151,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
<h4>{i18n.t("list_of_communities")}</h4>
|
<h4>{i18n.t("list_of_communities")}</h4>
|
||||||
<span className="mb-2">
|
<span className="mb-2">
|
||||||
<ListingTypeSelect
|
<ListingTypeSelect
|
||||||
type_={this.state.listingType}
|
type_={listingType}
|
||||||
showLocal={showLocal(this.isoData)}
|
showLocal={showLocal(this.isoData)}
|
||||||
showSubscribed
|
showSubscribed
|
||||||
onChange={this.handleListingTypeChange}
|
onChange={this.handleListingTypeChange}
|
||||||
|
@ -192,7 +214,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
{i18n.t("unsubscribe")}
|
{i18n.t("unsubscribe")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{cv.subscribed == SubscribedType.NotSubscribed && (
|
{cv.subscribed === SubscribedType.NotSubscribed && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link d-inline-block"
|
className="btn btn-link d-inline-block"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
|
@ -203,7 +225,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
{i18n.t("subscribe")}
|
{i18n.t("subscribe")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{cv.subscribed == SubscribedType.Pending && (
|
{cv.subscribed === SubscribedType.Pending && (
|
||||||
<div className="text-warning d-inline-block">
|
<div className="text-warning d-inline-block">
|
||||||
{i18n.t("subscribe_pending")}
|
{i18n.t("subscribe_pending")}
|
||||||
</div>
|
</div>
|
||||||
|
@ -214,10 +236,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<Paginator
|
<Paginator page={page} onChange={this.handlePageChange} />
|
||||||
page={this.state.page}
|
|
||||||
onChange={this.handlePageChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -250,12 +269,18 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl(paramUpdates: CommunitiesProps) {
|
updateUrl({ listingType, page }: Partial<CommunitiesProps>) {
|
||||||
const page = paramUpdates.page || this.state.page;
|
const { listingType: urlListingType, page: urlPage } =
|
||||||
const listingTypeStr = paramUpdates.listingType || this.state.listingType;
|
getCommunitiesQueryParams();
|
||||||
this.props.history.push(
|
|
||||||
`/communities/listing_type/${listingTypeStr}/page/${page}`
|
const queryParams: QueryParams<CommunitiesProps> = {
|
||||||
);
|
listingType: listingType ?? urlListingType,
|
||||||
|
page: (page ?? urlPage)?.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.props.history.push(`/communities${getQueryString(queryParams)}`);
|
||||||
|
|
||||||
|
refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePageChange(page: number) {
|
handlePageChange(page: number) {
|
||||||
|
@ -270,27 +295,11 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnsubscribe(communityId: number) {
|
handleUnsubscribe(communityId: number) {
|
||||||
let auth = myAuth();
|
toggleSubscribe(communityId, false);
|
||||||
if (auth) {
|
|
||||||
let form: FollowCommunity = {
|
|
||||||
community_id: communityId,
|
|
||||||
follow: false,
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.followCommunity(form));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubscribe(communityId: number) {
|
handleSubscribe(communityId: number) {
|
||||||
let auth = myAuth();
|
toggleSubscribe(communityId, true);
|
||||||
if (auth) {
|
|
||||||
let form: FollowCommunity = {
|
|
||||||
community_id: communityId,
|
|
||||||
follow: true,
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
WebSocketService.Instance.send(wsClient.followCommunity(form));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchChange(i: Communities, event: any) {
|
handleSearchChange(i: Communities, event: any) {
|
||||||
|
@ -299,61 +308,50 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
|
|
||||||
handleSearchSubmit(i: Communities) {
|
handleSearchSubmit(i: Communities) {
|
||||||
const searchParamEncoded = encodeURIComponent(i.state.searchText);
|
const searchParamEncoded = encodeURIComponent(i.state.searchText);
|
||||||
i.context.router.history.push(
|
i.context.router.history.push(`/search?q=${searchParamEncoded}`);
|
||||||
`/search/q/${searchParamEncoded}/type/Communities/sort/TopAll/listing_type/All/community_id/0/creator_id/0/page/1`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refetch() {
|
static fetchInitialData({
|
||||||
let listCommunitiesForm: ListCommunities = {
|
query: { listingType, page },
|
||||||
type_: this.state.listingType,
|
client,
|
||||||
|
auth,
|
||||||
|
}: InitialFetchRequest<QueryParams<CommunitiesProps>>): Promise<any>[] {
|
||||||
|
const listCommunitiesForm: ListCommunities = {
|
||||||
|
type_: getListingTypeFromQuery(listingType),
|
||||||
sort: SortType.TopMonth,
|
sort: SortType.TopMonth,
|
||||||
limit: communityLimit,
|
limit: communityLimit,
|
||||||
page: this.state.page,
|
page: getPageFromString(page),
|
||||||
auth: myAuth(false),
|
auth: auth,
|
||||||
};
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.send(
|
return [client.listCommunities(listCommunitiesForm)];
|
||||||
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)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
let op = wsUserOp(msg);
|
const op = wsUserOp(msg);
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
toast(i18n.t(msg.error), "danger");
|
toast(i18n.t(msg.error), "danger");
|
||||||
return;
|
} else if (op === UserOperation.ListCommunities) {
|
||||||
} else if (op == UserOperation.ListCommunities) {
|
const data = wsJsonToRes<ListCommunitiesResponse>(msg);
|
||||||
let data = wsJsonToRes<ListCommunitiesResponse>(msg);
|
|
||||||
this.setState({ listCommunitiesResponse: data, loading: false });
|
this.setState({ listCommunitiesResponse: data, loading: false });
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
} else if (op == UserOperation.FollowCommunity) {
|
} else if (op === UserOperation.FollowCommunity) {
|
||||||
let data = wsJsonToRes<CommunityResponse>(msg);
|
const {
|
||||||
let res = this.state.listCommunitiesResponse;
|
community_view: {
|
||||||
let found = res?.communities.find(
|
community,
|
||||||
c => c.community.id == data.community_view.community.id
|
subscribed,
|
||||||
|
counts: { subscribers },
|
||||||
|
},
|
||||||
|
} = wsJsonToRes<CommunityResponse>(msg);
|
||||||
|
const res = this.state.listCommunitiesResponse;
|
||||||
|
const found = res?.communities.find(
|
||||||
|
({ community: { id } }) => id == community.id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (found) {
|
if (found) {
|
||||||
found.subscribed = data.community_view.subscribed;
|
found.subscribed = subscribed;
|
||||||
found.counts.subscribers = data.community_view.counts.subscribers;
|
found.counts.subscribers = subscribers;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
import {
|
import {
|
||||||
AddModToCommunityResponse,
|
AddModToCommunityResponse,
|
||||||
BanFromCommunityResponse,
|
BanFromCommunityResponse,
|
||||||
BlockCommunityResponse,
|
BlockCommunityResponse,
|
||||||
BlockPersonResponse,
|
BlockPersonResponse,
|
||||||
CommentReportResponse,
|
|
||||||
CommentResponse,
|
CommentResponse,
|
||||||
CommentView,
|
CommentView,
|
||||||
CommunityResponse,
|
CommunityResponse,
|
||||||
|
@ -14,7 +14,6 @@ import {
|
||||||
GetCommunityResponse,
|
GetCommunityResponse,
|
||||||
GetPosts,
|
GetPosts,
|
||||||
GetPostsResponse,
|
GetPostsResponse,
|
||||||
GetSiteResponse,
|
|
||||||
ListingType,
|
ListingType,
|
||||||
PostReportResponse,
|
PostReportResponse,
|
||||||
PostResponse,
|
PostResponse,
|
||||||
|
@ -43,16 +42,20 @@ import {
|
||||||
enableDownvotes,
|
enableDownvotes,
|
||||||
enableNsfw,
|
enableNsfw,
|
||||||
fetchLimit,
|
fetchLimit,
|
||||||
getDataTypeFromProps,
|
getDataTypeString,
|
||||||
getPageFromProps,
|
getPageFromString,
|
||||||
getSortTypeFromProps,
|
getQueryParams,
|
||||||
|
getQueryString,
|
||||||
isPostBlocked,
|
isPostBlocked,
|
||||||
myAuth,
|
myAuth,
|
||||||
notifyPost,
|
notifyPost,
|
||||||
nsfwCheck,
|
nsfwCheck,
|
||||||
postToCommentSortType,
|
postToCommentSortType,
|
||||||
|
QueryParams,
|
||||||
relTags,
|
relTags,
|
||||||
restoreScrollPosition,
|
restoreScrollPosition,
|
||||||
|
routeDataTypeToEnum,
|
||||||
|
routeSortTypeToEnum,
|
||||||
saveCommentRes,
|
saveCommentRes,
|
||||||
saveScrollPosition,
|
saveScrollPosition,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
|
@ -78,16 +81,10 @@ import { CommunityLink } from "./community-link";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
communityRes?: GetCommunityResponse;
|
communityRes?: GetCommunityResponse;
|
||||||
siteRes: GetSiteResponse;
|
|
||||||
communityName: string;
|
|
||||||
communityLoading: boolean;
|
communityLoading: boolean;
|
||||||
postsLoading: boolean;
|
listingsLoading: boolean;
|
||||||
commentsLoading: boolean;
|
|
||||||
posts: PostView[];
|
posts: PostView[];
|
||||||
comments: CommentView[];
|
comments: CommentView[];
|
||||||
dataType: DataType;
|
|
||||||
sort: SortType;
|
|
||||||
page: number;
|
|
||||||
showSidebarMobile: boolean;
|
showSidebarMobile: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,30 +94,43 @@ interface CommunityProps {
|
||||||
page: number;
|
page: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UrlParams {
|
function getCommunityQueryParams() {
|
||||||
dataType?: string;
|
return getQueryParams<CommunityProps>({
|
||||||
sort?: SortType;
|
dataType: getDataTypeFromQuery,
|
||||||
page?: number;
|
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 isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
private subscription?: Subscription;
|
||||||
state: State = {
|
state: State = {
|
||||||
communityName: this.props.match.params.name,
|
|
||||||
communityLoading: true,
|
communityLoading: true,
|
||||||
postsLoading: true,
|
listingsLoading: true,
|
||||||
commentsLoading: true,
|
|
||||||
posts: [],
|
posts: [],
|
||||||
comments: [],
|
comments: [],
|
||||||
dataType: getDataTypeFromProps(this.props),
|
|
||||||
sort: getSortTypeFromProps(this.props),
|
|
||||||
page: getPageFromProps(this.props),
|
|
||||||
siteRes: this.isoData.site_res,
|
|
||||||
showSidebarMobile: false,
|
showSidebarMobile: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: RouteComponentProps<{ name: string }>, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.handleSortChange = this.handleSortChange.bind(this);
|
this.handleSortChange = this.handleSortChange.bind(this);
|
||||||
|
@ -136,8 +146,10 @@ export class Community extends Component<any, State> {
|
||||||
...this.state,
|
...this.state,
|
||||||
communityRes: this.isoData.routeData[0] as GetCommunityResponse,
|
communityRes: this.isoData.routeData[0] as GetCommunityResponse,
|
||||||
};
|
};
|
||||||
let postsRes = this.isoData.routeData[1] as GetPostsResponse | undefined;
|
const postsRes = this.isoData.routeData[1] as
|
||||||
let commentsRes = this.isoData.routeData[2] as
|
| GetPostsResponse
|
||||||
|
| undefined;
|
||||||
|
const commentsRes = this.isoData.routeData[2] as
|
||||||
| GetCommentsResponse
|
| GetCommentsResponse
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
|
@ -152,8 +164,7 @@ export class Community extends Component<any, State> {
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
communityLoading: false,
|
communityLoading: false,
|
||||||
postsLoading: false,
|
listingsLoading: false,
|
||||||
commentsLoading: false,
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.fetchCommunity();
|
this.fetchCommunity();
|
||||||
|
@ -162,8 +173,8 @@ export class Community extends Component<any, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchCommunity() {
|
fetchCommunity() {
|
||||||
let form: GetCommunity = {
|
const form: GetCommunity = {
|
||||||
name: this.state.communityName,
|
name: this.props.match.params.name,
|
||||||
auth: myAuth(false),
|
auth: myAuth(false),
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.send(wsClient.getCommunity(form));
|
WebSocketService.Instance.send(wsClient.getCommunity(form));
|
||||||
|
@ -178,95 +189,67 @@ export class Community extends Component<any, State> {
|
||||||
this.subscription?.unsubscribe();
|
this.subscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: any): CommunityProps {
|
static fetchInitialData({
|
||||||
return {
|
client,
|
||||||
dataType: getDataTypeFromProps(props),
|
path,
|
||||||
sort: getSortTypeFromProps(props),
|
query: { dataType: urlDataType, page: urlPage, sort: urlSort },
|
||||||
page: getPageFromProps(props),
|
auth,
|
||||||
};
|
}: InitialFetchRequest<QueryParams<CommunityProps>>): Promise<any>[] {
|
||||||
}
|
const pathSplit = path.split("/");
|
||||||
|
const promises: Promise<any>[] = [];
|
||||||
|
|
||||||
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
|
const communityName = pathSplit[2];
|
||||||
let pathSplit = req.path.split("/");
|
const communityForm: GetCommunity = {
|
||||||
let promises: Promise<any>[] = [];
|
|
||||||
|
|
||||||
let communityName = pathSplit[2];
|
|
||||||
let communityForm: GetCommunity = {
|
|
||||||
name: communityName,
|
name: communityName,
|
||||||
auth: req.auth,
|
auth,
|
||||||
};
|
};
|
||||||
promises.push(req.client.getCommunity(communityForm));
|
promises.push(client.getCommunity(communityForm));
|
||||||
|
|
||||||
let dataType: DataType = pathSplit[4]
|
const dataType = getDataTypeFromQuery(urlDataType);
|
||||||
? DataType[pathSplit[4]]
|
|
||||||
: DataType.Post;
|
|
||||||
|
|
||||||
let mui = UserService.Instance.myUserInfo;
|
const sort = getSortTypeFromQuery(urlSort);
|
||||||
|
|
||||||
let sort: SortType = pathSplit[6]
|
const page = getPageFromString(urlPage);
|
||||||
? SortType[pathSplit[6]]
|
|
||||||
: mui
|
|
||||||
? Object.values(SortType)[
|
|
||||||
mui.local_user_view.local_user.default_sort_type
|
|
||||||
]
|
|
||||||
: SortType.Active;
|
|
||||||
|
|
||||||
let page = pathSplit[8] ? Number(pathSplit[8]) : 1;
|
if (dataType === DataType.Post) {
|
||||||
|
const getPostsForm: GetPosts = {
|
||||||
if (dataType == DataType.Post) {
|
|
||||||
let getPostsForm: GetPosts = {
|
|
||||||
community_name: communityName,
|
community_name: communityName,
|
||||||
page,
|
page,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
sort,
|
sort,
|
||||||
type_: ListingType.All,
|
type_: ListingType.All,
|
||||||
saved_only: false,
|
saved_only: false,
|
||||||
auth: req.auth,
|
auth,
|
||||||
};
|
};
|
||||||
promises.push(req.client.getPosts(getPostsForm));
|
promises.push(client.getPosts(getPostsForm));
|
||||||
promises.push(Promise.resolve());
|
promises.push(Promise.resolve());
|
||||||
} else {
|
} else {
|
||||||
let getCommentsForm: GetComments = {
|
const getCommentsForm: GetComments = {
|
||||||
community_name: communityName,
|
community_name: communityName,
|
||||||
page,
|
page,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
sort: postToCommentSortType(sort),
|
sort: postToCommentSortType(sort),
|
||||||
type_: ListingType.All,
|
type_: ListingType.All,
|
||||||
saved_only: false,
|
saved_only: false,
|
||||||
auth: req.auth,
|
auth,
|
||||||
};
|
};
|
||||||
promises.push(Promise.resolve());
|
promises.push(Promise.resolve());
|
||||||
promises.push(req.client.getComments(getCommentsForm));
|
promises.push(client.getComments(getCommentsForm));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promises;
|
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 {
|
get documentTitle(): string {
|
||||||
let cRes = this.state.communityRes;
|
const cRes = this.state.communityRes;
|
||||||
return cRes
|
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() {
|
render() {
|
||||||
// For some reason, this returns an empty vec if it matches the site langs
|
const res = this.state.communityRes;
|
||||||
let res = this.state.communityRes;
|
const { page } = getCommunityQueryParams();
|
||||||
let communityLangs =
|
|
||||||
res?.discussion_languages.length == 0
|
|
||||||
? this.state.siteRes.all_languages.map(l => l.id)
|
|
||||||
: res?.discussion_languages;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-lg">
|
<div className="container-lg">
|
||||||
|
@ -286,7 +269,7 @@ export class Community extends Component<any, State> {
|
||||||
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12 col-md-8">
|
<div className="col-12 col-md-8">
|
||||||
{this.communityInfo()}
|
{this.communityInfo}
|
||||||
<div className="d-block d-md-none">
|
<div className="d-block d-md-none">
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary d-inline-block mb-2 mr-3"
|
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"
|
classes="icon-inline"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{this.state.showSidebarMobile && (
|
{this.state.showSidebarMobile && this.sidebar(res)}
|
||||||
<>
|
|
||||||
<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)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{this.selects()}
|
{this.selects}
|
||||||
{this.listings()}
|
{this.listings}
|
||||||
<Paginator
|
<Paginator page={page} onChange={this.handlePageChange} />
|
||||||
page={this.state.page}
|
|
||||||
onChange={this.handlePageChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="d-none d-md-block col-md-4">
|
<div className="d-none d-md-block col-md-4">
|
||||||
<Sidebar
|
{this.sidebar(res)}
|
||||||
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)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -360,43 +302,82 @@ export class Community extends Component<any, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
listings() {
|
sidebar({
|
||||||
return this.state.dataType == DataType.Post ? (
|
community_view,
|
||||||
this.state.postsLoading ? (
|
moderators,
|
||||||
<h5>
|
online,
|
||||||
<Spinner large />
|
discussion_languages,
|
||||||
</h5>
|
site,
|
||||||
) : (
|
}: GetCommunityResponse) {
|
||||||
<PostListings
|
const { site_res } = this.isoData;
|
||||||
posts={this.state.posts}
|
// For some reason, this returns an empty vec if it matches the site langs
|
||||||
removeDuplicates
|
const communityLangs =
|
||||||
enableDownvotes={enableDownvotes(this.state.siteRes)}
|
discussion_languages.length === 0
|
||||||
enableNsfw={enableNsfw(this.state.siteRes)}
|
? site_res.all_languages.map(({ id }) => id)
|
||||||
allLanguages={this.state.siteRes.all_languages}
|
: discussion_languages;
|
||||||
siteLanguages={this.state.siteRes.discussion_languages}
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar
|
||||||
|
community_view={community_view}
|
||||||
|
moderators={moderators}
|
||||||
|
admins={site_res.admins}
|
||||||
|
online={online}
|
||||||
|
enableNsfw={enableNsfw(site_res)}
|
||||||
|
editable
|
||||||
|
allLanguages={site_res.all_languages}
|
||||||
|
siteLanguages={site_res.discussion_languages}
|
||||||
|
communityLanguages={communityLangs}
|
||||||
/>
|
/>
|
||||||
)
|
{!community_view.community.local && site && (
|
||||||
) : this.state.commentsLoading ? (
|
<SiteSidebar site={site} showLocal={showLocal(this.isoData)} />
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
communityInfo() {
|
get listings() {
|
||||||
let community = this.state.communityRes?.community_view.community;
|
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 (
|
return (
|
||||||
community && (
|
community && (
|
||||||
<div className="mb-2">
|
<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 =>
|
// let communityRss = this.state.communityRes.map(r =>
|
||||||
// communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
|
// communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
|
||||||
// );
|
// );
|
||||||
let res = this.state.communityRes;
|
const { dataType, sort } = getCommunityQueryParams();
|
||||||
let communityRss = res
|
const res = this.state.communityRes;
|
||||||
? communityRSSUrl(res.community_view.community.actor_id, this.state.sort)
|
const communityRss = res
|
||||||
|
? communityRSSUrl(res.community_view.community.actor_id, sort)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<span className="mr-3">
|
<span className="mr-3">
|
||||||
<DataTypeSelect
|
<DataTypeSelect
|
||||||
type_={this.state.dataType}
|
type_={dataType}
|
||||||
onChange={this.handleDataTypeChange}
|
onChange={this.handleDataTypeChange}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span className="mr-2">
|
<span className="mr-2">
|
||||||
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
|
<SortSelect sort={sort} onChange={this.handleSortChange} />
|
||||||
</span>
|
</span>
|
||||||
{communityRss && (
|
{communityRss && (
|
||||||
<>
|
<>
|
||||||
|
@ -455,66 +437,90 @@ export class Community extends Component<any, State> {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(val: SortType) {
|
handleSortChange(sort: SortType) {
|
||||||
this.updateUrl({ sort: val, page: 1 });
|
this.updateUrl({ sort, page: 1 });
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDataTypeChange(val: DataType) {
|
handleDataTypeChange(dataType: DataType) {
|
||||||
this.updateUrl({ dataType: DataType[val], page: 1 });
|
this.updateUrl({ dataType, page: 1 });
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowSidebarMobile(i: Community) {
|
handleShowSidebarMobile(i: Community) {
|
||||||
i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
|
i.setState(({ showSidebarMobile }) => ({
|
||||||
|
showSidebarMobile: !showSidebarMobile,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl(paramUpdates: UrlParams) {
|
updateUrl({ dataType, page, sort }: Partial<CommunityProps>) {
|
||||||
const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
|
const {
|
||||||
const sortStr = paramUpdates.sort || this.state.sort;
|
dataType: urlDataType,
|
||||||
const page = paramUpdates.page || this.state.page;
|
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(
|
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() {
|
fetchData() {
|
||||||
if (this.state.dataType == DataType.Post) {
|
const { dataType, page, sort } = getCommunityQueryParams();
|
||||||
let form: GetPosts = {
|
const { name } = this.props.match.params;
|
||||||
page: this.state.page,
|
|
||||||
|
let req: string;
|
||||||
|
if (dataType === DataType.Post) {
|
||||||
|
const form: GetPosts = {
|
||||||
|
page,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
sort: this.state.sort,
|
sort,
|
||||||
type_: ListingType.All,
|
type_: ListingType.All,
|
||||||
community_name: this.state.communityName,
|
community_name: name,
|
||||||
saved_only: false,
|
saved_only: false,
|
||||||
auth: myAuth(false),
|
auth: myAuth(false),
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.send(wsClient.getPosts(form));
|
req = wsClient.getPosts(form);
|
||||||
} else {
|
} else {
|
||||||
let form: GetComments = {
|
const form: GetComments = {
|
||||||
page: this.state.page,
|
page,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
sort: postToCommentSortType(this.state.sort),
|
sort: postToCommentSortType(sort),
|
||||||
type_: ListingType.All,
|
type_: ListingType.All,
|
||||||
community_name: this.state.communityName,
|
community_name: name,
|
||||||
saved_only: false,
|
saved_only: false,
|
||||||
auth: myAuth(false),
|
auth: myAuth(false),
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.send(wsClient.getComments(form));
|
|
||||||
|
req = wsClient.getComments(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WebSocketService.Instance.send(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
let op = wsUserOp(msg);
|
const { page } = getCommunityQueryParams();
|
||||||
|
const op = wsUserOp(msg);
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
let res = this.state.communityRes;
|
const res = this.state.communityRes;
|
||||||
|
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
toast(i18n.t(msg.error), "danger");
|
toast(i18n.t(msg.error), "danger");
|
||||||
this.context.router.history.push("/");
|
this.context.router.history.push("/");
|
||||||
return;
|
|
||||||
} else if (msg.reconnect) {
|
} else if (msg.reconnect) {
|
||||||
if (res) {
|
if (res) {
|
||||||
WebSocketService.Instance.send(
|
WebSocketService.Instance.send(
|
||||||
|
@ -523,143 +529,225 @@ export class Community extends Component<any, State> {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
} else if (op == UserOperation.GetCommunity) {
|
} else {
|
||||||
let data = wsJsonToRes<GetCommunityResponse>(msg);
|
switch (op) {
|
||||||
this.setState({ communityRes: data, communityLoading: false });
|
case UserOperation.GetCommunity: {
|
||||||
// TODO why is there no auth in this form?
|
const data = wsJsonToRes<GetCommunityResponse>(msg);
|
||||||
WebSocketService.Instance.send(
|
|
||||||
wsClient.communityJoin({
|
|
||||||
community_id: data.community_view.community.id,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
op == UserOperation.EditCommunity ||
|
|
||||||
op == UserOperation.DeleteCommunity ||
|
|
||||||
op == UserOperation.RemoveCommunity
|
|
||||||
) {
|
|
||||||
let data = wsJsonToRes<CommunityResponse>(msg);
|
|
||||||
if (res) {
|
|
||||||
res.community_view = data.community_view;
|
|
||||||
res.discussion_languages = data.discussion_languages;
|
|
||||||
}
|
|
||||||
this.setState(this.state);
|
|
||||||
} else if (op == UserOperation.FollowCommunity) {
|
|
||||||
let data = wsJsonToRes<CommunityResponse>(msg);
|
|
||||||
if (res) {
|
|
||||||
res.community_view.subscribed = data.community_view.subscribed;
|
|
||||||
res.community_view.counts.subscribers =
|
|
||||||
data.community_view.counts.subscribers;
|
|
||||||
}
|
|
||||||
this.setState(this.state);
|
|
||||||
} else if (op == UserOperation.GetPosts) {
|
|
||||||
let data = wsJsonToRes<GetPostsResponse>(msg);
|
|
||||||
this.setState({ posts: data.posts, postsLoading: false });
|
|
||||||
restoreScrollPosition(this.context);
|
|
||||||
setupTippy();
|
|
||||||
} else if (
|
|
||||||
op == UserOperation.EditPost ||
|
|
||||||
op == UserOperation.DeletePost ||
|
|
||||||
op == UserOperation.RemovePost ||
|
|
||||||
op == UserOperation.LockPost ||
|
|
||||||
op == UserOperation.FeaturePost ||
|
|
||||||
op == UserOperation.SavePost
|
|
||||||
) {
|
|
||||||
let data = wsJsonToRes<PostResponse>(msg);
|
|
||||||
editPostFindRes(data.post_view, this.state.posts);
|
|
||||||
this.setState(this.state);
|
|
||||||
} else if (op == UserOperation.CreatePost) {
|
|
||||||
let data = wsJsonToRes<PostResponse>(msg);
|
|
||||||
|
|
||||||
let showPostNotifs =
|
this.setState({ communityRes: data, communityLoading: false });
|
||||||
UserService.Instance.myUserInfo?.local_user_view.local_user
|
// TODO why is there no auth in this form?
|
||||||
.show_new_post_notifs;
|
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
|
break;
|
||||||
//
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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
|
case UserOperation.EditCommunity:
|
||||||
this.state.posts
|
case UserOperation.DeleteCommunity:
|
||||||
.filter(p => p.creator.id == data.person_view.person.id)
|
case UserOperation.RemoveCommunity: {
|
||||||
.forEach(p => (p.creator_banned_from_community = data.banned));
|
const { community_view, discussion_languages } =
|
||||||
|
wsJsonToRes<CommunityResponse>(msg);
|
||||||
|
|
||||||
this.setState(this.state);
|
if (res) {
|
||||||
} else if (op == UserOperation.GetComments) {
|
res.community_view = community_view;
|
||||||
let data = wsJsonToRes<GetCommentsResponse>(msg);
|
res.discussion_languages = discussion_languages;
|
||||||
this.setState({ comments: data.comments, commentsLoading: false });
|
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.comments);
|
|
||||||
this.setState(this.state);
|
|
||||||
} else if (op == UserOperation.CreateComment) {
|
|
||||||
let data = wsJsonToRes<CommentResponse>(msg);
|
|
||||||
|
|
||||||
// Necessary since it might be a user reply
|
break;
|
||||||
if (data.form_id) {
|
}
|
||||||
this.state.comments.unshift(data.comment_view);
|
|
||||||
this.setState(this.state);
|
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 ${
|
className={`btn btn-secondary btn-block mb-2 ${
|
||||||
cv.community.deleted || cv.community.removed ? "no-click" : ""
|
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")}
|
{i18n.t("create_a_post")}
|
||||||
</Link>
|
</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 { Component, linkEvent } from "inferno";
|
||||||
import { Link } from "inferno-router";
|
import { Link } from "inferno-router";
|
||||||
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
import {
|
import {
|
||||||
AddAdminResponse,
|
AddAdminResponse,
|
||||||
BanPerson,
|
BanPerson,
|
||||||
|
@ -7,6 +10,8 @@ import {
|
||||||
BlockPerson,
|
BlockPerson,
|
||||||
BlockPersonResponse,
|
BlockPersonResponse,
|
||||||
CommentResponse,
|
CommentResponse,
|
||||||
|
CommunityModeratorView,
|
||||||
|
CommunitySafe,
|
||||||
GetPersonDetails,
|
GetPersonDetails,
|
||||||
GetPersonDetailsResponse,
|
GetPersonDetailsResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
@ -33,12 +38,15 @@ import {
|
||||||
enableNsfw,
|
enableNsfw,
|
||||||
fetchLimit,
|
fetchLimit,
|
||||||
futureDaysToUnixTime,
|
futureDaysToUnixTime,
|
||||||
getUsernameFromProps,
|
getPageFromString,
|
||||||
|
getQueryParams,
|
||||||
|
getQueryString,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isBanned,
|
isBanned,
|
||||||
mdToHtml,
|
mdToHtml,
|
||||||
myAuth,
|
myAuth,
|
||||||
numToSI,
|
numToSI,
|
||||||
|
QueryParams,
|
||||||
relTags,
|
relTags,
|
||||||
restoreScrollPosition,
|
restoreScrollPosition,
|
||||||
routeSortTypeToEnum,
|
routeSortTypeToEnum,
|
||||||
|
@ -62,10 +70,6 @@ import { PersonListing } from "./person-listing";
|
||||||
|
|
||||||
interface ProfileState {
|
interface ProfileState {
|
||||||
personRes?: GetPersonDetailsResponse;
|
personRes?: GetPersonDetailsResponse;
|
||||||
userName: string;
|
|
||||||
view: PersonDetailsView;
|
|
||||||
sort: SortType;
|
|
||||||
page: number;
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
personBlocked: boolean;
|
personBlocked: boolean;
|
||||||
banReason?: string;
|
banReason?: string;
|
||||||
|
@ -79,32 +83,84 @@ interface ProfileProps {
|
||||||
view: PersonDetailsView;
|
view: PersonDetailsView;
|
||||||
sort: SortType;
|
sort: SortType;
|
||||||
page: number;
|
page: number;
|
||||||
person_id?: number;
|
|
||||||
username: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UrlParams {
|
const getProfileQueryParams = () =>
|
||||||
view?: string;
|
getQueryParams<ProfileProps>({
|
||||||
sort?: SortType;
|
view: getViewFromProps,
|
||||||
page?: number;
|
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 isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
private subscription?: Subscription;
|
||||||
state: ProfileState = {
|
state: ProfileState = {
|
||||||
userName: getUsernameFromProps(this.props),
|
|
||||||
loading: true,
|
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,
|
personBlocked: false,
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
showBanDialog: false,
|
showBanDialog: false,
|
||||||
removeData: false,
|
removeData: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: RouteComponentProps<{ username: string }>, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.handleSortChange = this.handleSortChange.bind(this);
|
this.handleSortChange = this.handleSortChange.bind(this);
|
||||||
|
@ -114,7 +170,7 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
this.subscription = wsSubscribe(this.parseMessage);
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// 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 = {
|
||||||
...this.state,
|
...this.state,
|
||||||
personRes: this.isoData.routeData[0] as GetPersonDetailsResponse,
|
personRes: this.isoData.routeData[0] as GetPersonDetailsResponse,
|
||||||
|
@ -126,65 +182,61 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchUserData() {
|
fetchUserData() {
|
||||||
let form: GetPersonDetails = {
|
const { page, sort, view } = getProfileQueryParams();
|
||||||
username: this.state.userName,
|
|
||||||
sort: this.state.sort,
|
const form: GetPersonDetails = {
|
||||||
saved_only: this.state.view === PersonDetailsView.Saved,
|
username: this.props.match.params.username,
|
||||||
page: this.state.page,
|
sort,
|
||||||
|
saved_only: view === PersonDetailsView.Saved,
|
||||||
|
page,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
auth: myAuth(false),
|
auth: myAuth(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.send(wsClient.getPersonDetails(form));
|
WebSocketService.Instance.send(wsClient.getPersonDetails(form));
|
||||||
}
|
}
|
||||||
|
|
||||||
get amCurrentUser() {
|
get amCurrentUser() {
|
||||||
return (
|
return (
|
||||||
UserService.Instance.myUserInfo?.local_user_view.person.id ==
|
UserService.Instance.myUserInfo?.local_user_view.person.id ===
|
||||||
this.state.personRes?.person_view.person.id
|
this.state.personRes?.person_view.person.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setPersonBlock() {
|
setPersonBlock() {
|
||||||
let mui = UserService.Instance.myUserInfo;
|
const mui = UserService.Instance.myUserInfo;
|
||||||
let res = this.state.personRes;
|
const res = this.state.personRes;
|
||||||
|
|
||||||
if (mui && res) {
|
if (mui && res) {
|
||||||
this.setState({
|
this.setState({
|
||||||
personBlocked: mui.person_blocks
|
personBlocked: mui.person_blocks.some(
|
||||||
.map(a => a.target.id)
|
({ target: { id } }) => id === res.person_view.person.id
|
||||||
.includes(res.person_view.person.id),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getViewFromProps(view: string): PersonDetailsView {
|
static fetchInitialData({
|
||||||
return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
|
client,
|
||||||
}
|
path,
|
||||||
|
query: { page, sort, view: urlView },
|
||||||
|
auth,
|
||||||
|
}: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<any>[] {
|
||||||
|
const pathSplit = path.split("/");
|
||||||
|
|
||||||
static getSortTypeFromProps(sort: string): SortType {
|
const username = pathSplit[2];
|
||||||
return sort ? routeSortTypeToEnum(sort) : SortType.New;
|
const view = getViewFromProps(urlView);
|
||||||
}
|
|
||||||
|
|
||||||
static getPageFromProps(page: number): number {
|
const form: GetPersonDetails = {
|
||||||
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 = {
|
|
||||||
username: username,
|
username: username,
|
||||||
sort,
|
sort: getSortTypeFromQuery(sort),
|
||||||
saved_only: view === PersonDetailsView.Saved,
|
saved_only: view === PersonDetailsView.Saved,
|
||||||
page,
|
page: getPageFromString(page),
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
auth: req.auth,
|
auth,
|
||||||
};
|
};
|
||||||
return [req.client.getPersonDetails(form)];
|
|
||||||
|
return [client.getPersonDetails(form)];
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -197,78 +249,59 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
saveScrollPosition(this.context);
|
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 {
|
get documentTitle(): string {
|
||||||
let res = this.state.personRes;
|
const res = this.state.personRes;
|
||||||
return res
|
return res
|
||||||
? `@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`
|
? `@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`
|
||||||
: "";
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let res = this.state.personRes;
|
const { personRes, loading, siteRes } = this.state;
|
||||||
|
const { page, sort, view } = getProfileQueryParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-lg">
|
<div className="container-lg">
|
||||||
{this.state.loading ? (
|
{loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
<Spinner large />
|
<Spinner large />
|
||||||
</h5>
|
</h5>
|
||||||
) : (
|
) : (
|
||||||
res && (
|
personRes && (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12 col-md-8">
|
<div className="col-12 col-md-8">
|
||||||
<>
|
<HtmlTags
|
||||||
<HtmlTags
|
title={this.documentTitle}
|
||||||
title={this.documentTitle}
|
path={this.context.router.route.match.url}
|
||||||
path={this.context.router.route.match.url}
|
description={personRes.person_view.person.bio}
|
||||||
description={res.person_view.person.bio}
|
image={personRes.person_view.person.avatar}
|
||||||
image={res.person_view.person.avatar}
|
/>
|
||||||
/>
|
|
||||||
{this.userInfo()}
|
{this.userInfo}
|
||||||
<hr />
|
|
||||||
</>
|
<hr />
|
||||||
{!this.state.loading && this.selects()}
|
|
||||||
|
{this.selects}
|
||||||
|
|
||||||
<PersonDetails
|
<PersonDetails
|
||||||
personRes={res}
|
personRes={personRes}
|
||||||
admins={this.state.siteRes.admins}
|
admins={siteRes.admins}
|
||||||
sort={this.state.sort}
|
sort={sort}
|
||||||
page={this.state.page}
|
page={page}
|
||||||
limit={fetchLimit}
|
limit={fetchLimit}
|
||||||
enableDownvotes={enableDownvotes(this.state.siteRes)}
|
enableDownvotes={enableDownvotes(siteRes)}
|
||||||
enableNsfw={enableNsfw(this.state.siteRes)}
|
enableNsfw={enableNsfw(siteRes)}
|
||||||
view={this.state.view}
|
view={view}
|
||||||
onPageChange={this.handlePageChange}
|
onPageChange={this.handlePageChange}
|
||||||
allLanguages={this.state.siteRes.all_languages}
|
allLanguages={siteRes.all_languages}
|
||||||
siteLanguages={this.state.siteRes.discussion_languages}
|
siteLanguages={siteRes.discussion_languages}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!this.state.loading && (
|
<div className="col-12 col-md-4">
|
||||||
<div className="col-12 col-md-4">
|
<Moderates moderates={personRes.moderates} />
|
||||||
{this.moderates()}
|
{this.amCurrentUser && <Follows />}
|
||||||
{this.amCurrentUser && this.follows()}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
@ -276,73 +309,49 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewRadios() {
|
get viewRadios() {
|
||||||
return (
|
return (
|
||||||
<div className="btn-group btn-group-toggle flex-wrap mb-2">
|
<div className="btn-group btn-group-toggle flex-wrap mb-2">
|
||||||
<label
|
{this.getRadio(PersonDetailsView.Overview)}
|
||||||
className={`btn btn-outline-secondary pointer
|
{this.getRadio(PersonDetailsView.Comments)}
|
||||||
${this.state.view == PersonDetailsView.Overview && "active"}
|
{this.getRadio(PersonDetailsView.Posts)}
|
||||||
`}
|
{this.getRadio(PersonDetailsView.Saved)}
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
selects() {
|
getRadio(view: PersonDetailsView) {
|
||||||
let profileRss = `/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`;
|
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 (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<span className="mr-3">{this.viewRadios()}</span>
|
<span className="mr-3">{this.viewRadios}</span>
|
||||||
<SortSelect
|
<SortSelect
|
||||||
sort={this.state.sort}
|
sort={sort}
|
||||||
onChange={this.handleSortChange}
|
onChange={this.handleSortChange}
|
||||||
hideHot
|
hideHot
|
||||||
hideMostComments
|
hideMostComments
|
||||||
|
@ -354,33 +363,15 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
</div>
|
</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() {
|
get userInfo() {
|
||||||
let pv = this.state.personRes?.person_view;
|
const pv = this.state.personRes?.person_view;
|
||||||
|
const {
|
||||||
|
personBlocked,
|
||||||
|
siteRes: { admins },
|
||||||
|
showBanDialog,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
pv && (
|
pv && (
|
||||||
<div>
|
<div>
|
||||||
|
@ -429,7 +420,7 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{this.banDialog()}
|
{this.banDialog}
|
||||||
<div className="flex-grow-1 unselectable pointer mx-2"></div>
|
<div className="flex-grow-1 unselectable pointer mx-2"></div>
|
||||||
{!this.amCurrentUser && UserService.Instance.myUserInfo && (
|
{!this.amCurrentUser && UserService.Instance.myUserInfo && (
|
||||||
<>
|
<>
|
||||||
|
@ -446,19 +437,16 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
className={
|
className={
|
||||||
"d-flex align-self-start btn btn-secondary mr-2"
|
"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")}
|
{i18n.t("send_message")}
|
||||||
</Link>
|
</Link>
|
||||||
{this.state.personBlocked ? (
|
{personBlocked ? (
|
||||||
<button
|
<button
|
||||||
className={
|
className={
|
||||||
"d-flex align-self-start btn btn-secondary mr-2"
|
"d-flex align-self-start btn btn-secondary mr-2"
|
||||||
}
|
}
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(pv.person.id, handleUnblockPerson)}
|
||||||
pv.person.id,
|
|
||||||
this.handleUnblockPerson
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{i18n.t("unblock_user")}
|
{i18n.t("unblock_user")}
|
||||||
</button>
|
</button>
|
||||||
|
@ -467,10 +455,7 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
className={
|
className={
|
||||||
"d-flex align-self-start btn btn-secondary mr-2"
|
"d-flex align-self-start btn btn-secondary mr-2"
|
||||||
}
|
}
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(pv.person.id, handleBlockPerson)}
|
||||||
pv.person.id,
|
|
||||||
this.handleBlockPerson
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{i18n.t("block_user")}
|
{i18n.t("block_user")}
|
||||||
</button>
|
</button>
|
||||||
|
@ -478,9 +463,9 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canMod(pv.person.id, undefined, this.state.siteRes.admins) &&
|
{canMod(pv.person.id, undefined, admins) &&
|
||||||
!isAdmin(pv.person.id, this.state.siteRes.admins) &&
|
!isAdmin(pv.person.id, admins) &&
|
||||||
!this.state.showBanDialog &&
|
!showBanDialog &&
|
||||||
(!isBanned(pv.person) ? (
|
(!isBanned(pv.person) ? (
|
||||||
<button
|
<button
|
||||||
className={
|
className={
|
||||||
|
@ -552,12 +537,14 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
banDialog() {
|
get banDialog() {
|
||||||
let pv = this.state.personRes?.person_view;
|
const pv = this.state.personRes?.person_view;
|
||||||
|
const { showBanDialog } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
pv && (
|
pv && (
|
||||||
<>
|
<>
|
||||||
{this.state.showBanDialog && (
|
{showBanDialog && (
|
||||||
<form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
|
||||||
<div className="form-group row col-12">
|
<div className="form-group row col-12">
|
||||||
<label className="col-form-label" htmlFor="profile-ban-reason">
|
<label className="col-form-label" htmlFor="profile-ban-reason">
|
||||||
|
@ -630,73 +617,38 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
moderates() {
|
updateUrl({ page, sort, view }: Partial<ProfileProps>) {
|
||||||
let moderates = this.state.personRes?.moderates;
|
const {
|
||||||
return (
|
page: urlPage,
|
||||||
moderates &&
|
sort: urlSort,
|
||||||
moderates.length > 0 && (
|
view: urlView,
|
||||||
<div className="card border-secondary mb-3">
|
} = getProfileQueryParams();
|
||||||
<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>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
follows() {
|
const queryParams: QueryParams<ProfileProps> = {
|
||||||
let follows = UserService.Instance.myUserInfo?.follows;
|
page: (page ?? urlPage).toString(),
|
||||||
return (
|
sort: sort ?? urlSort,
|
||||||
follows &&
|
view: view ?? urlView,
|
||||||
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>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUrl(paramUpdates: UrlParams) {
|
const { username } = this.props.match.params;
|
||||||
const page = paramUpdates.page || this.state.page;
|
|
||||||
const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
|
|
||||||
const sortStr = paramUpdates.sort || this.state.sort;
|
|
||||||
|
|
||||||
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.setState({ loading: true });
|
||||||
this.fetchUserData();
|
this.fetchUserData();
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePageChange(page: number) {
|
handlePageChange(page: number) {
|
||||||
this.updateUrl({ page: page });
|
this.updateUrl({ page });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(val: SortType) {
|
handleSortChange(sort: SortType) {
|
||||||
this.updateUrl({ sort: val, page: 1 });
|
this.updateUrl({ sort, page: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleViewChange(i: Profile, event: any) {
|
handleViewChange(i: Profile, event: any) {
|
||||||
i.updateUrl({
|
i.updateUrl({
|
||||||
view: PersonDetailsView[Number(event.target.value)],
|
view: PersonDetailsView[event.target.value],
|
||||||
page: 1,
|
page: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -724,20 +676,25 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
|
|
||||||
handleModBanSubmit(i: Profile, event?: any) {
|
handleModBanSubmit(i: Profile, event?: any) {
|
||||||
if (event) event.preventDefault();
|
if (event) event.preventDefault();
|
||||||
let person = i.state.personRes?.person_view.person;
|
const { personRes, removeData, banReason, banExpireDays } = i.state;
|
||||||
let auth = myAuth();
|
|
||||||
|
const person = personRes?.person_view.person;
|
||||||
|
const auth = myAuth();
|
||||||
|
|
||||||
if (person && auth) {
|
if (person && auth) {
|
||||||
|
const ban = !person.banned;
|
||||||
|
|
||||||
// If its an unban, restore all their data
|
// If its an unban, restore all their data
|
||||||
let ban = !person.banned;
|
if (!ban) {
|
||||||
if (ban == false) {
|
|
||||||
i.setState({ removeData: false });
|
i.setState({ removeData: false });
|
||||||
}
|
}
|
||||||
let form: BanPerson = {
|
|
||||||
|
const form: BanPerson = {
|
||||||
person_id: person.id,
|
person_id: person.id,
|
||||||
ban,
|
ban,
|
||||||
remove_data: i.state.removeData,
|
remove_data: removeData,
|
||||||
reason: i.state.banReason,
|
reason: banReason,
|
||||||
expires: futureDaysToUnixTime(i.state.banExpireDays),
|
expires: futureDaysToUnixTime(banExpireDays),
|
||||||
auth,
|
auth,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.send(wsClient.banPerson(form));
|
WebSocketService.Instance.send(wsClient.banPerson(form));
|
||||||
|
@ -747,94 +704,138 @@ export class Profile extends Component<any, ProfileState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
let op = wsUserOp(msg);
|
const op = wsUserOp(msg);
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
|
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
toast(i18n.t(msg.error), "danger");
|
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("/");
|
this.context.router.history.push("/");
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
} else if (msg.reconnect) {
|
} else if (msg.reconnect) {
|
||||||
this.fetchUserData();
|
this.fetchUserData();
|
||||||
} else if (op == UserOperation.GetPersonDetails) {
|
} else {
|
||||||
// Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
|
switch (op) {
|
||||||
// and set the parent state if it is not set or differs
|
case UserOperation.GetPersonDetails: {
|
||||||
// TODO this might need to get abstracted
|
// Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
|
||||||
let data = wsJsonToRes<GetPersonDetailsResponse>(msg);
|
// and set the parent state if it is not set or differs
|
||||||
this.setState({ personRes: data, loading: false });
|
// TODO this might need to get abstracted
|
||||||
this.setPersonBlock();
|
const data = wsJsonToRes<GetPersonDetailsResponse>(msg);
|
||||||
restoreScrollPosition(this.context);
|
this.setState({ personRes: data, loading: false });
|
||||||
} else if (op == UserOperation.AddAdmin) {
|
this.setPersonBlock();
|
||||||
let data = wsJsonToRes<AddAdminResponse>(msg);
|
restoreScrollPosition(this.context);
|
||||||
this.setState(s => ((s.siteRes.admins = data.admins), s));
|
|
||||||
} else if (op == UserOperation.CreateCommentLike) {
|
|
||||||
let data = wsJsonToRes<CommentResponse>(msg);
|
|
||||||
createCommentLikeRes(data.comment_view, this.state.personRes?.comments);
|
|
||||||
this.setState(this.state);
|
|
||||||
} else if (
|
|
||||||
op == UserOperation.EditComment ||
|
|
||||||
op == UserOperation.DeleteComment ||
|
|
||||||
op == UserOperation.RemoveComment
|
|
||||||
) {
|
|
||||||
let data = wsJsonToRes<CommentResponse>(msg);
|
|
||||||
editCommentRes(data.comment_view, this.state.personRes?.comments);
|
|
||||||
this.setState(this.state);
|
|
||||||
} else if (op == UserOperation.CreateComment) {
|
|
||||||
let data = wsJsonToRes<CommentResponse>(msg);
|
|
||||||
let mui = UserService.Instance.myUserInfo;
|
|
||||||
if (data.comment_view.creator.id == mui?.local_user_view.person.id) {
|
|
||||||
toast(i18n.t("reply_sent"));
|
|
||||||
}
|
|
||||||
} else if (op == UserOperation.SaveComment) {
|
|
||||||
let data = wsJsonToRes<CommentResponse>(msg);
|
|
||||||
saveCommentRes(data.comment_view, this.state.personRes?.comments);
|
|
||||||
this.setState(this.state);
|
|
||||||
} else if (
|
|
||||||
op == UserOperation.EditPost ||
|
|
||||||
op == UserOperation.DeletePost ||
|
|
||||||
op == UserOperation.RemovePost ||
|
|
||||||
op == UserOperation.LockPost ||
|
|
||||||
op == UserOperation.FeaturePost ||
|
|
||||||
op == UserOperation.SavePost
|
|
||||||
) {
|
|
||||||
let data = wsJsonToRes<PostResponse>(msg);
|
|
||||||
editPostFindRes(data.post_view, this.state.personRes?.posts);
|
|
||||||
this.setState(this.state);
|
|
||||||
} else if (op == UserOperation.CreatePostLike) {
|
|
||||||
let data = wsJsonToRes<PostResponse>(msg);
|
|
||||||
createPostLikeFindRes(data.post_view, this.state.personRes?.posts);
|
|
||||||
this.setState(this.state);
|
|
||||||
} else if (op == UserOperation.BanPerson) {
|
|
||||||
let data = wsJsonToRes<BanPersonResponse>(msg);
|
|
||||||
let res = this.state.personRes;
|
|
||||||
res?.comments
|
|
||||||
.filter(c => c.creator.id == data.person_view.person.id)
|
|
||||||
.forEach(c => (c.creator.banned = data.banned));
|
|
||||||
res?.posts
|
|
||||||
.filter(c => c.creator.id == data.person_view.person.id)
|
|
||||||
.forEach(c => (c.creator.banned = data.banned));
|
|
||||||
let pv = res?.person_view;
|
|
||||||
|
|
||||||
if (pv?.person.id == data.person_view.person.id) {
|
break;
|
||||||
pv.person.banned = data.banned;
|
}
|
||||||
}
|
|
||||||
this.setState(this.state);
|
case UserOperation.AddAdmin: {
|
||||||
} else if (op == UserOperation.BlockPerson) {
|
const { admins } = wsJsonToRes<AddAdminResponse>(msg);
|
||||||
let data = wsJsonToRes<BlockPersonResponse>(msg);
|
this.setState(s => ((s.siteRes.admins = admins), s));
|
||||||
updatePersonBlock(data);
|
|
||||||
this.setPersonBlock();
|
break;
|
||||||
this.setState(this.state);
|
}
|
||||||
} else if (
|
|
||||||
op == UserOperation.PurgePerson ||
|
case UserOperation.CreateCommentLike: {
|
||||||
op == UserOperation.PurgePost ||
|
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
|
||||||
op == UserOperation.PurgeComment ||
|
createCommentLikeRes(comment_view, this.state.personRes?.comments);
|
||||||
op == UserOperation.PurgeCommunity
|
this.setState(this.state);
|
||||||
) {
|
|
||||||
let data = wsJsonToRes<PurgeItemResponse>(msg);
|
break;
|
||||||
if (data.success) {
|
}
|
||||||
toast(i18n.t("purge_success"));
|
|
||||||
this.context.router.history.push(`/`);
|
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 { Component, linkEvent } from "inferno";
|
||||||
import {
|
import {
|
||||||
BlockCommunity,
|
BlockCommunity,
|
||||||
|
@ -6,13 +7,11 @@ import {
|
||||||
BlockPersonResponse,
|
BlockPersonResponse,
|
||||||
ChangePassword,
|
ChangePassword,
|
||||||
CommunityBlockView,
|
CommunityBlockView,
|
||||||
CommunityView,
|
|
||||||
DeleteAccount,
|
DeleteAccount,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
ListingType,
|
ListingType,
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
PersonBlockView,
|
PersonBlockView,
|
||||||
PersonViewSafe,
|
|
||||||
SaveUserSettings,
|
SaveUserSettings,
|
||||||
SortType,
|
SortType,
|
||||||
UserOperation,
|
UserOperation,
|
||||||
|
@ -24,19 +23,17 @@ import { i18n, languages } from "../../i18next";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { UserService, WebSocketService } from "../../services";
|
||||||
import {
|
import {
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
choicesConfig,
|
Choice,
|
||||||
communitySelectName,
|
|
||||||
communityToChoice,
|
communityToChoice,
|
||||||
debounce,
|
debounce,
|
||||||
elementUrl,
|
elementUrl,
|
||||||
|
emDash,
|
||||||
enableNsfw,
|
enableNsfw,
|
||||||
fetchCommunities,
|
fetchCommunities,
|
||||||
fetchThemeList,
|
fetchThemeList,
|
||||||
fetchUsers,
|
fetchUsers,
|
||||||
getLanguages,
|
getLanguages,
|
||||||
isBrowser,
|
|
||||||
myAuth,
|
myAuth,
|
||||||
personSelectName,
|
|
||||||
personToChoice,
|
personToChoice,
|
||||||
relTags,
|
relTags,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
|
@ -55,15 +52,11 @@ import { ImageUploadForm } from "../common/image-upload-form";
|
||||||
import { LanguageSelect } from "../common/language-select";
|
import { LanguageSelect } from "../common/language-select";
|
||||||
import { ListingTypeSelect } from "../common/listing-type-select";
|
import { ListingTypeSelect } from "../common/listing-type-select";
|
||||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||||
|
import { SearchableSelect } from "../common/searchable-select";
|
||||||
import { SortSelect } from "../common/sort-select";
|
import { SortSelect } from "../common/sort-select";
|
||||||
import { CommunityLink } from "../community/community-link";
|
import { CommunityLink } from "../community/community-link";
|
||||||
import { PersonListing } from "./person-listing";
|
import { PersonListing } from "./person-listing";
|
||||||
|
|
||||||
var Choices: any;
|
|
||||||
if (isBrowser()) {
|
|
||||||
Choices = require("choices.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
// TODO redo these forms
|
// TODO redo these forms
|
||||||
saveUserSettingsForm: {
|
saveUserSettingsForm: {
|
||||||
|
@ -97,10 +90,7 @@ interface SettingsState {
|
||||||
password?: string;
|
password?: string;
|
||||||
};
|
};
|
||||||
personBlocks: PersonBlockView[];
|
personBlocks: PersonBlockView[];
|
||||||
blockPerson?: PersonViewSafe;
|
|
||||||
communityBlocks: CommunityBlockView[];
|
communityBlocks: CommunityBlockView[];
|
||||||
blockCommunityId: number;
|
|
||||||
blockCommunity?: CommunityView;
|
|
||||||
currentTab: string;
|
currentTab: string;
|
||||||
themeList: string[];
|
themeList: string[];
|
||||||
saveUserSettingsLoading: boolean;
|
saveUserSettingsLoading: boolean;
|
||||||
|
@ -108,12 +98,50 @@ interface SettingsState {
|
||||||
deleteAccountLoading: boolean;
|
deleteAccountLoading: boolean;
|
||||||
deleteAccountShowConfirm: boolean;
|
deleteAccountShowConfirm: boolean;
|
||||||
siteRes: GetSiteResponse;
|
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> {
|
export class Settings extends Component<any, SettingsState> {
|
||||||
private isoData = setIsoData(this.context);
|
private isoData = setIsoData(this.context);
|
||||||
private blockPersonChoices: any;
|
|
||||||
private blockCommunityChoices: any;
|
|
||||||
private subscription?: Subscription;
|
private subscription?: Subscription;
|
||||||
state: SettingsState = {
|
state: SettingsState = {
|
||||||
saveUserSettingsForm: {},
|
saveUserSettingsForm: {},
|
||||||
|
@ -125,10 +153,13 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
deleteAccountForm: {},
|
deleteAccountForm: {},
|
||||||
personBlocks: [],
|
personBlocks: [],
|
||||||
communityBlocks: [],
|
communityBlocks: [],
|
||||||
blockCommunityId: 0,
|
|
||||||
currentTab: "settings",
|
currentTab: "settings",
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
themeList: [],
|
themeList: [],
|
||||||
|
searchCommunityLoading: false,
|
||||||
|
searchCommunityOptions: [],
|
||||||
|
searchPersonLoading: false,
|
||||||
|
searchPersonOptions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -149,35 +180,58 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
this.parseMessage = this.parseMessage.bind(this);
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
this.subscription = wsSubscribe(this.parseMessage);
|
||||||
|
|
||||||
let mui = UserService.Instance.myUserInfo;
|
const mui = UserService.Instance.myUserInfo;
|
||||||
if (mui) {
|
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 = {
|
||||||
...this.state,
|
...this.state,
|
||||||
personBlocks: mui.person_blocks,
|
personBlocks: mui.person_blocks,
|
||||||
communityBlocks: mui.community_blocks,
|
communityBlocks: mui.community_blocks,
|
||||||
saveUserSettingsForm: {
|
saveUserSettingsForm: {
|
||||||
...this.state.saveUserSettingsForm,
|
...this.state.saveUserSettingsForm,
|
||||||
show_nsfw: luv.local_user.show_nsfw,
|
show_nsfw,
|
||||||
theme: luv.local_user.theme ? luv.local_user.theme : "browser",
|
theme: theme ?? "browser",
|
||||||
default_sort_type: luv.local_user.default_sort_type,
|
default_sort_type,
|
||||||
default_listing_type: luv.local_user.default_listing_type,
|
default_listing_type,
|
||||||
interface_language: luv.local_user.interface_language,
|
interface_language,
|
||||||
discussion_languages: mui.discussion_languages,
|
discussion_languages: mui.discussion_languages,
|
||||||
avatar: luv.person.avatar,
|
avatar,
|
||||||
banner: luv.person.banner,
|
banner,
|
||||||
display_name: luv.person.display_name,
|
display_name,
|
||||||
show_avatars: luv.local_user.show_avatars,
|
show_avatars,
|
||||||
bot_account: luv.person.bot_account,
|
bot_account,
|
||||||
show_bot_accounts: luv.local_user.show_bot_accounts,
|
show_bot_accounts,
|
||||||
show_scores: luv.local_user.show_scores,
|
show_scores,
|
||||||
show_read_posts: luv.local_user.show_read_posts,
|
show_read_posts,
|
||||||
show_new_post_notifs: luv.local_user.show_new_post_notifs,
|
show_new_post_notifs,
|
||||||
email: luv.local_user.email,
|
email,
|
||||||
bio: luv.person.bio,
|
bio,
|
||||||
send_notifications_to_email:
|
send_notifications_to_email,
|
||||||
luv.local_user.send_notifications_to_email,
|
matrix_user_id,
|
||||||
matrix_user_id: luv.person.matrix_user_id,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -349,9 +403,17 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
blockUserCard() {
|
blockUserCard() {
|
||||||
|
const { searchPersonLoading, searchPersonOptions } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.blockUserForm()}
|
<Filter
|
||||||
|
filterType="user"
|
||||||
|
loading={searchPersonLoading}
|
||||||
|
onChange={this.handleBlockPerson}
|
||||||
|
onSearch={this.handlePersonSearch}
|
||||||
|
options={searchPersonOptions}
|
||||||
|
/>
|
||||||
{this.blockedUsersList()}
|
{this.blockedUsersList()}
|
||||||
</div>
|
</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() {
|
blockCommunityCard() {
|
||||||
|
const { searchCommunityLoading, searchCommunityOptions } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.blockCommunityForm()}
|
<Filter
|
||||||
|
filterType="community"
|
||||||
|
loading={searchCommunityLoading}
|
||||||
|
onChange={this.handleBlockCommunity}
|
||||||
|
onSearch={this.handleCommunitySearch}
|
||||||
|
options={searchCommunityOptions}
|
||||||
|
/>
|
||||||
{this.blockedCommunitiesList()}
|
{this.blockedCommunitiesList()}
|
||||||
</div>
|
</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() {
|
saveUserSettingsHtmlForm() {
|
||||||
let selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
|
let selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
|
||||||
|
|
||||||
|
@ -907,91 +922,57 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupBlockPersonChoices() {
|
handlePersonSearch = debounce(async (text: string) => {
|
||||||
if (isBrowser()) {
|
this.setState({ searchPersonLoading: true });
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupBlockCommunityChoices() {
|
const searchPersonOptions: Choice[] = [];
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBlockPerson(personId: number) {
|
if (text.length > 0) {
|
||||||
let auth = myAuth();
|
searchPersonOptions.push(
|
||||||
if (auth && personId != 0) {
|
...(await fetchUsers(text)).users.map(personToChoice)
|
||||||
let blockUserForm: BlockPerson = {
|
);
|
||||||
person_id: personId,
|
}
|
||||||
|
|
||||||
|
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,
|
block: true,
|
||||||
auth,
|
auth,
|
||||||
};
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
|
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
|
handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
|
||||||
let auth = myAuth();
|
const auth = myAuth();
|
||||||
if (auth) {
|
if (auth) {
|
||||||
let blockUserForm: BlockPerson = {
|
const blockUserForm: BlockPerson = {
|
||||||
person_id: i.recipientId,
|
person_id: i.recipientId,
|
||||||
block: false,
|
block: false,
|
||||||
auth,
|
auth,
|
||||||
|
@ -1000,11 +981,11 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBlockCommunity(community_id: number) {
|
handleBlockCommunity({ value }: Choice) {
|
||||||
let auth = myAuth();
|
const auth = myAuth();
|
||||||
if (auth && community_id != 0) {
|
if (auth && value !== "0") {
|
||||||
let blockCommunityForm: BlockCommunity = {
|
const blockCommunityForm: BlockCommunity = {
|
||||||
community_id,
|
community_id: Number(value),
|
||||||
block: true,
|
block: true,
|
||||||
auth,
|
auth,
|
||||||
};
|
};
|
||||||
|
@ -1015,9 +996,9 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
|
handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
|
||||||
let auth = myAuth();
|
const auth = myAuth();
|
||||||
if (auth) {
|
if (auth) {
|
||||||
let blockCommunityForm: BlockCommunity = {
|
const blockCommunityForm: BlockCommunity = {
|
||||||
community_id: i.communityId,
|
community_id: i.communityId,
|
||||||
block: false,
|
block: false,
|
||||||
auth,
|
auth,
|
||||||
|
@ -1249,11 +1230,6 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
|
|
||||||
handleSwitchTab(i: { ctx: Settings; tab: string }) {
|
handleSwitchTab(i: { ctx: Settings; tab: string }) {
|
||||||
i.ctx.setState({ currentTab: i.tab });
|
i.ctx.setState({ currentTab: i.tab });
|
||||||
|
|
||||||
if (i.ctx.state.currentTab == "blocks") {
|
|
||||||
i.ctx.setupBlockPersonChoices();
|
|
||||||
i.ctx.setupBlockCommunityChoices();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
import {
|
import {
|
||||||
GetCommunity,
|
GetCommunity,
|
||||||
GetCommunityResponse,
|
GetCommunityResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
ListCommunities,
|
|
||||||
ListCommunitiesResponse,
|
|
||||||
ListingType,
|
|
||||||
PostView,
|
PostView,
|
||||||
SortType,
|
|
||||||
UserOperation,
|
UserOperation,
|
||||||
wsJsonToRes,
|
wsJsonToRes,
|
||||||
wsUserOp,
|
wsUserOp,
|
||||||
|
@ -17,11 +14,15 @@ import { InitialFetchRequest, PostFormParams } from "shared/interfaces";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { UserService, WebSocketService } from "../../services";
|
||||||
import {
|
import {
|
||||||
|
Choice,
|
||||||
enableDownvotes,
|
enableDownvotes,
|
||||||
enableNsfw,
|
enableNsfw,
|
||||||
fetchLimit,
|
getIdFromString,
|
||||||
|
getQueryParams,
|
||||||
|
getQueryString,
|
||||||
isBrowser,
|
isBrowser,
|
||||||
myAuth,
|
myAuth,
|
||||||
|
QueryParams,
|
||||||
setIsoData,
|
setIsoData,
|
||||||
toast,
|
toast,
|
||||||
wsClient,
|
wsClient,
|
||||||
|
@ -31,13 +32,26 @@ import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
import { PostForm } from "./post-form";
|
import { PostForm } from "./post-form";
|
||||||
|
|
||||||
interface CreatePostState {
|
export interface CreatePostProps {
|
||||||
listCommunitiesResponse?: ListCommunitiesResponse;
|
communityId?: number;
|
||||||
siteRes: GetSiteResponse;
|
|
||||||
loading: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 isoData = setIsoData(this.context);
|
||||||
private subscription?: Subscription;
|
private subscription?: Subscription;
|
||||||
state: CreatePostState = {
|
state: CreatePostState = {
|
||||||
|
@ -45,10 +59,12 @@ export class CreatePost extends Component<any, CreatePostState> {
|
||||||
loading: true,
|
loading: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: RouteComponentProps<Record<string, never>>, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.handlePostCreate = this.handlePostCreate.bind(this);
|
this.handlePostCreate = this.handlePostCreate.bind(this);
|
||||||
|
this.handleSelectedCommunityChange =
|
||||||
|
this.handleSelectedCommunityChange.bind(this);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
this.parseMessage = this.parseMessage.bind(this);
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
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
|
// 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 = {
|
||||||
...this.state,
|
...this.state,
|
||||||
listCommunitiesResponse: this.isoData
|
|
||||||
.routeData[0] as ListCommunitiesResponse,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.refetch();
|
this.fetchCommunity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refetch() {
|
fetchCommunity() {
|
||||||
let nameOrId = this.params.nameOrId;
|
const { communityId } = getCreatePostQueryParams();
|
||||||
let auth = myAuth(false);
|
const auth = myAuth(false);
|
||||||
if (nameOrId) {
|
|
||||||
if (typeof nameOrId === "string") {
|
if (communityId) {
|
||||||
let form: GetCommunity = {
|
const form: GetCommunity = {
|
||||||
name: nameOrId,
|
id: communityId,
|
||||||
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,
|
|
||||||
auth,
|
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() {
|
render() {
|
||||||
let res = this.state.listCommunitiesResponse;
|
const { selectedCommunityChoice } = this.state;
|
||||||
|
|
||||||
|
const locationState = this.props.history.location.state as
|
||||||
|
| PostFormParams
|
||||||
|
| undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-lg">
|
<div className="container-lg">
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
|
@ -126,96 +158,93 @@ export class CreatePost extends Component<any, CreatePostState> {
|
||||||
<Spinner large />
|
<Spinner large />
|
||||||
</h5>
|
</h5>
|
||||||
) : (
|
) : (
|
||||||
res && (
|
<div className="row">
|
||||||
<div className="row">
|
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
|
<h5>{i18n.t("create_post")}</h5>
|
||||||
<h5>{i18n.t("create_post")}</h5>
|
<PostForm
|
||||||
<PostForm
|
onCreate={this.handlePostCreate}
|
||||||
communities={res.communities}
|
params={locationState}
|
||||||
onCreate={this.handlePostCreate}
|
enableDownvotes={enableDownvotes(this.state.siteRes)}
|
||||||
params={this.params}
|
enableNsfw={enableNsfw(this.state.siteRes)}
|
||||||
enableDownvotes={enableDownvotes(this.state.siteRes)}
|
allLanguages={this.state.siteRes.all_languages}
|
||||||
enableNsfw={enableNsfw(this.state.siteRes)}
|
siteLanguages={this.state.siteRes.discussion_languages}
|
||||||
allLanguages={this.state.siteRes.all_languages}
|
selectedCommunityChoice={selectedCommunityChoice}
|
||||||
siteLanguages={this.state.siteRes.discussion_languages}
|
onSelectCommunity={this.handleSelectedCommunityChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get params(): PostFormParams {
|
updateUrl({ communityId }: Partial<CreatePostProps>) {
|
||||||
let urlParams = new URLSearchParams(this.props.location.search);
|
const { communityId: urlCommunityId } = getCreatePostQueryParams();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let params: PostFormParams = {
|
const queryParams: QueryParams<CreatePostProps> = {
|
||||||
name: urlParams.get("title") ?? undefined,
|
communityId: (communityId ?? urlCommunityId)?.toString(),
|
||||||
nameOrId,
|
|
||||||
body: urlParams.get("body") ?? undefined,
|
|
||||||
url: urlParams.get("url") ?? undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
handleSelectedCommunityChange(choice: Choice) {
|
||||||
if (this.props.match.params.name) {
|
this.updateUrl({
|
||||||
return this.props.match.params.name;
|
communityId: getIdFromString(choice?.value),
|
||||||
} 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostCreate(post_view: PostView) {
|
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>[] {
|
static fetchInitialData({
|
||||||
let listCommunitiesForm: ListCommunities = {
|
client,
|
||||||
type_: ListingType.All,
|
query: { communityId },
|
||||||
sort: SortType.TopAll,
|
auth,
|
||||||
limit: fetchLimit,
|
}: InitialFetchRequest<QueryParams<CreatePostProps>>): Promise<any>[] {
|
||||||
auth: req.auth,
|
const promises: Promise<any>[] = [];
|
||||||
};
|
|
||||||
return [req.client.listCommunities(listCommunitiesForm)];
|
if (communityId) {
|
||||||
|
const form: GetCommunity = {
|
||||||
|
auth,
|
||||||
|
id: getIdFromString(communityId),
|
||||||
|
};
|
||||||
|
|
||||||
|
promises.push(client.getCommunity(form));
|
||||||
|
} else {
|
||||||
|
promises.push(Promise.resolve());
|
||||||
|
}
|
||||||
|
|
||||||
|
return promises;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
let op = wsUserOp(msg);
|
const op = wsUserOp(msg);
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
toast(i18n.t(msg.error), "danger");
|
toast(i18n.t(msg.error), "danger");
|
||||||
return;
|
return;
|
||||||
} else if (op == UserOperation.ListCommunities) {
|
}
|
||||||
let data = wsJsonToRes<ListCommunitiesResponse>(msg);
|
|
||||||
this.setState({ listCommunitiesResponse: data, loading: false });
|
if (op === UserOperation.GetCommunity) {
|
||||||
} else if (op == UserOperation.GetCommunity) {
|
const {
|
||||||
let data = wsJsonToRes<GetCommunityResponse>(msg);
|
community_view: {
|
||||||
this.setState({
|
community: { name, id },
|
||||||
listCommunitiesResponse: {
|
|
||||||
communities: [data.community_view],
|
|
||||||
},
|
},
|
||||||
|
} = wsJsonToRes<GetCommunityResponse>(msg);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedCommunityChoice: { label: name, value: id.toString() },
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import autosize from "autosize";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import { Prompt } from "inferno-router";
|
import { Prompt } from "inferno-router";
|
||||||
import {
|
import {
|
||||||
CommunityView,
|
|
||||||
CreatePost,
|
CreatePost,
|
||||||
EditPost,
|
EditPost,
|
||||||
Language,
|
Language,
|
||||||
|
@ -24,14 +23,13 @@ import { UserService, WebSocketService } from "../../services";
|
||||||
import {
|
import {
|
||||||
archiveTodayUrl,
|
archiveTodayUrl,
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
choicesConfig,
|
Choice,
|
||||||
communitySelectName,
|
|
||||||
communityToChoice,
|
communityToChoice,
|
||||||
debounce,
|
debounce,
|
||||||
fetchCommunities,
|
fetchCommunities,
|
||||||
|
getIdFromString,
|
||||||
getSiteMetadata,
|
getSiteMetadata,
|
||||||
ghostArchiveUrl,
|
ghostArchiveUrl,
|
||||||
isBrowser,
|
|
||||||
isImage,
|
isImage,
|
||||||
myAuth,
|
myAuth,
|
||||||
myFirstDiscussionLanguageId,
|
myFirstDiscussionLanguageId,
|
||||||
|
@ -50,26 +48,23 @@ import {
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { LanguageSelect } from "../common/language-select";
|
import { LanguageSelect } from "../common/language-select";
|
||||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||||
|
import { SearchableSelect } from "../common/searchable-select";
|
||||||
import { PostListings } from "./post-listings";
|
import { PostListings } from "./post-listings";
|
||||||
|
|
||||||
var Choices: any;
|
|
||||||
if (isBrowser()) {
|
|
||||||
Choices = require("choices.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_POST_TITLE_LENGTH = 200;
|
const MAX_POST_TITLE_LENGTH = 200;
|
||||||
|
|
||||||
interface PostFormProps {
|
interface PostFormProps {
|
||||||
post_view?: PostView; // If a post is given, that means this is an edit
|
post_view?: PostView; // If a post is given, that means this is an edit
|
||||||
allLanguages: Language[];
|
allLanguages: Language[];
|
||||||
siteLanguages: number[];
|
siteLanguages: number[];
|
||||||
communities?: CommunityView[];
|
|
||||||
params?: PostFormParams;
|
params?: PostFormParams;
|
||||||
onCancel?(): any;
|
onCancel?(): any;
|
||||||
onCreate?(post: PostView): any;
|
onCreate?(post: PostView): any;
|
||||||
onEdit?(post: PostView): any;
|
onEdit?(post: PostView): any;
|
||||||
enableNsfw?: boolean;
|
enableNsfw?: boolean;
|
||||||
enableDownvotes?: boolean;
|
enableDownvotes?: boolean;
|
||||||
|
selectedCommunityChoice?: Choice;
|
||||||
|
onSelectCommunity?: (choice: Choice) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PostFormState {
|
interface PostFormState {
|
||||||
|
@ -88,32 +83,34 @@ interface PostFormState {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
imageLoading: boolean;
|
imageLoading: boolean;
|
||||||
communitySearchLoading: boolean;
|
communitySearchLoading: boolean;
|
||||||
|
communitySearchOptions: Choice[];
|
||||||
previewMode: boolean;
|
previewMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PostForm extends Component<PostFormProps, PostFormState> {
|
export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
private subscription?: Subscription;
|
private subscription?: Subscription;
|
||||||
private choices: any;
|
|
||||||
state: PostFormState = {
|
state: PostFormState = {
|
||||||
form: {},
|
form: {},
|
||||||
loading: false,
|
loading: false,
|
||||||
imageLoading: false,
|
imageLoading: false,
|
||||||
communitySearchLoading: false,
|
communitySearchLoading: false,
|
||||||
previewMode: false,
|
previewMode: false,
|
||||||
|
communitySearchOptions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: PostFormProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this));
|
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this));
|
||||||
this.fetchPageTitle = debounce(this.fetchPageTitle.bind(this));
|
this.fetchPageTitle = debounce(this.fetchPageTitle.bind(this));
|
||||||
this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
|
this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
|
||||||
this.handleLanguageChange = this.handleLanguageChange.bind(this);
|
this.handleLanguageChange = this.handleLanguageChange.bind(this);
|
||||||
|
this.handleCommunitySelect = this.handleCommunitySelect.bind(this);
|
||||||
|
|
||||||
this.parseMessage = this.parseMessage.bind(this);
|
this.parseMessage = this.parseMessage.bind(this);
|
||||||
this.subscription = wsSubscribe(this.parseMessage);
|
this.subscription = wsSubscribe(this.parseMessage);
|
||||||
|
|
||||||
// Means its an edit
|
// Means its an edit
|
||||||
let pv = this.props.post_view;
|
const pv = this.props.post_view;
|
||||||
if (pv) {
|
if (pv) {
|
||||||
this.state = {
|
this.state = {
|
||||||
...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) {
|
if (params) {
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
form: {
|
form: {
|
||||||
...this.state.form,
|
...this.state.form,
|
||||||
name: params.name,
|
...params,
|
||||||
url: params.url,
|
|
||||||
body: params.body,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -144,8 +152,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
setupTippy();
|
setupTippy();
|
||||||
this.setupCommunities();
|
const textarea: any = document.getElementById("post-title");
|
||||||
let textarea: any = document.getElementById("post-title");
|
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
autosize(textarea);
|
autosize(textarea);
|
||||||
}
|
}
|
||||||
|
@ -168,6 +176,19 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
window.onbeforeunload = null;
|
window.onbeforeunload = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(
|
||||||
|
{ selectedCommunityChoice }: PostFormProps,
|
||||||
|
{ form, ...restState }: PostFormState
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...restState,
|
||||||
|
form: {
|
||||||
|
...form,
|
||||||
|
community_id: getIdFromString(selectedCommunityChoice?.value),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let firstLang =
|
let firstLang =
|
||||||
this.state.form.language_id ??
|
this.state.form.language_id ??
|
||||||
|
@ -342,26 +363,23 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
className="col-sm-2 col-form-label"
|
className="col-sm-2 col-form-label"
|
||||||
htmlFor="post-community"
|
htmlFor="post-community"
|
||||||
>
|
>
|
||||||
{this.state.communitySearchLoading ? (
|
{i18n.t("community")}
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
i18n.t("community")
|
|
||||||
)}
|
|
||||||
</label>
|
</label>
|
||||||
<div className="col-sm-10">
|
<div className="col-sm-10">
|
||||||
<select
|
<SearchableSelect
|
||||||
className="form-control"
|
|
||||||
id="post-community"
|
id="post-community"
|
||||||
value={this.state.form.community_id}
|
value={this.state.form.community_id}
|
||||||
onInput={linkEvent(this, this.handlePostCommunityChange)}
|
options={[
|
||||||
>
|
{
|
||||||
<option>{i18n.t("select_a_community")}</option>
|
label: i18n.t("select_a_community"),
|
||||||
{this.props.communities?.map(cv => (
|
value: "",
|
||||||
<option key={cv.community.id} value={cv.community.id}>
|
disabled: true,
|
||||||
{communitySelectName(cv)}
|
} as Choice,
|
||||||
</option>
|
].concat(this.state.communitySearchOptions)}
|
||||||
))}
|
loading={this.state.communitySearchLoading}
|
||||||
</select>
|
onChange={this.handleCommunitySelect}
|
||||||
|
onSearch={this.handleCommunitySearch}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -609,67 +627,41 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupCommunities() {
|
handleCommunitySearch = debounce(async (text: string) => {
|
||||||
// Set up select searching
|
const { selectedCommunityChoice } = this.props;
|
||||||
if (isBrowser()) {
|
this.setState({ communitySearchLoading: true });
|
||||||
let selectId: any = document.getElementById("post-community");
|
|
||||||
if (selectId) {
|
const newOptions: Choice[] = [];
|
||||||
this.choices = new Choices(selectId, choicesConfig);
|
|
||||||
this.choices.passedElement.element.addEventListener(
|
if (selectedCommunityChoice) {
|
||||||
"choice",
|
newOptions.push(selectedCommunityChoice);
|
||||||
(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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let pv = this.props.post_view;
|
if (text.length > 0) {
|
||||||
this.setState(s => ((s.form.community_id = pv?.community.id), s));
|
newOptions.push(
|
||||||
|
...(await fetchCommunities(text)).communities.map(communityToChoice)
|
||||||
|
);
|
||||||
|
|
||||||
let nameOrId = this.props.params?.nameOrId;
|
this.setState({
|
||||||
if (nameOrId) {
|
communitySearchOptions: newOptions,
|
||||||
if (typeof nameOrId === "string") {
|
});
|
||||||
let name_ = nameOrId;
|
|
||||||
let foundCommunityId = this.props.communities?.find(
|
|
||||||
r => r.community.name == name_
|
|
||||||
)?.community.id;
|
|
||||||
this.setState(s => ((s.form.community_id = foundCommunityId), s));
|
|
||||||
} else {
|
|
||||||
let id = nameOrId;
|
|
||||||
this.setState(s => ((s.form.community_id = id), s));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBrowser() && this.state.form.community_id) {
|
this.setState({
|
||||||
this.choices.setChoiceByValue(this.state.form.community_id.toString());
|
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) {
|
parseMessage(msg: any) {
|
||||||
|
|
|
@ -25,7 +25,7 @@ import {
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { externalHost } from "../../env";
|
import { externalHost } from "../../env";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { BanType, PurgeType } from "../../interfaces";
|
import { BanType, PostFormParams, PurgeType } from "../../interfaces";
|
||||||
import { UserService, WebSocketService } from "../../services";
|
import { UserService, WebSocketService } from "../../services";
|
||||||
import {
|
import {
|
||||||
amAdmin,
|
amAdmin,
|
||||||
|
@ -147,7 +147,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let post = this.props.post_view.post;
|
const post = this.props.post_view.post;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="post-listing">
|
<div className="post-listing">
|
||||||
{!this.state.showEdit ? (
|
{!this.state.showEdit ? (
|
||||||
|
@ -734,7 +735,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
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")}
|
title={i18n.t("cross_post")}
|
||||||
>
|
>
|
||||||
<Icon icon="copy" inline />
|
<Icon icon="copy" inline />
|
||||||
|
@ -1461,18 +1469,22 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get crossPostParams(): string {
|
get crossPostParams(): PostFormParams {
|
||||||
let post = this.props.post_view.post;
|
const queryParams: PostFormParams = {};
|
||||||
let params = `?title=${encodeURIComponent(post.name)}`;
|
const { name, url } = this.props.post_view.post;
|
||||||
|
|
||||||
if (post.url) {
|
queryParams.name = name;
|
||||||
params += `&url=${encodeURIComponent(post.url)}`;
|
|
||||||
|
if (url) {
|
||||||
|
queryParams.url = url;
|
||||||
}
|
}
|
||||||
let crossPostBody = this.crossPostBody();
|
|
||||||
|
const crossPostBody = this.crossPostBody();
|
||||||
if (crossPostBody) {
|
if (crossPostBody) {
|
||||||
params += `&body=${encodeURIComponent(crossPostBody)}`;
|
queryParams.body = crossPostBody;
|
||||||
}
|
}
|
||||||
return params;
|
|
||||||
|
return queryParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
crossPostBody(): string | undefined {
|
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 { GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
||||||
|
import type { ParsedQs } from "qs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This contains serialized data, it needs to be deserialized before use.
|
* 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;
|
auth?: string;
|
||||||
client: LemmyHttp;
|
client: LemmyHttp;
|
||||||
path: string;
|
path: string;
|
||||||
|
query: T;
|
||||||
|
site: GetSiteResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PostFormParams {
|
export interface PostFormParams {
|
||||||
name?: string;
|
name?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
nameOrId?: string | number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CommentViewType {
|
export enum CommentViewType {
|
||||||
|
@ -49,10 +51,10 @@ export enum BanType {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PersonDetailsView {
|
export enum PersonDetailsView {
|
||||||
Overview,
|
Overview = "Overview",
|
||||||
Comments,
|
Comments = "Comments",
|
||||||
Posts,
|
Posts = "Posts",
|
||||||
Saved,
|
Saved = "Saved",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PurgeType {
|
export enum PurgeType {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Inferno } from "inferno";
|
|
||||||
import { IRouteProps } from "inferno-router/dist/Route";
|
import { IRouteProps } from "inferno-router/dist/Route";
|
||||||
import { Communities } from "./components/community/communities";
|
import { Communities } from "./components/community/communities";
|
||||||
import { Community } from "./components/community/community";
|
import { Community } from "./components/community/community";
|
||||||
|
@ -26,21 +25,15 @@ import { InitialFetchRequest } from "./interfaces";
|
||||||
|
|
||||||
interface IRoutePropsWithFetch extends IRouteProps {
|
interface IRoutePropsWithFetch extends IRouteProps {
|
||||||
// TODO Make sure this one is good.
|
// TODO Make sure this one is good.
|
||||||
component: Inferno.ComponentClass;
|
|
||||||
fetchInitialData?(req: InitialFetchRequest): Promise<any>[];
|
fetchInitialData?(req: InitialFetchRequest): Promise<any>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routes: IRoutePropsWithFetch[] = [
|
export const routes: IRoutePropsWithFetch[] = [
|
||||||
{
|
{
|
||||||
path: `/`,
|
path: `/`,
|
||||||
|
component: Home,
|
||||||
|
fetchInitialData: Home.fetchInitialData,
|
||||||
exact: true,
|
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`,
|
path: `/login`,
|
||||||
|
@ -53,101 +46,81 @@ export const routes: IRoutePropsWithFetch[] = [
|
||||||
{
|
{
|
||||||
path: `/create_post`,
|
path: `/create_post`,
|
||||||
component: CreatePost,
|
component: CreatePost,
|
||||||
fetchInitialData: req => CreatePost.fetchInitialData(req),
|
fetchInitialData: CreatePost.fetchInitialData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/create_community`,
|
path: `/create_community`,
|
||||||
component: CreateCommunity,
|
component: CreateCommunity,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/create_private_message/recipient/:recipient_id`,
|
path: `/create_private_message/:recipient_id`,
|
||||||
component: CreatePrivateMessage,
|
component: CreatePrivateMessage,
|
||||||
fetchInitialData: req => CreatePrivateMessage.fetchInitialData(req),
|
fetchInitialData: CreatePrivateMessage.fetchInitialData,
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `/communities/listing_type/:listing_type/page/:page`,
|
|
||||||
component: Communities,
|
|
||||||
fetchInitialData: req => Communities.fetchInitialData(req),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/communities`,
|
path: `/communities`,
|
||||||
component: Communities,
|
component: Communities,
|
||||||
fetchInitialData: req => Communities.fetchInitialData(req),
|
fetchInitialData: Communities.fetchInitialData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/post/:post_id`,
|
path: `/post/:post_id`,
|
||||||
component: Post,
|
component: Post,
|
||||||
fetchInitialData: req => Post.fetchInitialData(req),
|
fetchInitialData: Post.fetchInitialData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/comment/:comment_id`,
|
path: `/comment/:comment_id`,
|
||||||
component: Post,
|
component: Post,
|
||||||
fetchInitialData: req => Post.fetchInitialData(req),
|
fetchInitialData: Post.fetchInitialData,
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `/c/:name/data_type/:data_type/sort/:sort/page/:page`,
|
|
||||||
component: Community,
|
|
||||||
fetchInitialData: req => Community.fetchInitialData(req),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/c/:name`,
|
path: `/c/:name`,
|
||||||
component: Community,
|
component: Community,
|
||||||
fetchInitialData: req => Community.fetchInitialData(req),
|
fetchInitialData: Community.fetchInitialData,
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `/u/:username/view/:view/sort/:sort/page/:page`,
|
|
||||||
component: Profile,
|
|
||||||
fetchInitialData: req => Profile.fetchInitialData(req),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/u/:username`,
|
path: `/u/:username`,
|
||||||
component: Profile,
|
component: Profile,
|
||||||
fetchInitialData: req => Profile.fetchInitialData(req),
|
fetchInitialData: Profile.fetchInitialData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/inbox`,
|
path: `/inbox`,
|
||||||
component: Inbox,
|
component: Inbox,
|
||||||
fetchInitialData: req => Inbox.fetchInitialData(req),
|
fetchInitialData: Inbox.fetchInitialData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/settings`,
|
path: `/settings`,
|
||||||
component: Settings,
|
component: Settings,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: `/modlog/community/:community_id`,
|
|
||||||
component: Modlog,
|
|
||||||
fetchInitialData: req => Modlog.fetchInitialData(req),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: `/modlog`,
|
path: `/modlog`,
|
||||||
component: Modlog,
|
component: Modlog,
|
||||||
fetchInitialData: req => Modlog.fetchInitialData(req),
|
fetchInitialData: Modlog.fetchInitialData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `/modlog/:communityId`,
|
||||||
|
component: Modlog,
|
||||||
|
fetchInitialData: Modlog.fetchInitialData,
|
||||||
},
|
},
|
||||||
{ path: `/setup`, component: Setup },
|
{ path: `/setup`, component: Setup },
|
||||||
{
|
{
|
||||||
path: `/admin`,
|
path: `/admin`,
|
||||||
component: AdminSettings,
|
component: AdminSettings,
|
||||||
fetchInitialData: req => AdminSettings.fetchInitialData(req),
|
fetchInitialData: AdminSettings.fetchInitialData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/reports`,
|
path: `/reports`,
|
||||||
component: Reports,
|
component: Reports,
|
||||||
fetchInitialData: req => Reports.fetchInitialData(req),
|
fetchInitialData: Reports.fetchInitialData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/registration_applications`,
|
path: `/registration_applications`,
|
||||||
component: RegistrationApplications,
|
component: RegistrationApplications,
|
||||||
fetchInitialData: req => RegistrationApplications.fetchInitialData(req),
|
fetchInitialData: RegistrationApplications.fetchInitialData,
|
||||||
},
|
|
||||||
{
|
|
||||||
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),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/search`,
|
path: `/search`,
|
||||||
component: Search,
|
component: Search,
|
||||||
fetchInitialData: req => Search.fetchInitialData(req),
|
fetchInitialData: Search.fetchInitialData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `/password_change/:token`,
|
path: `/password_change/:token`,
|
||||||
|
|
|
@ -82,6 +82,8 @@ export const concurrentImageUpload = 4;
|
||||||
|
|
||||||
export const relTags = "noopener nofollow";
|
export const relTags = "noopener nofollow";
|
||||||
|
|
||||||
|
export const emDash = "\u2014";
|
||||||
|
|
||||||
export type ThemeColor =
|
export type ThemeColor =
|
||||||
| "primary"
|
| "primary"
|
||||||
| "secondary"
|
| "secondary"
|
||||||
|
@ -118,6 +120,14 @@ function getRandomCharFromAlphabet(alphabet: string): string {
|
||||||
return alphabet.charAt(Math.floor(Math.random() * alphabet.length));
|
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(
|
export function randomStr(
|
||||||
idDesiredLength = 20,
|
idDesiredLength = 20,
|
||||||
alphabet = DEFAULT_ALPHABET
|
alphabet = DEFAULT_ALPHABET
|
||||||
|
@ -332,8 +342,11 @@ export function capitalizeFirstLetter(str: string): string {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function routeSortTypeToEnum(sort: string): SortType {
|
export function routeSortTypeToEnum(
|
||||||
return SortType[sort];
|
sort: string,
|
||||||
|
defaultValue: SortType
|
||||||
|
): SortType {
|
||||||
|
return SortType[sort] ?? defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listingTypeFromNum(type_: number): ListingType {
|
export function listingTypeFromNum(type_: number): ListingType {
|
||||||
|
@ -344,16 +357,25 @@ export function sortTypeFromNum(type_: number): SortType {
|
||||||
return Object.values(SortType)[type_];
|
return Object.values(SortType)[type_];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function routeListingTypeToEnum(type: string): ListingType {
|
export function routeListingTypeToEnum(
|
||||||
return ListingType[type];
|
type: string,
|
||||||
|
defaultValue: ListingType
|
||||||
|
): ListingType {
|
||||||
|
return ListingType[type] ?? defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function routeDataTypeToEnum(type: string): DataType {
|
export function routeDataTypeToEnum(
|
||||||
return DataType[capitalizeFirstLetter(type)];
|
type: string,
|
||||||
|
defaultValue: DataType
|
||||||
|
): DataType {
|
||||||
|
return DataType[type] ?? defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function routeSearchTypeToEnum(type: string): SearchType {
|
export function routeSearchTypeToEnum(
|
||||||
return SearchType[type];
|
type: string,
|
||||||
|
defaultValue: SearchType
|
||||||
|
): SearchType {
|
||||||
|
return SearchType[type] ?? defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSiteMetadata(url: string) {
|
export async function getSiteMetadata(url: string) {
|
||||||
|
@ -362,26 +384,34 @@ export async function getSiteMetadata(url: string) {
|
||||||
return client.getSiteMetadata(form);
|
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
|
// 'private' variable for instance
|
||||||
// The returned function will be able to reference this due to closure.
|
// The returned function will be able to reference this due to closure.
|
||||||
// Each call to the returned function will share this common timer.
|
// 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
|
// Calling debounce returns a new anonymous function
|
||||||
return function () {
|
return function () {
|
||||||
// reference the context and args for the setTimeout 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
|
// Should the function be called now? If immediate is true
|
||||||
// and not already in a timeout then the answer is: Yes
|
// 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
|
// function several times, but it will only execute once
|
||||||
// [before or after imposing a delay].
|
// [before or after imposing a delay].
|
||||||
// Each time the returned function is called, the timer starts over.
|
// Each time the returned function is called, the timer starts over.
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout ?? undefined);
|
||||||
|
|
||||||
// Set the new timeout
|
// Set the new timeout
|
||||||
timeout = setTimeout(function () {
|
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..
|
// Immediate mode and no wait timer? Execute the function..
|
||||||
if (callNow) func.apply(this, args);
|
if (callNow) func.apply(this, args);
|
||||||
};
|
} as (...e: T) => R;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLanguages(
|
export function getLanguages(
|
||||||
|
@ -903,47 +933,6 @@ async function communitySearch(text: string): Promise<CommunityTribute[]> {
|
||||||
return communities;
|
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 {
|
export function getRecipientIdFromProps(props: any): number {
|
||||||
return props.match.params.recipient_id
|
return props.match.params.recipient_id
|
||||||
? Number(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;
|
return id ? Number(id) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUsernameFromProps(props: any): string {
|
|
||||||
return props.match.params.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function editCommentRes(data: CommentView, comments?: CommentView[]) {
|
export function editCommentRes(data: CommentView, comments?: CommentView[]) {
|
||||||
let found = comments?.find(c => c.comment.id == data.comment.id);
|
let found = comments?.find(c => c.comment.id == data.comment.id);
|
||||||
if (found) {
|
if (found) {
|
||||||
|
@ -1378,25 +1363,30 @@ export function showLocal(isoData: IsoData): boolean {
|
||||||
return linked ? linked.length > 0 : false;
|
return linked ? linked.length > 0 : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChoicesValue {
|
export interface Choice {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function communityToChoice(cv: CommunityView): ChoicesValue {
|
export function getUpdatedSearchId(id?: number | null, urlId?: number | null) {
|
||||||
let choice: ChoicesValue = {
|
return id === null
|
||||||
|
? undefined
|
||||||
|
: ((id ?? urlId) === 0 ? undefined : id ?? urlId)?.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function communityToChoice(cv: CommunityView): Choice {
|
||||||
|
return {
|
||||||
value: cv.community.id.toString(),
|
value: cv.community.id.toString(),
|
||||||
label: communitySelectName(cv),
|
label: communitySelectName(cv),
|
||||||
};
|
};
|
||||||
return choice;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function personToChoice(pvs: PersonViewSafe): ChoicesValue {
|
export function personToChoice(pvs: PersonViewSafe): Choice {
|
||||||
let choice: ChoicesValue = {
|
return {
|
||||||
value: pvs.person.id.toString(),
|
value: pvs.person.id.toString(),
|
||||||
label: personSelectName(pvs),
|
label: personSelectName(pvs),
|
||||||
};
|
};
|
||||||
return choice;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchCommunities(q: string) {
|
export async function fetchCommunities(q: string) {
|
||||||
|
@ -1427,49 +1417,17 @@ export async function fetchUsers(q: string) {
|
||||||
return client.search(form);
|
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 {
|
export function communitySelectName(cv: CommunityView): string {
|
||||||
return cv.community.local
|
return cv.community.local
|
||||||
? cv.community.title
|
? cv.community.title
|
||||||
: `${hostname(cv.community.actor_id)}/${cv.community.title}`;
|
: `${hostname(cv.community.actor_id)}/${cv.community.title}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function personSelectName(pvs: PersonViewSafe): string {
|
export function personSelectName({
|
||||||
let pName = pvs.person.display_name ?? pvs.person.name;
|
person: { display_name, name, local, actor_id },
|
||||||
return pvs.person.local ? pName : `${hostname(pvs.person.actor_id)}/${pName}`;
|
}: PersonViewSafe): string {
|
||||||
|
const pName = display_name ?? name;
|
||||||
|
return local ? pName : `${hostname(actor_id)}/${pName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initializeSite(site: GetSiteResponse) {
|
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 {
|
export function myAuth(throwErr = true): string | undefined {
|
||||||
return UserService.Instance.auth(throwErr);
|
return UserService.Instance.auth(throwErr);
|
||||||
}
|
}
|
||||||
|
@ -1524,14 +1476,17 @@ export function enableNsfw(siteRes: GetSiteResponse): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postToCommentSortType(sort: SortType): CommentSortType {
|
export function postToCommentSortType(sort: SortType): CommentSortType {
|
||||||
if ([SortType.Active, SortType.Hot].includes(sort)) {
|
switch (sort) {
|
||||||
return CommentSortType.Hot;
|
case SortType.Active:
|
||||||
} else if ([SortType.New, SortType.NewComments].includes(sort)) {
|
case SortType.Hot:
|
||||||
return CommentSortType.New;
|
return CommentSortType.Hot;
|
||||||
} else if (sort == SortType.Old) {
|
case SortType.New:
|
||||||
return CommentSortType.Old;
|
case SortType.NewComments:
|
||||||
} else {
|
return CommentSortType.New;
|
||||||
return CommentSortType.Top;
|
case SortType.Old:
|
||||||
|
return CommentSortType.Old;
|
||||||
|
default:
|
||||||
|
return CommentSortType.Top;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1553,7 +1508,8 @@ export function canCreateCommunity(
|
||||||
siteRes: GetSiteResponse,
|
siteRes: GetSiteResponse,
|
||||||
myUserInfo = UserService.Instance.myUserInfo
|
myUserInfo = UserService.Instance.myUserInfo
|
||||||
): boolean {
|
): 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);
|
return !adminOnly || amAdmin(myUserInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1651,3 +1607,36 @@ const groupBy = <T>(
|
||||||
(acc[predicate(value, index, array)] ||= []).push(value);
|
(acc[predicate(value, index, array)] ||= []).push(value);
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as { [key: string]: T[] });
|
}, {} 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:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.11"
|
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"
|
version "7.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
|
||||||
integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
|
integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
|
||||||
|
@ -1534,14 +1534,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
|
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
|
||||||
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
|
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@*":
|
"@types/node@*":
|
||||||
version "18.7.23"
|
version "18.7.23"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.23.tgz#75c580983846181ebe5f4abc40fe9dfb2d65665f"
|
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"
|
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==
|
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:
|
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3:
|
||||||
version "3.5.3"
|
version "3.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
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"
|
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||||
integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==
|
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:
|
form-data@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
resolved "https://registry.yarnpkg.com/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"
|
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
|
||||||
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
|
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:
|
gauge@~2.7.3:
|
||||||
version "2.7.4"
|
version "2.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
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"
|
json-parse-better-errors "^1.0.0"
|
||||||
safe-buffer "^5.1.1"
|
safe-buffer "^5.1.1"
|
||||||
|
|
||||||
node-fetch@2.6.7, node-fetch@^2.6.1:
|
node-fetch@2.6.7:
|
||||||
version "2.6.7"
|
version "2.6.7"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||||
|
@ -7386,13 +7355,6 @@ rechoir@^0.8.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
resolve "^1.20.0"
|
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:
|
regenerate-unicode-properties@^10.1.0:
|
||||||
version "10.1.0"
|
version "10.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"
|
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"
|
||||||
|
|
Loading…
Reference in a new issue