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:
SleeplessOne1917 2023-04-15 14:47:10 +00:00 committed by GitHub
parent 699c3ff4b1
commit 3526baf465
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 3591 additions and 3057 deletions

@ -1 +1 @@
Subproject commit d2b85d582071d84b559f7b9db1ab623f6596c586 Subproject commit 5c50ce3ebaf058ad5d4e9bcd445653960cbc98b1

View file

@ -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",

View file

@ -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);
} }

View file

@ -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();

View file

@ -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} />

View file

@ -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();

View file

@ -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";

View file

@ -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" />

View file

@ -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);
} }

View 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: "" });
}
}
}

View file

@ -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);
} }
} }

View file

@ -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)} />
)}
</>
);
}
get listings() {
const { dataType } = getCommunityQueryParams();
const { site_res } = this.isoData;
const { listingsLoading, posts, comments, communityRes } = this.state;
if (listingsLoading) {
return (
<h5> <h5>
<Spinner large /> <Spinner large />
</h5> </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 <CommentNodes
nodes={commentsToFlatNodes(this.state.comments)} nodes={commentsToFlatNodes(comments)}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
noIndent noIndent
showContext showContext
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(site_res)}
moderators={this.state.communityRes?.moderators} moderators={communityRes?.moderators}
admins={this.state.siteRes.admins} admins={site_res.admins}
allLanguages={this.state.siteRes.all_languages} allLanguages={site_res.all_languages}
siteLanguages={this.state.siteRes.discussion_languages} siteLanguages={site_res.discussion_languages}
/> />
); );
} }
}
get communityInfo() {
const community = this.state.communityRes?.community_view.community;
communityInfo() {
let 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,9 +529,13 @@ export class Community extends Component<any, State> {
}) })
); );
} }
this.fetchData(); this.fetchData();
} else if (op == UserOperation.GetCommunity) { } else {
let data = wsJsonToRes<GetCommunityResponse>(msg); switch (op) {
case UserOperation.GetCommunity: {
const data = wsJsonToRes<GetCommunityResponse>(msg);
this.setState({ communityRes: data, communityLoading: false }); this.setState({ communityRes: data, communityLoading: false });
// TODO why is there no auth in this form? // TODO why is there no auth in this form?
WebSocketService.Instance.send( WebSocketService.Instance.send(
@ -533,133 +543,211 @@ export class Community extends Component<any, State> {
community_id: data.community_view.community.id, community_id: data.community_view.community.id,
}) })
); );
} else if (
op == UserOperation.EditCommunity || break;
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) { case UserOperation.EditCommunity:
let data = wsJsonToRes<CommunityResponse>(msg); case UserOperation.DeleteCommunity:
case UserOperation.RemoveCommunity: {
const { community_view, discussion_languages } =
wsJsonToRes<CommunityResponse>(msg);
if (res) { if (res) {
res.community_view.subscribed = data.community_view.subscribed; res.community_view = community_view;
res.community_view.counts.subscribers = res.discussion_languages = discussion_languages;
data.community_view.counts.subscribers;
}
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetPosts) { }
let data = wsJsonToRes<GetPostsResponse>(msg);
this.setState({ posts: data.posts, postsLoading: false }); break;
}
case UserOperation.FollowCommunity: {
const {
community_view: {
subscribed,
counts: { subscribers },
},
} = wsJsonToRes<CommunityResponse>(msg);
if (res) {
res.community_view.subscribed = subscribed;
res.community_view.counts.subscribers = subscribers;
this.setState(this.state);
}
break;
}
case UserOperation.GetPosts: {
const { posts } = wsJsonToRes<GetPostsResponse>(msg);
this.setState({ posts, listingsLoading: false });
restoreScrollPosition(this.context); restoreScrollPosition(this.context);
setupTippy(); 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 = 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 UserService.Instance.myUserInfo?.local_user_view.local_user
.show_new_post_notifs; .show_new_post_notifs;
// Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked // 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)) {
if ( this.state.posts.unshift(post_view);
this.state.page == 1 &&
nsfwCheck(data.post_view) &&
!isPostBlocked(data.post_view)
) {
this.state.posts.unshift(data.post_view);
if (showPostNotifs) { if (showPostNotifs) {
notifyPost(data.post_view, this.context.router); notifyPost(post_view, this.context.router);
} }
this.setState(this.state); this.setState(this.state);
} }
} else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg); break;
createPostLikeFindRes(data.post_view, this.state.posts); }
case UserOperation.CreatePostLike: {
const { post_view } = wsJsonToRes<PostResponse>(msg);
createPostLikeFindRes(post_view, this.state.posts);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.AddModToCommunity) {
let data = wsJsonToRes<AddModToCommunityResponse>(msg); break;
}
case UserOperation.AddModToCommunity: {
const { moderators } = wsJsonToRes<AddModToCommunityResponse>(msg);
if (res) { if (res) {
res.moderators = data.moderators; res.moderators = moderators;
}
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BanFromCommunity) { }
let data = wsJsonToRes<BanFromCommunityResponse>(msg);
break;
}
case UserOperation.BanFromCommunity: {
const {
person_view: {
person: { id: personId },
},
banned,
} = wsJsonToRes<BanFromCommunityResponse>(msg);
// TODO this might be incorrect // TODO this might be incorrect
this.state.posts this.state.posts
.filter(p => p.creator.id == data.person_view.person.id) .filter(p => p.creator.id === personId)
.forEach(p => (p.creator_banned_from_community = data.banned)); .forEach(p => (p.creator_banned_from_community = banned));
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetComments) {
let data = wsJsonToRes<GetCommentsResponse>(msg); break;
this.setState({ comments: data.comments, commentsLoading: false }); }
} else if (
op == UserOperation.EditComment || case UserOperation.GetComments: {
op == UserOperation.DeleteComment || const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
op == UserOperation.RemoveComment this.setState({ comments, listingsLoading: false });
) {
let data = wsJsonToRes<CommentResponse>(msg); break;
editCommentRes(data.comment_view, this.state.comments); }
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); this.setState(this.state);
} else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg); break;
}
case UserOperation.CreateComment: {
const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
// Necessary since it might be a user reply // Necessary since it might be a user reply
if (data.form_id) { if (form_id) {
this.state.comments.unshift(data.comment_view); this.setState(({ comments }) => ({
this.setState(this.state); comments: [comment_view].concat(comments),
}));
} }
} else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg); break;
saveCommentRes(data.comment_view, this.state.comments); }
case UserOperation.SaveComment: {
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
saveCommentRes(comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg); break;
createCommentLikeRes(data.comment_view, this.state.comments); }
case UserOperation.CreateCommentLike: {
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
createCommentLikeRes(comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg); break;
}
case UserOperation.BlockPerson: {
const data = wsJsonToRes<BlockPersonResponse>(msg);
updatePersonBlock(data); updatePersonBlock(data);
} else if (op == UserOperation.CreatePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg); break;
}
case UserOperation.CreatePostReport:
case UserOperation.CreateCommentReport: {
const data = wsJsonToRes<PostReportResponse>(msg);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.CreateCommentReport) {
let data = wsJsonToRes<CommentReportResponse>(msg); break;
if (data) {
toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.PurgeCommunity) {
let data = wsJsonToRes<PurgeItemResponse>(msg); case UserOperation.PurgeCommunity: {
if (data.success) { const { success } = wsJsonToRes<PurgeItemResponse>(msg);
if (success) {
toast(i18n.t("purge_success")); toast(i18n.t("purge_success"));
this.context.router.history.push(`/`); this.context.router.history.push(`/`);
} }
} else if (op == UserOperation.BlockCommunity) {
let data = wsJsonToRes<BlockCommunityResponse>(msg); break;
}
case UserOperation.BlockCommunity: {
const data = wsJsonToRes<BlockCommunityResponse>(msg);
if (res) { if (res) {
res.community_view.blocked = data.blocked; res.community_view.blocked = data.blocked;
}
updateCommunityBlock(data);
this.setState(this.state); this.setState(this.state);
} }
updateCommunityBlock(data);
break;
}
}
}
} }
} }

View file

@ -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

View file

@ -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={res.person_view.person.bio} description={personRes.person_view.person.bio}
image={res.person_view.person.avatar} image={personRes.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">
{this.moderates()} <Moderates moderates={personRes.moderates} />
{this.amCurrentUser && this.follows()} {this.amCurrentUser && <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,95 +704,139 @@ 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 {
switch (op) {
case UserOperation.GetPersonDetails: {
// Since the PersonDetails contains posts/comments as well as some general user info we listen here as well // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
// and set the parent state if it is not set or differs // and set the parent state if it is not set or differs
// TODO this might need to get abstracted // TODO this might need to get abstracted
let data = wsJsonToRes<GetPersonDetailsResponse>(msg); const data = wsJsonToRes<GetPersonDetailsResponse>(msg);
this.setState({ personRes: data, loading: false }); this.setState({ personRes: data, loading: false });
this.setPersonBlock(); this.setPersonBlock();
restoreScrollPosition(this.context); restoreScrollPosition(this.context);
} else if (op == UserOperation.AddAdmin) {
let data = wsJsonToRes<AddAdminResponse>(msg); break;
this.setState(s => ((s.siteRes.admins = data.admins), s)); }
} else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg); case UserOperation.AddAdmin: {
createCommentLikeRes(data.comment_view, this.state.personRes?.comments); const { admins } = wsJsonToRes<AddAdminResponse>(msg);
this.setState(s => ((s.siteRes.admins = admins), s));
break;
}
case UserOperation.CreateCommentLike: {
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
createCommentLikeRes(comment_view, this.state.personRes?.comments);
this.setState(this.state); this.setState(this.state);
} else if (
op == UserOperation.EditComment || break;
op == UserOperation.DeleteComment || }
op == UserOperation.RemoveComment
) { case UserOperation.EditComment:
let data = wsJsonToRes<CommentResponse>(msg); case UserOperation.DeleteComment:
editCommentRes(data.comment_view, this.state.personRes?.comments); case UserOperation.RemoveComment: {
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
editCommentRes(comment_view, this.state.personRes?.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg); break;
let mui = UserService.Instance.myUserInfo; }
if (data.comment_view.creator.id == mui?.local_user_view.person.id) {
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")); toast(i18n.t("reply_sent"));
} }
} else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg); break;
saveCommentRes(data.comment_view, this.state.personRes?.comments); }
case UserOperation.SaveComment: {
const { comment_view } = wsJsonToRes<CommentResponse>(msg);
saveCommentRes(comment_view, this.state.personRes?.comments);
this.setState(this.state); this.setState(this.state);
} else if (
op == UserOperation.EditPost || break;
op == UserOperation.DeletePost || }
op == UserOperation.RemovePost ||
op == UserOperation.LockPost || case UserOperation.EditPost:
op == UserOperation.FeaturePost || case UserOperation.DeletePost:
op == UserOperation.SavePost case UserOperation.RemovePost:
) { case UserOperation.LockPost:
let data = wsJsonToRes<PostResponse>(msg); case UserOperation.FeaturePost:
editPostFindRes(data.post_view, this.state.personRes?.posts); case UserOperation.SavePost: {
const { post_view } = wsJsonToRes<PostResponse>(msg);
editPostFindRes(post_view, this.state.personRes?.posts);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg); break;
createPostLikeFindRes(data.post_view, this.state.personRes?.posts); }
case UserOperation.CreatePostLike: {
const { post_view } = wsJsonToRes<PostResponse>(msg);
createPostLikeFindRes(post_view, this.state.personRes?.posts);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BanPerson) {
let data = wsJsonToRes<BanPersonResponse>(msg); break;
let res = this.state.personRes; }
case UserOperation.BanPerson: {
const data = wsJsonToRes<BanPersonResponse>(msg);
const res = this.state.personRes;
res?.comments res?.comments
.filter(c => c.creator.id == data.person_view.person.id) .filter(c => c.creator.id === data.person_view.person.id)
.forEach(c => (c.creator.banned = data.banned)); .forEach(c => (c.creator.banned = data.banned));
res?.posts res?.posts
.filter(c => c.creator.id == data.person_view.person.id) .filter(c => c.creator.id === data.person_view.person.id)
.forEach(c => (c.creator.banned = data.banned)); .forEach(c => (c.creator.banned = data.banned));
let pv = res?.person_view; const pv = res?.person_view;
if (pv?.person.id == data.person_view.person.id) { if (pv?.person.id === data.person_view.person.id) {
pv.person.banned = data.banned; pv.person.banned = data.banned;
} }
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg); break;
}
case UserOperation.BlockPerson: {
const data = wsJsonToRes<BlockPersonResponse>(msg);
updatePersonBlock(data); updatePersonBlock(data);
this.setPersonBlock(); this.setPersonBlock();
this.setState(this.state);
} else if ( break;
op == UserOperation.PurgePerson || }
op == UserOperation.PurgePost ||
op == UserOperation.PurgeComment || case UserOperation.PurgePerson:
op == UserOperation.PurgeCommunity case UserOperation.PurgePost:
) { case UserOperation.PurgeComment:
let data = wsJsonToRes<PurgeItemResponse>(msg); case UserOperation.PurgeCommunity: {
if (data.success) { const { success } = wsJsonToRes<PurgeItemResponse>(msg);
if (success) {
toast(i18n.t("purge_success")); toast(i18n.t("purge_success"));
this.context.router.history.push(`/`); this.context.router.history.push(`/`);
} }
} }
} }
} }
}
}

View file

@ -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) { const searchPersonOptions: Choice[] = [];
this.blockPersonChoices = new Choices(selectId, choicesConfig);
this.blockPersonChoices.passedElement.element.addEventListener( if (text.length > 0) {
"choice", searchPersonOptions.push(
(e: any) => { ...(await fetchUsers(text)).users.map(personToChoice)
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() { this.setState({
if (isBrowser()) { searchPersonLoading: false,
let selectId: any = document.getElementById("block-community-filter"); searchPersonOptions,
if (selectId) { });
this.blockCommunityChoices = new Choices(selectId, choicesConfig); });
this.blockCommunityChoices.passedElement.element.addEventListener(
"choice", handleCommunitySearch = debounce(async (text: string) => {
(e: any) => { this.setState({ searchCommunityLoading: true });
this.handleBlockCommunity(Number(e.detail.choice.value));
}, const searchCommunityOptions: Choice[] = [];
false
if (text.length > 0) {
searchCommunityOptions.push(
...(await fetchCommunities(text)).communities.map(communityToChoice)
); );
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) { this.setState({
let auth = myAuth(); searchCommunityLoading: false,
if (auth && personId != 0) { searchCommunityOptions,
let blockUserForm: BlockPerson = { });
person_id: personId, });
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) {

View file

@ -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, auth,
}; };
WebSocketService.Instance.send(wsClient.getCommunity(form)); WebSocketService.Instance.send(wsClient.getCommunity(form));
} }
} else { }
let listCommunitiesForm: ListCommunities = {
type_: ListingType.All, componentDidMount(): void {
sort: SortType.TopAll, const { communityId } = getCreatePostQueryParams();
limit: fetchLimit,
auth, if (communityId?.toString() !== this.state.selectedCommunityChoice?.value) {
}; this.fetchCommunity();
WebSocketService.Instance.send( } else if (!communityId) {
wsClient.listCommunities(listCommunitiesForm) 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
communities={res.communities}
onCreate={this.handlePostCreate} onCreate={this.handlePostCreate}
params={this.params} params={locationState}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages} allLanguages={this.state.siteRes.all_languages}
siteLanguages={this.state.siteRes.discussion_languages} siteLanguages={this.state.siteRes.discussion_languages}
selectedCommunityChoice={selectedCommunityChoice}
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>[] = [];
if (communityId) {
const form: GetCommunity = {
auth,
id: getIdFromString(communityId),
}; };
return [req.client.listCommunities(listCommunitiesForm)];
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,
}); });
} }

View file

@ -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()) {
let selectId: any = document.getElementById("post-community");
if (selectId) {
this.choices = new Choices(selectId, choicesConfig);
this.choices.passedElement.element.addEventListener(
"choice",
(e: any) => {
this.setState(
s => ((s.form.community_id = Number(e.detail.choice.value)), s)
);
},
false
);
this.choices.passedElement.element.addEventListener("search", () => {
this.setState({ communitySearchLoading: true }); this.setState({ communitySearchLoading: true });
const newOptions: Choice[] = [];
if (selectedCommunityChoice) {
newOptions.push(selectedCommunityChoice);
}
if (text.length > 0) {
newOptions.push(
...(await fetchCommunities(text)).communities.map(communityToChoice)
);
this.setState({
communitySearchOptions: newOptions,
}); });
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; this.setState({
this.setState(s => ((s.form.community_id = pv?.community.id), s)); communitySearchLoading: false,
});
});
let nameOrId = this.props.params?.nameOrId; handleCommunitySelect(choice: Choice) {
if (nameOrId) { if (this.props.onSelectCommunity) {
if (typeof nameOrId === "string") { this.setState({
let name_ = nameOrId; loading: true,
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.props.onSelectCommunity(choice);
this.choices.setChoiceByValue(this.state.form.community_id.toString());
this.setState({ loading: false });
} }
this.setState(this.state);
} }
parseMessage(msg: any) { parseMessage(msg: any) {

View file

@ -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

View file

@ -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 {

View file

@ -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`,

View file

@ -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,13 +1476,16 @@ 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) {
case SortType.Active:
case SortType.Hot:
return CommentSortType.Hot; return CommentSortType.Hot;
} else if ([SortType.New, SortType.NewComments].includes(sort)) { case SortType.New:
case SortType.NewComments:
return CommentSortType.New; return CommentSortType.New;
} else if (sort == SortType.Old) { case SortType.Old:
return CommentSortType.Old; return CommentSortType.Old;
} else { default:
return CommentSortType.Top; 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}`,
"?"
);
}

View file

@ -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"